milter-0.8.18/0002755000160600001450000000000012121400433011756 5ustar stuartbmsmilter-0.8.18/dkim-milter.rc0000755000160600001450000000266011744673207014554 0ustar stuartbms#!/bin/bash # # dkim-milter This shell script starts and stops dkim-milter. # # chkconfig: 2345 80 30 # description: a process that checks DKIM for messages sent through sendmail. # processname: dkim-milter # config: /etc/mail/dkim-milter.cfg # pidfile: /var/run/milter/dkim-milter.pid python="python2.6" pidof() { set - "" if set - `ps -e -o pid,cmd | grep "${python} dkim-milter.py"` && [ "$2" != "grep" ]; then echo $1 return 0 fi return 1 } # Source function library. . /etc/rc.d/init.d/functions [ -x /usr/lib/pymilter/start.sh ] || exit 0 RETVAL=0 prog="dkim-milter" start() { # Start daemons. echo -n "Starting $prog: " if ! test -d /var/run/milter; then mkdir -p /var/run/milter chown mail:mail /var/run/milter fi daemon --check milter --user mail /usr/lib/pymilter/start.sh dkim-milter RETVAL=$? echo [ $RETVAL -eq 0 ] && touch /var/lock/subsys/dkim-milter return $RETVAL } stop() { # Stop daemons. echo -n "Shutting down $prog: " killproc milter RETVAL=$? echo [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/dkim-milter return $RETVAL } # See how we were called. case "$1" in start) start ;; stop) stop ;; restart|reload) stop start RETVAL=$? ;; condrestart) if [ -f /var/lock/subsys/dkim-milter ]; then stop start RETVAL=$? fi ;; status) status dkim-milter RETVAL=$? ;; *) echo "Usage: $0 {start|stop|restart|condrestart|status}" exit 1 esac exit $RETVAL milter-0.8.18/dkim-milter.py0000644000160600001450000002150112117502775014564 0ustar stuartbms# A simple DKIM milter. # You must install pydkim/dkimpy for this to work. # http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html # Author: Stuart D. Gathman # Copyright 2007 Business Management Systems, Inc. # This code is under GPL. See COPYING for details. import sys import Milter import dkim from dkim.dnsplug import get_txt from dkim.util import parse_tag_value import authres import logging import logging.config import os import tempfile import StringIO import re from Milter.config import MilterConfigParser from Milter.utils import iniplist,parse_addr,parseaddr class Config(object): "Hold configuration options." pass def read_config(list): "Return new config object." for fn in list: if os.access(fn,os.R_OK): logging.config.fileConfig(fn) break cp = MilterConfigParser() cp.read(list) if cp.has_option('milter','datadir'): os.chdir(cp.get('milter','datadir')) conf = Config() conf.log = logging.getLogger('dkim-milter') conf.log.info('logging started') conf.socketname = cp.getdefault('milter','socketname', '/tmp/dkimmiltersock') conf.miltername = cp.getdefault('milter','name','pydkimfilter') conf.internal_connect = cp.getlist('milter','internal_connect') # DKIM section if cp.has_option('dkim','privkey'): conf.keyfile = cp.getdefault('dkim','privkey') conf.selector = cp.getdefault('dkim','selector','default') conf.domain = cp.getdefault('dkim','domain') conf.reject = cp.getdefault('dkim','reject') if conf.keyfile and conf.domain: try: with open(conf.keyfile,'r') as kf: conf.key = kf.read() except: conf.log.error('Unable to read: %s',conf.keyfile) return conf FWS = re.compile(r'\r?\n[ \t]+') class dkimMilter(Milter.Base): "Milter to check and sign DKIM. Each connection gets its own instance." def log(self,*msg): self.conf.log.info('[%d] %s' % (self.id,' '.join([str(m) for m in msg]))) def __init__(self): self.mailfrom = None self.id = Milter.uniqueID() # we don't want config used to change during a connection self.conf = config self.fp = None @Milter.noreply def connect(self,hostname,unused,hostaddr): self.internal_connection = False self.hello_name = None # sometimes people put extra space in sendmail config, so we strip self.receiver = self.getsymval('j').strip() if hostaddr and len(hostaddr) > 0: ipaddr = hostaddr[0] if iniplist(ipaddr,self.conf.internal_connect): self.internal_connection = True else: ipaddr = '' self.connectip = ipaddr if self.internal_connection: connecttype = 'INTERNAL' else: connecttype = 'EXTERNAL' self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype)) return Milter.CONTINUE # multiple messages can be received on a single connection # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # of each message. @Milter.noreply def envfrom(self,f,*str): self.log("mail from",f,str) self.fp = StringIO.StringIO() self.mailfrom = f t = parse_addr(f) if len(t) == 2: t[1] = t[1].lower() self.canon_from = '@'.join(t) self.user = self.getsymval('{auth_authen}') self.has_dkim = False self.author = None self.arheaders = [] self.arresults = [] if self.user: # Very simple SMTP AUTH policy by default: # any successful authentication is considered INTERNAL self.internal_connection = True auth_type = self.getsymval('{auth_type}') ssl_bits = self.getsymval('{cipher_bits}') self.log( "SMTP AUTH:",self.user,"sslbits =",ssl_bits, auth_type, "ssf =",self.getsymval('{auth_ssf}'), "INTERNAL" ) # Detailed authorization policy is configured in the access file below. self.arresults.append( authres.SMTPAUTHAuthenticationResult(result = 'pass', result_comment = auth_type+' sslbits='+ssl_bits, smtp_auth = self.user) ) return Milter.CONTINUE @Milter.noreply def header(self,name,val): lname = name.lower() if not self.has_dkim and lname == 'dkim-signature': self.log("%s: %s" % (name,val)) self.has_dkim = True if lname == 'from': fname,self.author = parseaddr(val) self.log("%s: %s" % (name,val)) elif lname == 'authentication-results': self.arheaders.append(val) if self.fp: self.fp.write("%s: %s\n" % (name,val)) return Milter.CONTINUE @Milter.noreply def eoh(self): if self.fp: self.fp.write("\n") # terminate headers self.bodysize = 0 return Milter.CONTINUE @Milter.noreply def body(self,chunk): # copy body to temp file if self.fp: self.fp.write(chunk) # IOError causes TEMPFAIL in milter self.bodysize += len(chunk) return Milter.CONTINUE def eom(self): if not self.fp: return Milter.ACCEPT # no message collected - so no eom processing # lookup Author Domain Signing Policy, if any adsp = { 'dkim': 'unknown' } if self.author: author_domain = self.author.split('@',1)[-1] s = get_txt('_adsp._domainkey.'+author_domain) if s: m = parse_tag_value(s) if m.has_key('dkim'): self.log(s) adsp = m # Remove existing Authentication-Results headers for our authserv_id for i,val in enumerate(self.arheaders,1): # FIXME: don't delete A-R headers from trusted MTAs ar = authres.AuthenticationResultsHeader.parse_value(FWS.sub('',val)) if ar.authserv_id == self.receiver: self.chgheader('authentication-results',i,'') self.log('REMOVE: ',val) # Check or sign DKIM self.fp.seek(0) if self.internal_connection: txt = self.fp.read() self.sign_dkim(txt) result = None elif self.has_dkim: txt = self.fp.read() if self.check_dkim(txt): result = 'pass' else: result = 'fail' self.arresults.append( authres.DKIMAuthenticationResult(result=result, header_i = self.header_i, header_d = self.header_d, result_comment = self.dkim_comment) ) else: result = 'none' # Check if local reject policy and ADSP indicate message should be rejected lp = self.conf.reject # local policy if lp and result and result != 'pass': p = adsp['dkim'] # author domain policy if lp == p or p == 'discardable' and lp == 'all': if result == 'none': t = 'Missing' else: t = 'Invalid' self.setreply('550','5.7.1', '%s DKIM signature for %s with ADSP dkim=%s'%(t,self.author,p)) self.log('REJECT: %s DKIM signature'%t) return Milter.REJECT if self.arresults: h = authres.AuthenticationResultsHeader(authserv_id = self.receiver, results=self.arresults) self.log(h) name,val = str(h).split(': ',1) self.addheader(name,val,0) return Milter.CONTINUE def sign_dkim(self,txt): conf = self.conf try: d = dkim.DKIM(txt,logger=conf.log) h = d.sign(conf.selector,conf.domain,conf.key, canonicalize=('relaxed','simple')) name,val = h.split(': ',1) self.addheader(name,val.strip().replace('\r\n','\n'),0) except dkim.DKIMException as x: self.log('DKIM: %s'%x) except Exception as x: conf.log.error("sign_dkim: %s",x,exc_info=True) def check_dkim(self,txt): res = False conf = self.conf d = dkim.DKIM(txt,logger=conf.log) try: res = d.verify() if res: self.dkim_comment = 'Good %d bit signature.' % d.keysize else: self.dkim_comment = 'Bad %d bit signature.' % d.keysize except dkim.DKIMException as x: self.dkim_comment = str(x) #self.log('DKIM: %s'%x) except Exception as x: self.dkim_comment = str(x) conf.log.error("check_dkim: %s",x,exc_info=True) self.header_i = d.signature_fields.get(b'i') self.header_d = d.signature_fields.get(b'd') if res: #self.log('DKIM: Pass (%s)'%d.domain) self.dkim_domain = d.domain else: fd,fname = tempfile.mkstemp(".dkim") with os.fdopen(fd,"w+b") as fp: fp.write(txt) self.log('DKIM: Fail (saved as %s)'%fname) return res if __name__ == "__main__": Milter.factory = dkimMilter Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS) global config config = read_config(['dkim-milter.cfg','/etc/mail/dkim-milter.cfg']) miltername = config.miltername socketname = config.socketname print """To use this with sendmail, add the following to sendmail.cf: O InputMailFilters=%s X%s, S=local:%s See the sendmail README for libmilter. sample dkim-milter startup""" % (miltername,miltername,socketname) sys.stdout.flush() Milter.runmilter(miltername,socketname,240) print "sample dkim-milter shutdown" milter-0.8.18/testbms.py0000644000160600001450000002160712117520463014030 0ustar stuartbmsimport unittest import doctest import Milter import bms from Milter.test import TestBase import mime import rfc822 import StringIO import email import sys #import pdb def DNSLookup(name,qtype,strict=True,timeout=None): return [] try: import spf except: spf = None class TestMilter(TestBase,bms.bmsMilter): def __init__(self): TestBase.__init__(self) bms.config = bms.Config() bms.config.access_file = 'test/access.db' bms.bmsMilter.__init__(self) self.setsymval('j','test.milter.org') # disable SPF for now if spf: spf.DNSLookup = DNSLookup class BMSMilterTestCase(unittest.TestCase): ## Test SMTP AUTH feature. def testAuth(self): milter = TestMilter() # Try a SMTP authorized user from an unauthorized IP, that is # authorized to use example.com milter.setsymval('{auth_authen}','good') milter.setsymval('{cipher_bits}','256') milter.setsymval('{auth_ssf}','0') rc = milter.connect('testAuth',ip='192.0.3.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='grief@example.com') self.assertEqual(rc,Milter.ACCEPT) # Try a user *not* authorized to use example.com milter.setsymval('{auth_authen}','bad') rc = milter.connect('testAuth',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@example.com') self.assertEqual(rc,Milter.REJECT) # Try to break it by using an implicit domain milter.setsymval('{auth_authen}','bad') rc = milter.connect('testAuth',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good') # unlike spfmilter, bms milter doesn't check AUTH for implicit domain self.assertEqual(rc,Milter.ACCEPT) milter.close() def testDefang(self,fname='virus1'): milter = TestMilter() rc = milter.connect('testDefang') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg(fname) self.assertEqual(rc,Milter.ACCEPT) self.failUnless(milter._bodyreplaced,"Message body not replaced") fp = milter._body open('test/'+fname+".tstout","w").write(fp.getvalue()) #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) fp.seek(0) msg = mime.message_from_file(fp) str = msg.get_payload(1).get_payload() milter.log(str) milter.close() # test some spams that crashed our parser def testParse(self,fname='spam7'): milter = TestMilter() milter.connect('testParse') rc = milter.feedMsg(fname) self.assertEqual(rc,Milter.ACCEPT) self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") fp = milter._body open('test/'+fname+".tstout","w").write(fp.getvalue()) milter.connect('pro-send.com') rc = milter.feedMsg('spam8') self.assertEqual(rc,Milter.ACCEPT) self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") rc = milter.feedMsg('bounce') self.assertEqual(rc,Milter.ACCEPT) self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") rc = milter.feedMsg('bounce1') self.assertEqual(rc,Milter.ACCEPT) self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") milter.close() def testDefang2(self): milter = TestMilter() milter.connect('testDefang2') rc = milter.feedMsg('samp1') self.assertEqual(rc,Milter.ACCEPT) self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") rc = milter.feedMsg("virus3") self.assertEqual(rc,Milter.ACCEPT) self.failUnless(milter._bodyreplaced,"Message body not replaced") fp = milter._body open("test/virus3.tstout","w").write(fp.getvalue()) #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read()) rc = milter.feedMsg("virus6") self.assertEqual(rc,Milter.ACCEPT) self.failUnless(milter._bodyreplaced,"Message body not replaced") self.failUnless(milter._headerschanged,"Message headers not adjusted") fp = milter._body open("test/virus6.tstout","w").write(fp.getvalue()) milter.close() def testDefang3(self): milter = TestMilter() milter.connect('testDefang3') # test script removal on complex HTML attachment rc = milter.feedMsg('amazon') self.assertEqual(rc,Milter.ACCEPT) self.failUnless(milter._bodyreplaced,"Message body not replaced") fp = milter._body open("test/amazon.tstout","w").write(fp.getvalue()) # test defanging Klez virus rc = milter.feedMsg("virus13") self.assertEqual(rc,Milter.ACCEPT) self.failUnless(milter._bodyreplaced,"Message body not replaced") fp = milter._body open("test/virus13.tstout","w").write(fp.getvalue()) # test script removal on quoted-printable HTML attachment # sgmllib can't handle the syntax rc = milter.feedMsg('spam44') self.assertEqual(rc,Milter.ACCEPT) self.failIf(milter._bodyreplaced,"Message body replaced") fp = milter._body open("test/spam44.tstout","w").write(fp.getvalue()) milter.close() def testRFC822(self): milter = TestMilter() milter.connect('testRFC822') # test encoded rfc822 attachment #pdb.set_trace() rc = milter.feedMsg('test8') self.assertEqual(rc,Milter.ACCEPT) # python2.4 doesn't scan encoded message attachments if sys.hexversion < 0x02040000: self.failUnless(milter._bodyreplaced,"Message body not replaced") #self.failIf(milter._bodyreplaced,"Message body replaced") fp = milter._body open("test/test8.tstout","w").write(fp.getvalue()) rc = milter.feedMsg('virus7') self.assertEqual(rc,Milter.ACCEPT) self.failUnless(milter._bodyreplaced,"Message body not replaced") #self.failIf(milter._bodyreplaced,"Message body replaced") fp = milter._body open("test/virus7.tstout","w").write(fp.getvalue()) def testSmartAlias(self): milter = TestMilter() milter.connect('testSmartAlias') # test smart alias feature key = ('foo@example.com','baz@bat.com') bms.smart_alias[key] = ['ham@eggs.com'] rc = milter.feedMsg('test8',key[0],key[1]) self.assertEqual(rc,Milter.ACCEPT) self.failUnless(milter._delrcpt == ['']) self.failUnless(milter._addrcpt == ['']) # python2.4 email does not decode message attachments, so script # is not replaced if sys.hexversion < 0x02040000: self.failUnless(milter._bodyreplaced,"Message body not replaced") def testBadBoundary(self): milter = TestMilter() milter.connect('testBadBoundary') # test rfc822 attachment with invalid boundaries #pdb.set_trace() rc = milter.feedMsg('bound') if sys.hexversion < 0x02040000: # python2.4 adds invalid boundaries to decects list and makes # payload a str self.assertEqual(rc,Milter.REJECT) self.assertEqual(milter._reply[0],'554') #self.failUnless(milter._bodyreplaced,"Message body not replaced") self.failIf(milter._bodyreplaced,"Message body replaced") fp = milter._body open("test/bound.tstout","w").write(fp.getvalue()) def testCompoundFilename(self): milter = TestMilter() milter.connect('testCompoundFilename') # test rfc822 attachment with invalid boundaries #pdb.set_trace() rc = milter.feedMsg('test1') self.assertEqual(rc,Milter.ACCEPT) #self.failUnless(milter._bodyreplaced,"Message body not replaced") self.failIf(milter._bodyreplaced,"Message body replaced") fp = milter._body open("test/test1.tstout","w").write(fp.getvalue()) def testFindsrs(self): if not bms.srs: import SRS bms.srs = SRS.new(secret='test') sender = bms.srs.forward('foo@bar.com','mail.example.com') sndr = bms.findsrs(StringIO.StringIO( """Received: from [1.16.33.86] (helo=mail.example.com) by bastion4.mail.zen.co.uk with smtp (Exim 4.50) id 1H3IBC-00013b-O9 for foo@bar.com; Sat, 06 Jan 2007 20:30:17 +0000 X-Mailer: "PyMilter-0.8.5" <%s> foo MIME-Version: 1.0 Content-Type: text/plain To: foo@bar.com From: postmaster@mail.example.com """ % sender )) self.assertEqual(sndr,'foo@bar.com') def testBanned(self): bd = set(('*.foo.bar','*.info','baz.bar')) self.assertTrue(bms.isbanned('bif.foo.bar',bd)) self.assertFalse(bms.isbanned('bif.foo.com',bd)) self.assertTrue(bms.isbanned('foo.info',bd)) self.assertFalse(bms.isbanned('foo.baz.bar',bd)) self.assertTrue(bms.isbanned('baz.bar',bd)) # def testReject(self): # "Test content based spam rejection." # milter = TestMilter() # milter.connect('gogo-china.com') # rc = milter.feedMsg('big5'); # self.failUnless(rc == Milter.REJECT) # milter.close(); def suite(): s = unittest.makeSuite(BMSMilterTestCase,'test') s.addTest(doctest.DocTestSuite(bms)) return s if __name__ == '__main__': if len(sys.argv) > 1: for fname in sys.argv[1:]: milter = TestMilter() milter.connect('main') fp = open(fname,'r') rc = milter.feedFile(fp) fp = milter._body sys.stdout.write(fp.getvalue()) else: #unittest.main() unittest.TextTestRunner().run(suite()) milter-0.8.18/testspf.py0000644000160600001450000001633612117767020014044 0ustar stuartbms#!/usr/bin/python2.6 import unittest import Milter import spfmilter #import spfmartin as spfmilter import spf from Milter.test import TestBase import sys zonedata = { } def DNSLookup(name,qtype,strict=True,timeout=None): try: #print name,qtype timeout = True # emulate pydns-2.3.0 label processing a = [] for label in name.split('.'): if label: if len(label) > 63: raise spf.TempError('DNS label too long') a.append(label) name = '.'.join(a) for i in zonedata[name.lower()]: if i == 'TIMEOUT': if timeout: raise spf.TempError('DNS timeout') return t,v = i if t == qtype: timeout = False if v == 'TIMEOUT': if t == qtype: raise spf.TempError('DNS timeout') continue # keep test zonedata human readable, but translate to simulate pydns if t == 'AAAA': v = spf.inet_pton(v) elif type(v) == unicode: v = v.encode('utf-8') yield ((name,t),v) except KeyError: if name.startswith('error.'): raise spf.TempError('DNS timeout') spf.DNSLookup = DNSLookup class TestMilter(TestBase,spfmilter.spfMilter): def __init__(self): TestBase.__init__(self) spfmilter.config = spfmilter.Config() spfmilter.config.access_file = 'test/access.db' spfmilter.spfMilter.__init__(self) #self.setsymval('j','test.milter.org') zonedata = { 'example.com': [ ('TXT', ('v=spf1 ip4:192.0.2.1',)) ], 'n.example.com': [ ('TXT', ('v=spf1 ip4:192.0.2.1',)) ], 'bad.example.com': [ ('TXT', ('v=spf1 a:192.0.2.1',)) ], 'fail.example.com': [ ('TXT', ('v=spf1 ip4:192.0.2.2 -all',)) ], 'mail.example.com': [ ('TXT', ('v=spf1 ip4:192.0.2.1 -all',)) ] } class SPFMilterTestCase(unittest.TestCase): def testPolicy(self): p = spfmilter.SPFPolicy('good@example.com',access_file='test/access.db') pol = p.getPolicy('smtp-auth:') p.close() self.assertEqual(pol,'OK') p = spfmilter.SPFPolicy('bad@example.com',access_file='test/access.db') pol = p.getPolicy('smtp-auth:') p.close() self.assertEqual(pol,'REJECT') p = spfmilter.SPFPolicy('bad@bad.example.com',access_file='test/access.db') pol = p.getPolicy('smtp-auth:') p.close() self.assertEqual(pol,None) p = spfmilter.SPFPolicy('any@random.com',access_file='test/access.db') pol = p.getPolicy('smtp-test:') p.close() self.assertEqual(pol,'REJECT') def testPass(self): milter = TestMilter() rc = milter.connect('mail.example.com',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@example.com') self.assertEqual(rc,Milter.CONTINUE) milter.close() def testNeutral(self): milter = TestMilter() # SPF result is Neutral, default access policy for example.com is REJECT rc = milter.connect('mail.example.com',ip='192.0.2.2') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@example.com') self.assertEqual(rc,Milter.REJECT) # SPF result is None, default policy is OK rc = milter.connect('mail.example.com',ip='192.0.2.3') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='whatever@random.com') self.assertEqual(rc,Milter.CONTINUE) milter.close() def testHelo(self): milter = TestMilter() # Reject numeric HELO rc = milter.connect('testHelo',helo='1.2.3.4',ip='192.0.3.1') self.assertEqual(rc,Milter.REJECT) # HELO Neutral allowed by access policy rc = milter.connect('testHelo',helo='example.com',ip='192.0.3.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@random.com') self.assertEqual(rc,Milter.CONTINUE) # HELO Neutral gets REJECT by default rc = milter.connect('testHelo',helo='n.example.com',ip='192.0.3.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@random.com') self.assertEqual(rc,Milter.REJECT) milter.close() def testFail(self): milter = TestMilter() # Reject HELO SPF Fail when domain has no policy rc = milter.connect(helo='fail.example.com',ip='192.0.3.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@random.com') self.assertEqual(rc,Milter.REJECT) # HELO SPF Fail overridden by MAIL FROM Pass rc = milter.connect(helo='fail.example.com',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@example.com') self.assertEqual(rc,Milter.CONTINUE) # HELO SPF Pass overridden by MAIL FROM Fail rc = milter.connect(helo='mail.example.com',ip='192.0.2.2') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@fail.example.com') self.assertEqual(rc,Milter.CONTINUE) milter.close() def testPermerror(self): milter = TestMilter() rc = milter.connect('mail.example.com',helo='bad.example.com', ip='192.0.2.2') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@example.com') # reject permerror on helo self.assertEqual(rc,Milter.REJECT) rc = milter.connect('mail.example.com',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='clueless@bad.example.com') self.assertEqual(rc,Milter.REJECT) self.assertEqual(milter._reply[0],'550') self.assertEqual(milter._reply[1],'5.5.2') rc = milter.connect('mail.example.com',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='foo@bad.example.com') # ignore permerror for particular localpart self.assertEqual(rc,Milter.CONTINUE) milter.close() ## Test SMTP AUTH feature. def testAuth(self): milter = TestMilter() # Try a SMTP authorized user from an unauthorized IP, that is # authorized to use example.com milter.setsymval('{auth_authen}','good') milter.setsymval('{cipher_bits}','256') milter.setsymval('{auth_ssf}','0') rc = milter.connect('mail.example.com',ip='192.0.3.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='grief@example.com') self.assertEqual(rc,Milter.CONTINUE) # Try a user *not* authorized to use example.com milter.setsymval('{auth_authen}','bad') rc = milter.connect('mail.example.com',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good@example.com') self.assertEqual(rc,Milter.REJECT) # Try to break it by using an implicit domain milter.setsymval('{auth_authen}','bad') rc = milter.connect('mail.example.com',ip='192.0.2.1') self.assertEqual(rc,Milter.CONTINUE) rc = milter.feedMsg('test1',sender='good') self.assertEqual(rc,Milter.REJECT) milter.close() def suite(): s = unittest.makeSuite(SPFMilterTestCase,'test') return s if __name__ == '__main__': #unittest.main() import os cmd = None if os.access('test/access',os.R_OK): if not os.path.exists('test/access.db') or \ os.path.getmtime('test/access') > os.path.getmtime('test/access.db'): cmd = 'makemap hash test/access.db 4: subdict[id] = a[4].rstrip() elif op == 'connect': ipdict[id] = a[4].rstrip() elif op in ('eom','dspam'): if id in subdict: del subdict[id] if id in ipdict: del ipdict[id] elif op in ('REJECT:','DSPAM:','SPAM:','abort'): if id in subdict: if id in ipdict: ip = ipdict[id] del ipdict[id] f,host,raw = ip.split(None,2) if host in spamcnt: spamcnt[host] += 1 else: spamcnt[host] = 1 else: ip = '' print dt,tm,op,a[4].rstrip(),subdict[id] del subdict[id] else: print line.rstrip() print len(subdict),'leftover entries' spamlist = filter(lambda x: x[1] > 1,spamcnt.items()) spamlist.sort(lambda x,y: x[1] - y[1]) for ip,cnt in spamlist: print cnt,ip milter-0.8.18/CREDITS0000644000160600001450000000345712117467204013023 0ustar stuartbmsJim Niemira (urmane@urmane.org) wrote the original C module and some quick and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that kludge and added threading and context objects to it, wrote a proper OO wrapper (Milter.py) that handles attachments, did lots of testing, packaged it with distutils, and generally transformed it from a quick hack to a real, usable Python extension. Other contributors (in random order): mvcstroomer for reporting that the SMTP AUTH feature was broken on spfmilter.py Dwayne Litzenberger, B.A.Sc. for library_dirs patch to compile on Debian Dave MacQuigg for noticing that smfi_insheader wasn't supported, and creating a template to help first time pymilter users create their own milter. Terence Way for providing a Python port of SPF Scott Kitterman for doing lots of testing and debugging of SPF against draft standard, and for putting up a web page that validates SPF records using spf.py Alexander Kourakos for plugging several memory leaks George Graf at Vienna University of Economics and Business Administration for handling None passed to setreply and chgheader. Deron Meranda for IPv6 patches Jason Erikson for handling NULL hostaddr in connect callback. John Draper for porting Python milter to OpenBSD, and starting to work on tutorials then pointing out that it would be easier to just write the MTA in Python. Eric S. Johansson for helpful design discussions while working on camram Alex Savguira for finding bugs with international headers and suggesting the scan_zip option. Business Management Systems - http://www.bmsi.com for hosting the website, and providing paying clients who need milter service so I can work on it as part of my day job. If I have left anybody out, send me a reminder: stuart@bmsi.com milter-0.8.18/milter.cfg0000644000160600001450000002762412121400316013744 0ustar stuartbms[milter] # the directory with persistent data files datadir = /var/lib/milter logdir = /var/log/milter # the socket used to communicate with sendmail. Must match sendmail.cf socket=/var/run/milter/pythonsock # where to save original copies of defanged and failed messages tempdir = /var/log/milter/save # how long to wait for a response from sendmail before giving up ;timeout=600 log_headers = 0 # Connection ips and hostnames are matched against this glob style list # to recognize internal senders. You probably need to change this. # The default is a good guess to try and prevent newbie frustration. internal_connect = 192.168.0.0/16,127.* # mail that is not an internal_connect and claims to be from an # internal domain is rejected. Furthermore, internal mail that # does not claim to be from an internal domain is rejected. # You should enable SPF instead if you can. SPF is much more comprehensive and # flexible. However, SPF is not currently checked for outgoing # (internal_connect) mail because it doesn't yet handle authorizing # internal IPs locally. ;internal_domains = mycorp.com,localhost.localdomain # connections from a trusted relay can trust the first Received header # SPF checks are bypassed for internal connections and trusted relays. ;trusted_relay = 1.2.3.4, 66.12.34.56 # Relaying to these domains is allowed from internal connections only. # You might want to restrict aol.com, for instance, so that stupid # users don't forward their spam to aol for filtering and get your MTA # blacklisted by aol. ;private_relay = aol.com, yahoo.com # If this is defined, internal connections not in this list are # not allowed to send DSNs (empty MAIL FROM). We could check that # a purported MTA accepts connections on port 25, but that could be # time consuming with firewalls typically discarding rather than rejecting. ;internal_mta = 192.168.1.2 # Reject external senders with hello names no legit external sender would use. # SPF will do this also, but listing your own domain and mailserver here # will save some DNS lookups when rejecting certain viruses, and the # connect IP is banned. ;hello_blacklist = mycorp.com, 66.12.34.56 # Reject mail for domains mentioned unless user is mentioned here also ;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com # Treat localparts in milter.cfg as case-sensitive. Set to false to handle # mailers that violate RFCs by failing to preserve case. (And make # sure sendmail is configured to ignore case.) case_sensitive_localpart = true # Various nasty MTA behaviours get demerits. When they reach this limit # on a single connection, the IP is banned. Leave unset for # "unlimited" (actually 2**31-1). I use 3. ;max_demerits = 3 # When a domain in this list would get banned, the specific mailbox # is banned instead. These free email providers have a "whack-a-mole" problem. email_providers = yahoo.com, gmail.com, aol.com, hotmail.com, me.com, googlegroups.com, att.net # features intended to filter or block incoming mail [defang] # do virus scanning on attached messages also scan_rfc822 = 0 # do virus scanning on attached zipfiles also scan_zip = 0 # Comment out scripts in HTML attachments. Can be CPU intensive. scan_html = 0 # reject messages with asian fonts because we can't read them block_chinese = 0 # list users who hate forwarded mail ;block_forward = egghead@mycorp.com, busybee@mycorp.com # reject mail with these case insensitive strings in the subject porn_words = penis, pussy, horse cock, porn, xenical, diet pill, d1ck, vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck, xanaax, p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam, v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra, x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin, valium, rolex, sexual, fuck, adv1t, vgaira, medz, acai berry # reject mail with these case sensitive strings in the subject spam_words = $$$, !!!, XXX, HGH # Experimental: Reject mail with these case sensitive strings in the human # readable part of the From header field. Also, ban the domain if it gets # a pass from SPF or best_guess and is a "new" domain according to # pygossip. This helps generate a list of "throwaway" spam domains. ;from_words = Stimulus, Diabetes, Trivia, Age Quiz, Detox, White Teeth # attachments with these extensions will be replaced with a warning # message. A copy of the original will be saved. banned_exts = ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta, inf,ins,isp,js,jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct, shs,url,vb,vbe,vbs,wsc,wsf,wsh # See http://bmsi.com/python/pysrs.html for details [srs] config=/etc/mail/pysrs.cfg # SRS options can be set here also, but must match the sendmail plugin ;secret="shhhh!" ;maxage=21 ;hashlength=4 ;database=/var/log/milter/srsdata ;fwdomain = mydomain.com # turn this on after a grace period to reject spoofed DSNs reject_spoofed = 0 # Many braindead MTAs send DSNs with a non-DSN MFROM (e.g. to report that # some virus claiming to be sent by you). This heuristic # refuses mail from user names commonly abused in that way. ;banned_users = postmaster, mailer-daemon, clamav # See http://www.openspf.com for more info on SPF. [spf] # namespace where SPF records can be supplied for domains without one # records are searched for under _spf.domain.com ;delegate = domain.com # domains where a neutral SPF result should cause mail to be rejected ;reject_neutral = aol.com # use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published ;best_guess = 0 # Reject senders that have neither PTR nor valid HELO nor SPF records, or send # DSN otherwise ;reject_noptr = 0 # always accept softfail from these domains, or send DSN otherwise ;accept_softfail = bounces.amazon.com # Treat fail from these domains like softfail: because their SPF record # or an important sender is screwed up. Must have valid HELO, however. ;accept_fail = custhelp.com # Use sendmail access map or similar format for detailed spf policy. # SPF entries in the access map will override any defaults set above. # Uncomment to activate. ;access_file = /etc/mail/access.db # Add MAIL FROM as Sender when Sender is missing and From domain # doesn't match MAIL FROM. Outlook and other email clients will then display # something like: "Sent by sender@domain.com on behalf of from@example.com" ;supply_sender = 0 # Connections that get an SPF pass for a pretend MAIL FROM of # postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM. # This is for non-SRS forwarders. It is a simple implementation that # is inefficient for more than a few entries. ;trusted_forwarder = careerbuilder.com # features intended to clean up outgoing mail [scrub] # domains that block visible private nodes ;hide_path = jcpenney.com # reject, don't just replace with warning, viruses from these domains ;reject_virus_from = mycorp.com # Apply SPF-Pass policy to internal mail, to check CBV, for instance. ;internal_policy = false # features intended for spying on users and coworkers [wiretap] blind = 1 # # wiretap lets you surreptitiously monitor a users outgoing email # (sendmail aliases let you monitor incoming mail) # ;users = disloyal@bigcorp.com, bigmouth@bigcorp.com # multiple destinations can use smart_alias ;dest = spy@bigcorp.com # discard outgoing mail without alerting sender # can be used in conjunction with wiretap to censor outgoing mail ;discard_users = canned@bigcorp.com # archive copies all delivered mail to a file ;mail_archive = /var/log/mail_archive # # smart aliases trigger on both sender and recipient # alias = sender, recipient[, destination] # [smart_alias] # multiple wiretap monitors. Smart aliases are applied after wiretap. ;spy1 = disloyal@bigcorp.com,spy@bigcorp.com ;spy2 = bigmouth@bigcorp.com,spy@bigcorp.com # mail from client@clientcorp.com to sue@bigcorp.com is redirected to # local alias copycust ;copycust = client@clientcorp.com,sue@bigcorp.com # mail from cust@othercorp.com to walter@bigcorp.com is redirected to # boss@bigcorp.com ;walter = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com # additional copies can be added ;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com, ; walter@bigcorp.com ;bulk = soruce@telex.com,bob@bigcorp.com ;bulk1 = soruce@telex.com,larry@bigcorp.com,bulk # See http://bmsi.com/python/dspam.html [dspam] # Select a well moderated dspam dictionary to reject spammy headers. # To filter on the entire message, use the full setup below. # only EXTERNAL messages are dspam filtered ;dspam_dict=/var/lib/dspam/moderator.dict # Recipients of mail sent from these senders are added to the auto_whitelist. # Auto_whitelisted senders with an SPF PASS are never rejected by dspam, and # messages from auto_whitelisted senders will be used to train screener # dictionaries as innocent mail. ;whitelist_senders = @mycorp.com # Also send auto_whitelist recipients to these MXes. This is not currently # parallelized, so list just one, or at most two. ;whitelist_mx = mail.mycorp.com,mail2.mycorp.com # Opt-out recipients entirely from dspam screening and header triage ;dspam_exempt=getitall@mycorp.com # Do not scan mail (ostensibly) from these senders ;dspam_whitelist=getitall@sender.com # Reject spam to these domains instead of quarantining it. ;dspam_reject=othercorp.com # Scan internal mail - often a good source of stats on legit mail. ;dspam_internal=1 # directory for dspam user quarantine, signature db, and dictionaries # defining this activates the dspam application # dspam and dspam-python must be installed ;dspam_userdir=/var/lib/dspam # do not dspam messages larger than this ;dspam_sizelimit=180000 # Map email addresses and aliases to dspam users ;dspam_users=david,goliath,spam,falsepositive # List dspam users which train on all delivered messages, as opposed to # "train on error" which trains only when a spam or falsepositive is reported. # Training mode will build the dictionary faster, but requires close attention # so as not to miss any spam or false positives. ;dspam_train=goliath ;david=david@foocorp.com,david.yelnetz@foocorp.com,david@bar.foocorp.com ;goliath=giant@foocorp.com,goliath.philistine@foocorp.com # address to forward spam to. milter will process these and not deliver ;spam=spam@foocorp.com # Spam forwarded to bandom will ban the domain after training as spam. # This helps deal with "throw-away" domains. The ban1 localpart # uses a wildcard for the first part of the banned domain, so that # instead of banning mx2.spammer.com, it bans *.spammer.com. # See email_providers above. ;bandom=bandom@mail.example.com,ban1@mail.example.com # address to forward false positives to. milter will process and not deliver ;falsepositive=ham@foocorp.com # account which receives only spam: all received messages are marked as spam. ;honeypot=spam-me@example.com # the dspam_screener is a list of dspam users who screen mail for all # recipients who are not dspam_users. Spam goes to the screeners quarantine, # and the original recipients are saved so that false positives can be properly # delivered. ;dspam_screener=david,goliath # The dspam CGI can also be used: logins must match dspam users # Optional pygossip interface # # GOSSiP tracks reputation of domain:qualifier pairs. For instance, # the reputation of example.com:SPF is tracked separately from # example.com:neutral. Currently qualifiers are # SPF,neutral,softfail,fail,permerror,GUESS,HELO [gossip] # Use a dedicated GOSSiP server. If not specified, a local database # will be used. ;server=host:11900 # To include peers of a peer in reputation, set ttl=2 ;ttl=1 # If a local database is used, also consult these GOSSiP servers about # domains. Peer reputation is also tracked as to how often they # agree with us, and weighted accordingly. ;peers=host1:port,host2 [greylist] dbfile=greylist.db # mins (Google retries in 5 mins) time=5 # hours (some legit sites don't retry for 6 hours) expire=6 # days (keep "first monday" type mailings on file) retain=36 [dkim] privkey = dkim_rsa ;domain = example.com ;selector = default milter-0.8.18/spfmilter.rc0000755000160600001450000000265210651702423014331 0ustar stuartbms#!/bin/bash # # spfmilter This shell script takes care of starting and stopping spfmilter. # # chkconfig: 2345 80 30 # description: a process that checks SPF for messages sent through sendmail. # processname: spfmilter # config: /etc/mail/spfmilter.cfg # pidfile: /var/run/milter/spfmilter.pid python="python2.4" pidof() { set - "" if set - `ps -e -o pid,cmd | grep "${python} spfmilter.py"` && [ "$2" != "grep" ]; then echo $1 return 0 fi return 1 } # Source function library. . /etc/rc.d/init.d/functions [ -x /usr/lib/pymilter/start.sh ] || exit 0 RETVAL=0 prog="spfmilter" start() { # Start daemons. echo -n "Starting $prog: " if ! test -d /var/run/milter; then mkdir -p /var/run/milter chown mail:mail /var/run/milter fi daemon --check milter --user mail /usr/lib/pymilter/start.sh spfmilter RETVAL=$? echo [ $RETVAL -eq 0 ] && touch /var/lock/subsys/spfmilter return $RETVAL } stop() { # Stop daemons. echo -n "Shutting down $prog: " killproc milter RETVAL=$? echo [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/spfmilter return $RETVAL } # See how we were called. case "$1" in start) start ;; stop) stop ;; restart|reload) stop start RETVAL=$? ;; condrestart) if [ -f /var/lock/subsys/spfmilter ]; then stop start RETVAL=$? fi ;; status) status spfmilter RETVAL=$? ;; *) echo "Usage: $0 {start|stop|restart|condrestart|status}" exit 1 esac exit $RETVAL milter-0.8.18/ban2zone.py0000644000160600001450000000227011535313066014062 0ustar stuartbms#!/usr/bin/python2.4 import socket import struct import sys from glob import glob def coverage(banned_ips,min=128): lastcnet = None bits = 0 bitcnt = 0 for ip in (struct.unpack('!I',i)[0] for i in banned_ips): cnet = ip & 0xFFFFFF00 if cnet != lastcnet: if lastcnet is not None: if bitcnt > min: yield lastcnet,bitcnt,bits bits = 0L bitcnt = 0 lastcnet = cnet bit = ip & 0xFF bits |= 1L< min: yield lastcnet,bitcnt,bits banned_ips = set(socket.inet_aton(ip) for fn in sys.argv[1:] for ip in open(fn)) banned_ips.difference_update(socket.inet_aton(ip) for ip in open('whitelist_ips')) ips = list(banned_ips) ips.sort() for cnet,bitcnt,bits in coverage(ips,128): ip = socket.inet_ntoa(struct.pack('!I',cnet)).split('.') ip[-1] = '*' ip.reverse() print "%s\tIN A 127.0.0.2 ; %d ips banned"%('.'.join(ip),bitcnt) banned_ips.difference_update(struct.pack('!I',cnet + i) for i in range(256)) del ips banned_ips = list(banned_ips) banned_ips.sort() for ip in banned_ips: a = socket.inet_ntoa(ip).split('.') a.reverse() print "%s\tIN A 127.0.0.2"%('.'.join(a)) milter-0.8.18/softfail.txt0000644000160600001450000000140310531113705014330 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: SPF %(result)s (POSSIBLE FORGERY) Auto-Submitted: auto-generated (configuration error) This is an automatically generated Delivery Status Notification. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Received-SPF: %(spf_result)s Your sender policy indicated that the above email was likely forged and that feedback was desired for debugging. If you are sending from a foreign ISP, then you may need to follow your home ISPs instructions for configuring your outgoing mail server. If you need further assistance, please do not hesitate to contact me. Kind regards, postmaster@%(receiver)s milter-0.8.18/makefile0000644000160600001450000000025112116746205013470 0ustar stuartbmsVERS=milter-0.8.18 V=milter-0_8_18 tar: cvs export -r $(V) -d $(VERS) milter tar cvf $(VERS).tar $(VERS) gzip -v $(VERS).tar rm -rf $(VERS) tag: cvs tag -F $(V) milter-0.8.18/test.py0000644000160600001450000000043712117520463013324 0ustar stuartbmsimport unittest import testbms import testspf import os def suite(): s = unittest.TestSuite() s.addTest(testspf.suite()) s.addTest(testbms.suite()) return s if __name__ == '__main__': try: os.remove('test/milter.log') except: pass unittest.TextTestRunner().run(suite()) milter-0.8.18/bms.py0000644000160600001450000027032212121400316013115 0ustar stuartbms#!/usr/bin/env python # A simple milter that has grown quite a bit. # $Log: bms.py,v $ # Revision 1.195 2013/03/17 17:43:42 customdesigned # Default logdir to datadir. # # Revision 1.194 2013/03/15 23:04:38 customdesigned # Move many configs to datadir # # Revision 1.193 2013/03/12 03:29:55 customdesigned # Add SMTP AUTH test case for bms milter. # # Revision 1.192 2013/03/12 01:33:49 customdesigned # Tab nanny # # Revision 1.191 2013/03/09 23:51:11 customdesigned # Move email_providers to config. Move many other configs to Config object. # # Revision 1.185 2012/07/13 21:05:57 customdesigned # Don't check banned ips on submission port (587). # # Revision 1.184 2012/05/24 18:26:43 customdesigned # Log unknown commands. # # Revision 1.181 2012/04/19 23:20:05 customdesigned # Simple DKIM signing support # # Revision 1.180 2012/04/12 05:37:25 customdesigned # Skip greylisting for trusted forwarders # # Revision 1.179 2012/02/25 22:21:16 customdesigned # Support urls with fuller explanation for rejections. # # Revision 1.178 2011/11/05 16:05:08 customdesigned # Change openspf.org -> openspf.net # # Revision 1.177 2011/11/01 17:43:33 customdesigned # Trust trusted relay not to be a zombie. # # Revision 1.176 2011/10/03 20:07:16 customdesigned # Make wiretap use orig_from (set efrom to orig_from earlier). # # Revision 1.175 2011/10/03 20:01:00 customdesigned # Let NOTIFY suppress real DSN. Since notify is forged on forged email, # perhaps this should be an option. # # Revision 1.174 2011/10/03 17:44:38 customdesigned # Fix SPF fail ip reported for IP6 # # Revision 1.173 2011/06/16 20:54:48 customdesigned # Update to pydkim-0.4 and log verified domains # # Revision 1.172 2011/06/07 22:24:38 customdesigned # Remove leftover tempname in envfrom. Save failed DKIM # # Revision 1.171 2011/06/07 19:45:01 customdesigned # Check DKIM (log only) # # Revision 1.170 2011/05/18 02:50:54 customdesigned # Improve chinese detection # # Revision 1.169 2011/04/13 19:50:04 customdesigned # Move persistent data to /var/lib/milter # # Revision 1.168 2011/04/01 02:34:38 customdesigned # Fix efrom and umis with delayed reject. # # Revision 1.166 2011/03/05 05:12:55 customdesigned # Release 0.8.15 # # Revision 1.165 2011/03/03 21:45:24 customdesigned # Extract original MFROM from SRS # # Revision 1.164 2010/10/27 03:07:33 customdesigned # Whitelist recipients from signed MFROM if aborted before DATA. # # Revision 1.163 2010/10/16 21:23:00 customdesigned # Send auto-whitelist recipients to whitelist_mx # # Revision 1.162 2010/08/18 03:58:06 customdesigned # Fix typos # # Revision 1.161 2010/08/18 03:52:09 customdesigned # Update reputation of parsed Received-SPF header if no Gossip header. # # Revision 1.160 2010/06/04 03:50:28 customdesigned # Allow wildcards just above TLD. # # Revision 1.159 2010/05/27 21:22:41 customdesigned # Fix helo policy lookup # # Revision 1.158 2010/05/27 18:23:33 customdesigned # Support HELO policies. # # Revision 1.157 2010/05/22 03:57:17 customdesigned # Bandomain aliases to ban wildcards. # # Revision 1.155 2010/04/16 19:51:05 customdesigned # Max_demerits config to disable banning ips. # # Revision 1.154 2010/04/09 18:37:53 customdesigned # Best guess pass is good enough to get quarrantine DSN or get banned. # # Revision 1.153 2010/04/09 18:23:34 customdesigned # Don't ban just for repeated anonymous MFROM # # Revision 1.152 2010/02/15 21:02:29 customdesigned # Lower reputation bar to avoid greylisting. # # Revision 1.151 2009/12/31 19:23:18 customdesigned # Don't check From unless dspam enabled. # # Revision 1.150 2009/12/30 20:53:20 customdesigned # Require pymilter >= 0.9.3 # # Revision 1.149 2009/09/14 14:59:53 customdesigned # Allow illegal HELO from internal to accomodate broken copier firmware. # # Revision 1.148 2009/09/14 14:28:22 customdesigned # Heuristically recognize multiple MXs. # # Revision 1.147 2009/09/14 14:24:11 customdesigned # Trust 127.0.0.1 not to be a zombie # # Revision 1.146 2009/08/29 03:38:27 customdesigned # Don't ban domains unless gossip score available. # # Revision 1.145 2009/08/27 21:18:16 customdesigned # Track banned domains and expand the offenses that can ban an IP. # # Revision 1.141 2009/05/20 02:48:18 customdesigned # Restrict internal DSNs to official MTAs. # # Revision 1.140 2009/02/04 02:40:14 customdesigned # Parse gossip header before add_spam. Replace nulls in smtp error txt. # Default internal_policy flag false. # # Revision 1.139 2008/12/22 02:34:39 customdesigned # Fix internal policy # # Revision 1.138 2008/12/13 21:22:51 customdesigned # Split off pymilter # # Revision 1.137 2008/12/06 21:13:57 customdesigned # Fix some reject messages. # # Revision 1.136 2008/12/04 19:42:46 customdesigned # SPF Pass policy # # Revision 1.135 2008/10/23 19:58:06 customdesigned # Example config had different names than actual code :-) # # Revision 1.134 2008/10/11 15:45:46 customdesigned # Don't greylist DSNs. # # Revision 1.133 2008/10/09 18:44:54 customdesigned # Skip greylisting for good reputation. # # Revision 1.132 2008/10/09 00:55:13 customdesigned # Don't reset greylist timer on early retries. # # Revision 1.131 2008/10/08 04:57:28 customdesigned # Greylisting # # Revision 1.130 2008/10/02 03:19:00 customdesigned # Delay strike3 REJECT and don't reject if whitelisted. # Recognize vacation messages as autoreplies. # # Revision 1.129 2008/09/09 23:24:56 customdesigned # Never ban a trusted relay. # # Revision 1.128 2008/09/09 23:08:16 customdesigned # Wasn't reading banned_ips # # Revision 1.127 2008/08/25 18:32:22 customdesigned # Handle missing gossip_node so self tests pass. # # Revision 1.126 2008/08/18 17:47:57 customdesigned # Log rcpt for SRS rejections. # # Revision 1.125 2008/08/06 00:52:38 customdesigned # CBV policy sends no DSN. DSN policy sends DSN. # # Revision 1.124 2008/08/05 18:04:06 customdesigned # Send quarantine DSN to SPF PASS only. # # Revision 1.123 2008/07/29 21:59:29 customdesigned # Parse ESMTP params # # Revision 1.122 2008/05/08 21:35:56 customdesigned # Allow explicitly whitelisted email from banned_users. # # Revision 1.121 2008/04/10 14:59:35 customdesigned # Configure gossip TTL. # # Revision 1.120 2008/04/02 18:59:14 customdesigned # Release 0.8.10 # # Revision 1.119 2008/04/01 00:13:10 customdesigned # Do not CBV whitelisted addresses. We already know they are good. # # Revision 1.118 2008/01/09 20:15:49 customdesigned # Handle unquoted fullname when parsing email. # # Revision 1.117 2007/11/29 14:35:17 customdesigned # Packaging tweaks. # # Revision 1.116 2007/11/01 20:09:14 customdesigned # Support temperror policy in access. # # Revision 1.115 2007/10/10 18:23:54 customdesigned # Send quarantine DSN to SPF pass (official or guessed) only. # Reject blacklisted email too big for dspam. # # Revision 1.114 2007/10/10 18:07:50 customdesigned # Check porn keywords in From header field. # # Revision 1.113 2007/09/25 16:37:26 customdesigned # Tested on RH7 # # Revision 1.112 2007/09/13 14:51:03 customdesigned # Report domain on reputation reject. # # Revision 1.111 2007/07/25 17:14:59 customdesigned # Move milter apps to /usr/lib/pymilter # # Revision 1.110 2007/07/02 03:06:10 customdesigned # Ban ips on bad mailfrom offenses as well as bad rcpts. # # Revision 1.109 2007/06/23 20:53:05 customdesigned # Ban IPs based on too many invalid recipients in a connection. Requires # configuring check_user. Tighten HELO best_guess policy. # # Revision 1.108 2007/04/19 16:02:43 customdesigned # Do not process valid SRS recipients as delayed_failure. # # Revision 1.107 2007/04/15 01:01:13 customdesigned # Ban ips with too many bad rcpts on a connection. # # Revision 1.105 2007/04/13 17:20:09 customdesigned # Check access_file at startup. Compress rcpt to log. # # Revision 1.104 2007/04/05 17:59:07 customdesigned # Stop querying gossip server twice. # # Revision 1.103 2007/04/02 18:37:25 customdesigned # Don't disable gossip for temporary error. # # Revision 1.102 2007/03/30 18:13:41 customdesigned # Report bestguess and helo-spf as key-value pairs in Received-SPF # instead of in their own headers. # # Revision 1.101 2007/03/29 03:06:10 customdesigned # Don't count DSN and unqualified MAIL FROM as internal_domain. # # Revision 1.100 2007/03/24 00:30:24 customdesigned # Do not CBV for internal domains. # # See ChangeLog # # Author: Stuart D. Gathman # Copyright 2001,2002,2003,2004,2005-2010 Business Management Systems, Inc. # This code is under the GNU General Public License. See COPYING for details. import sys import os import os.path import StringIO import mime import email.Errors import Milter import tempfile import time import socket import re import shutil import gc import anydbm import smtplib import urllib import Milter.dsn as dsn from Milter.dynip import is_dynip as dynip from Milter.utils import \ iniplist,parse_addr,parse_header,ip4re,addr2bin,parseaddr from Milter.config import MilterConfigParser from Milter.greysql import Greylist from fnmatch import fnmatchcase from email.Utils import getaddresses from glob import glob # Import gossip if available try: import gossip import gossip.client import gossip.server gossip_node = None except: gossip = None # Import pysrs if available try: import SRS srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE) except: SRS = None try: import SES except: SES = None # Import spf if available try: import spf except: spf = None # Import dkim if available try: import dkim except: dkim = None # Import authres if available try: import authres except: authres = None # Sometimes, MTAs reply to our DSN. We recognize this type of reply/DSN # and check for the original recipient SRS encoded in Message-ID. # If found, we blacklist that recipient. _subjpats = ( r'^failure notice', r'^subjectbounce', r'^returned mail', r'^undeliver', r'\bdelivery\b.*\bfail', r'\bdelivery problem', r'\bnot\s+be\s+delivered', r'\buser unknown\b', r'^failed', r'^mail failed', r'^echec de distribution', r'\berror\s+sending\b', r'^fallo en la entrega', r'\bfehlgeschlagen\b' ) refaildsn = re.compile('|'.join(_subjpats),re.IGNORECASE) # We don't want to whitelist recipients of Autoreplys and other robots. # There doesn't seem to be a foolproof way to recognize these, so # we use this heuristic. The worst that can happen is someone won't get # whitelisted when they should, or we'll whitelist some spammer for a while. _autopats = ( r'^read:', r'\bautoreply:\b', r'^return receipt', r'^Your message\b.*\bawaits moderator approval' ) reautoreply = re.compile('|'.join(_autopats),re.IGNORECASE) import logging # Thanks to Chris Liechti for config parsing suggestions class Config(object): def __init__(self): ## True if greylisting is activated self.greylist = False ## List email providers which should have mailboxes banned, not domain. self.email_providers = ( 'yahoo.com','gmail.com','aol.com','hotmail.com','me.com', 'googlegroups.com', 'att.net', 'nokiamail.com' ) self.access_file = None ## URL of CGI to display enhanced error diagnostics via web. self.errors_url = "http://bmsi.com/cgi-bin/errors.cgi" self.dkim_domain = None self.dkim_key = None self.dkim_selector = 'default' ## Banned case sensitive Subject keywords self.spam_words = () ## Banned case insensitive Subject keywords self.porn_words = () ## Banned keywords in From: header self.from_words = () ## Internal senders which should whitelist recipients self.whitelist_senders = {} ## Send whitelisted recipients to this MX for consolidation self.whitelist_mx = () ## Ban these HELO names (usually local domains) self.hello_blacklist = () ## Treat these MAIL FROM mailboxes as DSNs - for braindead MTAs. self.banned_users = () ## Log header fields self.log_headers = False ## Data directory, or '' to use logdir self.datadir = '' def getGreylist(self): if not self.greylist: return None greylist = getattr(local,'greylist',None) if not greylist: grey_db = os.path.join(self.datadir,self.grey_db) greylist = Greylist(grey_db,self.grey_time, self.grey_expire,self.grey_days) local.greylist = greylist return greylist config = Config() # Global configuration defaults suitable for test framework. socketname = "/tmp/pythonsock" reject_virus_from = () delayed_reject = True wiretap_users = {} discard_users = {} wiretap_dest = None mail_archive = None _archive_lock = None blind_wiretap = True check_user = {} block_forward = {} hide_path = () block_chinese = False case_sensitive_localpart = False banned_exts = mime.extlist.split(',') scan_zip = False scan_html = True scan_rfc822 = True internal_policy = False internal_connect = () trusted_relay = () private_relay = () internal_mta = () trusted_forwarder = () internal_domains = () smart_alias = {} dspam_dict = None dspam_users = {} dspam_train = {} dspam_userdir = None dspam_exempt = {} dspam_whitelist = {} dspam_screener = () dspam_internal = True # True if internal mail should be dspammed dspam_reject = () dspam_sizelimit = 180000 srs = None ses = None srs_reject_spoofed = False srs_domain = () spf_reject_neutral = () spf_accept_softfail = () spf_accept_fail = () spf_best_guess = False spf_reject_noptr = False supply_sender = False timeout = 600 banned_ips = set() banned_domains = set() UNLIMITED = 0x7fffffff max_demerits = UNLIMITED logging.basicConfig( stream=sys.stdout, level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%Y%b%d %H:%M:%S' ) milter_log = logging.getLogger('milter') import threading local = threading.local() ## Read config files. # Only some configs are returned in a Config object. Most are still # globals set as a side effect. The intent is to migrate them over time. # @return Config def read_config(list): cp = MilterConfigParser({ 'tempdir': "/var/log/milter/save", 'datadir': "/var/lib/milter", 'socket': "/var/run/milter/pythonsock", 'errors_url': "http://bmsi.com/cgi-bin/errors.cgi", 'scan_html': 'no', 'scan_rfc822': 'yes', 'scan_zip': 'no', 'block_chinese': 'no', 'log_headers': 'no', 'blind_wiretap': 'yes', 'reject_spoofed': 'no', 'reject_noptr': 'no', 'supply_sender': 'no', 'best_guess': 'no', 'dspam_internal': 'yes', 'case_sensitive_localpart': 'no', 'internal_policy': 'no' }) cp.read(list) config = Config() # old configs have datadir for both logging and data config.datadir = cp.getdefault('milter','datadir','') config.logdir = cp.getdefault('milter','logdir',config.datadir) # config reference files are in datadir by default if config.datadir: print "chdir:",config.datadir os.chdir(config.datadir) # milter section tempfile.tempdir = cp.get('milter','tempdir') global socketname, timeout, check_user global internal_connect, internal_domains, trusted_relay global case_sensitive_localpart, private_relay, internal_mta, max_demerits socketname = cp.get('milter','socket') timeout = cp.getintdefault('milter','timeout',600) check_user = cp.getaddrset('milter','check_user') config.log_headers = cp.getboolean('milter','log_headers') internal_connect = cp.getlist('milter','internal_connect') internal_domains = cp.getlist('milter','internal_domains') trusted_relay = cp.getlist('milter','trusted_relay') private_relay = cp.getlist('milter','private_relay') internal_mta = cp.getlist('milter','internal_mta') config.hello_blacklist = cp.getlist('milter','hello_blacklist') case_sensitive_localpart = cp.getboolean('milter','case_sensitive_localpart') max_demerits = cp.getintdefault('milter','max_demerits',UNLIMITED) config.errors_url = cp.get('milter','errors_url') if cp.has_option('milter','email_providers'): config.email_providers = cp.get('milter','email_providers') # defang section global scan_rfc822, scan_html, block_chinese, scan_zip, block_forward global banned_exts if cp.has_section('defang'): section = 'defang' # for backward compatibility, # banned extensions defaults to empty only when defang section exists banned_exts = cp.getlist(section,'banned_exts') else: # use milter section if no defang section for compatibility section = 'milter' scan_rfc822 = cp.getboolean(section,'scan_rfc822') scan_zip = cp.getboolean(section,'scan_zip') scan_html = cp.getboolean(section,'scan_html') block_chinese = cp.getboolean(section,'block_chinese') block_forward = cp.getaddrset(section,'block_forward') config.porn_words = [x for x in cp.getlist(section,'porn_words') if len(x) > 1] config.spam_words = [x for x in cp.getlist(section,'spam_words') if len(x) > 1] from_words = [x for x in cp.getlist(section,'from_words') if len(x) > 1] if len(from_words) == 1 and from_words[0].startswith("file:"): with open(from_words[0][5:],'r') as fp: from_words = [s.strip() for s in fp.readlines()] from_words = [s for s in from_words if len(s) > 2] config.from_words = from_words # scrub section global hide_path, reject_virus_from, internal_policy hide_path = cp.getlist('scrub','hide_path') reject_virus_from = cp.getlist('scrub','reject_virus_from') internal_policy = cp.getboolean('scrub','internal_policy') # wiretap section global blind_wiretap,wiretap_users,wiretap_dest,discard_users,mail_archive blind_wiretap = cp.getboolean('wiretap','blind') wiretap_users = cp.getaddrset('wiretap','users') discard_users = cp.getaddrset('wiretap','discard') wiretap_dest = cp.getdefault('wiretap','dest') if wiretap_dest: wiretap_dest = '<%s>' % wiretap_dest mail_archive = cp.getdefault('wiretap','archive') global smart_alias for sa,v in [ (k,cp.get('wiretap',k)) for k in cp.getlist('wiretap','smart_alias') ] + (cp.has_section('smart_alias') and cp.items('smart_alias',True) or []): print sa,v sm = [q.strip() for q in v.split(',')] if len(sm) < 2: milter_log.warning('malformed smart alias: %s',sa) continue if len(sm) == 2: sm.append(sa) if case_sensitive_localpart: key = (sm[0],sm[1]) else: key = (sm[0].lower(),sm[1].lower()) smart_alias[key] = sm[2:] # dspam section global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit config.whitelist_senders = cp.getaddrset('dspam','whitelist_senders') config.whitelist_mx = cp.getlist('dspam','whitelist_mx') dspam_dict = cp.getdefault('dspam','dspam_dict') dspam_exempt = cp.getaddrset('dspam','dspam_exempt') dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist') dspam_users = cp.getaddrdict('dspam','dspam_users') dspam_userdir = cp.getdefault('dspam','dspam_userdir') dspam_screener = cp.getlist('dspam','dspam_screener') dspam_train = set(cp.getlist('dspam','dspam_train')) dspam_reject = cp.getlist('dspam','dspam_reject') dspam_internal = cp.getboolean('dspam','dspam_internal') if cp.has_option('dspam','dspam_sizelimit'): dspam_sizelimit = cp.getint('dspam','dspam_sizelimit') # spf section global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr global spf_accept_softfail,spf_accept_fail,supply_sender global trusted_forwarder if spf: spf.DELEGATE = cp.getdefault('spf','delegate') spf_reject_neutral = cp.getlist('spf','reject_neutral') spf_accept_softfail = cp.getlist('spf','accept_softfail') spf_accept_fail = cp.getlist('spf','accept_fail') spf_best_guess = cp.getboolean('spf','best_guess') spf_reject_noptr = cp.getboolean('spf','reject_noptr') supply_sender = cp.getboolean('spf','supply_sender') config.access_file = cp.getdefault('spf','access_file') trusted_forwarder = cp.getlist('spf','trusted_forwarder') srs_config = cp.getdefault('srs','config') if srs_config: cp.read([srs_config]) srs_secret = cp.getdefault('srs','secret') if SRS and srs_secret: global ses,srs,srs_reject_spoofed,srs_domain database = cp.getdefault('srs','database') srs_reject_spoofed = cp.getboolean('srs','reject_spoofed') maxage = cp.getintdefault('srs','maxage',8) hashlength = cp.getintdefault('srs','hashlength',8) separator = cp.getdefault('srs','separator','=') if database: import SRS.DB srs = SRS.DB.DB(database=database,secret=srs_secret, maxage=maxage,hashlength=hashlength,separator=separator) else: srs = SRS.Guarded.Guarded(secret=srs_secret, maxage=maxage,hashlength=hashlength,separator=separator) if SES: ses = SES.new(secret=srs_secret,expiration=maxage) srs_domain = set(cp.getlist('srs','ses')) srs_domain.update(cp.getlist('srs','srs')) else: srs_domain = set(cp.getlist('srs','srs')) srs_domain.update(cp.getlist('srs','sign')) srs_domain.add(cp.getdefault('srs','fwdomain')) config.banned_users = cp.getlist('srs','banned_users') if gossip: global gossip_node, gossip_ttl if cp.has_option('gossip','server'): server = cp.get('gossip','server') host,port = gossip.splitaddr(server) gossip_node = gossip.client.Gossip(host,port) else: gossip_db = os.path.join(config.datadir,'gossip4.db') gossip_node = gossip.server.Gossip(gossip_db,1000) for p in cp.getlist('gossip','peers'): host,port = gossip.splitaddr(p) gossip_node.peers.append(gossip.server.Peer(host,port)) gossip_ttl = cp.getintdefault('gossip','ttl',1) # greylist section if cp.has_option('greylist','dbfile'): config.grey_db = cp.getdefault('greylist','dbfile') config.grey_days = cp.getintdefault('greylist','retain',36) config.grey_expire = cp.getintdefault('greylist','expire',6) config.grey_time = cp.getintdefault('greylist','time',5) config.greylist = True # DKIM section if cp.has_option('dkim','privkey'): dkim_keyfile = cp.getdefault('dkim','privkey') config.dkim_selector = cp.getdefault('dkim','selector','default') config.dkim_domain = cp.getdefault('dkim','domain') if dkim_keyfile and config.dkim_domain: try: with open(dkim_keyfile,'r') as kf: config.dkim_key = kf.read() except: milter_log.error('Unable to read: %s',dkim_keyfile) return config def findsrs(fp): lastln = None for ln in fp: if lastln: if ln[0].isspace() and ln[0] != '\n': lastln += ln continue try: name,val = lastln.rstrip().split(None,1) pos = val.find('= 0: end = val.find('>',pos+4) return srs.reverse(val[pos+1:end]) except: pass lnl = ln.lower() if lnl.startswith('action:'): if lnl.split()[-1] != 'failed': break for k in ('message-id:','x-mailer:','sender:','references:'): if lnl.startswith(k): lastln = ln break def inCharSets(v,*encs): try: u = unicode(v,'utf8') except: return True for enc in encs: try: s = u.encode(enc,'backslashreplace') return s.count(r'\u') < 3 except: UnicodeError return False def param2dict(str): pairs = [x.split('=',1) for x in str] for e in pairs: if len(e) < 2: e.append(None) return dict([(k.upper(),v) for k,v in pairs]) class SPFPolicy(object): "Get SPF/DKIM policy by result from sendmail style access file." def __init__(self,sender): self.sender = sender self.domain = sender.split('@')[-1].lower() if config.access_file: try: acf = anydbm.open(config.access_file,'r') except: acf = None else: acf = None self.acf = acf def close(self): if self.acf: self.acf.close() def getPolicy(self,pfx): acf = self.acf if not acf: return None try: return acf[pfx + self.sender] except KeyError: try: return acf[pfx + self.domain] except KeyError: try: return acf[pfx] except KeyError: return None def getFailPolicy(self): policy = self.getPolicy('spf-fail:') if not policy: if self.domain in spf_accept_fail: policy = 'CBV' else: policy = 'REJECT' return policy def getNonePolicy(self): policy = self.getPolicy('spf-none:') if not policy: if spf_reject_noptr: policy = 'REJECT' else: policy = 'CBV' return policy def getSoftfailPolicy(self): policy = self.getPolicy('spf-softfail:') if not policy: if self.domain in spf_accept_softfail: policy = 'OK' elif self.domain in spf_reject_neutral: policy = 'REJECT' else: policy = 'CBV' return policy def getNeutralPolicy(self): policy = self.getPolicy('spf-neutral:') if not policy: if self.domain in spf_reject_neutral: policy = 'REJECT' policy = 'OK' return policy def getPermErrorPolicy(self): policy = self.getPolicy('spf-permerror:') if not policy: policy = 'REJECT' return policy def getTempErrorPolicy(self): policy = self.getPolicy('spf-temperror:') if not policy: policy = 'REJECT' return policy def getPassPolicy(self): policy = self.getPolicy('spf-pass:') if not policy: policy = 'OK' return policy from Milter.cache import AddrCache cbv_cache = AddrCache(renew=7) auto_whitelist = AddrCache(renew=60) blacklist = AddrCache(renew=30) def isbanned(dom,s): if dom in s: return True a = dom.split('.') if a[0] == '*': a = a[1:] if len(a) < 2: return False a[0] = '*' return isbanned('.'.join(a),s) RE_MULTIMX = re.compile(r'^(mail|smtp|mx)[0-9]{1,3}[.]') class bmsMilter(Milter.Base): """Milter to replace attachments poisonous to Windows with a WARNING message, check SPF, and other anti-forgery features, and implement wiretapping and smart alias redirection.""" def log(self,*msg): milter_log.info('[%d] %s',self.id,' '.join([str(m) for m in msg])) def logstream(outerself): "Return a file like object that call self.log for each line" class LineWriter(object): def __init__(self): self._buf = '' def write(self,s): s = self._buf + s pos = s.find('\n') while pos >= 0: outerself.log(s[:pos]) s = s[pos+1:] pos = s.find('\n') self._buf = s return LineWriter() def __init__(self): self.tempname = None self.mailfrom = None # sender in SMTP form self.canon_from = None # sender in end user form self.fp = None self.pristine_headers = None self.bodysize = 0 self.id = Milter.uniqueID() # delrcpt can only be called from eom(). This accumulates recipient # changes which can then be applied by alter_recipients() def del_recipient(self,rcpt): rcpt = rcpt.lower() if not rcpt in self.discard_list: self.discard_list.append(rcpt) # addrcpt can only be called from eom(). This accumulates recipient # changes which can then be applied by alter_recipients() def add_recipient(self,rcpt): rcpt = rcpt.lower() if not rcpt in self.redirect_list: self.redirect_list.append(rcpt) # addheader can only be called from eom(). This accumulates added headers # which can then be applied by alter_headers() def add_header(self,name,val,idx=-1): self.new_headers.append((name,val,idx)) self.log('%s: %s' % (name,val)) def delay_reject(self,*args,**kw): if delayed_reject: self.reject = (args,kw) return Milter.CONTINUE self.htmlreply(*args,**kw) return Milter.REJECT def connect(self,hostname,unused,hostaddr): self.internal_connection = False self.trusted_relay = False self.reject = None self.offenses = 0 # sometimes people put extra space in sendmail config, so we strip self.receiver = self.getsymval('j').strip() dport = self.getsymval('{daemon_port}') if dport: self.dport = int(dport) else: self.dport = 0 if hostaddr and len(hostaddr) > 0: ipaddr = hostaddr[0] if iniplist(ipaddr,internal_connect): self.internal_connection = True if iniplist(ipaddr,trusted_relay): self.trusted_relay = True else: ipaddr = '' self.connectip = ipaddr self.missing_ptr = dynip(hostname,self.connectip) if self.internal_connection: connecttype = 'INTERNAL' else: connecttype = 'EXTERNAL' if self.trusted_relay: connecttype += ' TRUSTED' if self.missing_ptr: connecttype += ' DYN' self.log("connect from %s at %s:%s %s" % (hostname,hostaddr,dport,connecttype)) self.hello_name = None self.connecthost = hostname # Sendmail is normally configured so that only authenticated senders # are allowed to proceed to MAIL FROM on port 587. if self.dport != 587 and addr2bin(ipaddr) in banned_ips: self.log("REJECT: BANNED IP") return self.delay_reject('550','5.7.1', 'Banned for dictionary attacks') if hostname == 'localhost' and not ipaddr.startswith('127.') \ or hostname == '.': self.log("REJECT: PTR is",hostname) return self.delay_reject('550','5.7.1', '"%s" is not a reasonable PTR name'%hostname) return Milter.CONTINUE def hello(self,hostname): self.hello_name = hostname self.log("hello from %s" % hostname) if not self.internal_connection: # Allow illegal HELO from internal network, some email enabled copier/fax # type devices (Toshiba) have broken firmware. if ip4re.match(hostname): self.log("REJECT: numeric hello name:",hostname) self.setreply('550','5.7.1','hello name cannot be numeric ip') return Milter.REJECT if hostname in config.hello_blacklist: self.log("REJECT: spam from self:",hostname) self.setreply('550','5.7.1', 'Your mail server lies. Its name is *not* %s.' % hostname) return self.offense(inc=4) if hostname == 'GC': n = gc.collect() self.log("gc:",n,' unreachable objects') self.log("auto-whitelist:",len(auto_whitelist),' entries') self.log("cbv_cache:",len(cbv_cache),' entries') self.setreply('550','5.7.1','%d unreachable objects'%n) return Milter.REJECT # HELO not allowed after MAIL FROM if self.mailfrom: self.offense(inc=2) return Milter.CONTINUE def smart_alias(self,to): if smart_alias: if case_sensitive_localpart: t = parse_addr(to) else: t = parse_addr(to.lower()) if len(t) == 2: ct = '@'.join(t) else: ct = t[0] if case_sensitive_localpart: cf = self.efrom else: cf = self.efrom.lower() cf0 = cf.split('@',1) if len(cf0) == 2: cf0 = '@' + cf0[1] else: cf0 = cf for key in ((cf,ct),(cf0,ct)): if smart_alias.has_key(key): self.del_recipient(to) for t in smart_alias[key]: self.add_recipient('<%s>'%t) def offense(self,inc=1,min=0): self.offenses += inc if self.offenses < min: self.offenses = min if self.offenses > max_demerits and not self.trusted_relay: try: ip = addr2bin(self.connectip) if ip not in banned_ips: banned_ips.add(ip) print >>open('banned_ips','a'),self.connectip self.log("BANNED IP:",self.connectip) except: pass return Milter.REJECT # multiple messages can be received on a single connection # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # of each message. def envfrom(self,f,*str): self.log("mail from",f,str) #param = param2dict(str) #self.envid = param.get('ENVID',None) #self.mail_param = param self.fp = StringIO.StringIO() self.pristine_headers = StringIO.StringIO() if self.tempname: os.remove(self.tempname) # remove any leftover from previous message self.tempname = None self.mailfrom = f self.forward = True self.bodysize = 0 self.hidepath = False self.discard = False self.dspam = True self.whitelist = False self.blacklist = False self.greylist = False self.reject_spam = True self.data_allowed = True self.delayed_failure = None self.trust_received = self.trusted_relay self.trust_spf = self.trusted_relay or self.internal_connection self.external_spf = None self.trust_dkim = self.trust_spf self.external_dkim = None self.redirect_list = [] self.discard_list = [] self.new_headers = [] self.recipients = [] self.confidence = None self.cbv_needed = None self.whitelist_sender = False self.postmaster_reply = False self.orig_from = None self.has_dkim = False self.dkim_domain = None self.arresults = [] if f == '<>' and internal_mta and self.internal_connection: if not iniplist(self.connectip,internal_mta): self.log("REJECT: pretend MTA at ",self.connectip, " sending MAIL FROM ",f) self.setreply('550','5.7.1', 'Your PC is trying to send a DSN even though it is not an MTA.', 'If you are running MS Outlook, it is broken. If you want to', 'send return receipts, use a more standards compliant email client.' ) return Milter.REJECT if authres and not self.missing_ptr: self.arresults.append( authres.IPRevAuthenticationResult(result = 'pass', policy_iprev=self.connectip,policy_iprev_comment=self.connecthost) ) if self.canon_from: self.reject = None # reset delayed reject seen after mail from t = parse_addr(f) if len(t) == 2: t[1] = t[1].lower() self.canon_from = '@'.join(t) self.efrom = self.canon_from # Some braindead MTAs can't be relied upon to properly flag DSNs. # This heuristic tries to recognize such. self.is_bounce = (f == '<>' or t[0].lower() in config.banned_users #and t[1] == self.hello_name ) # Check SMTP AUTH, also available: # auth_authen authenticated user # auth_author (ESMTP AUTH= param) # auth_ssf (connection security, 0 = unencrypted) # auth_type (authentication method, CRAM-MD5, DIGEST-MD5, PLAIN, etc) # cipher_bits SSL encryption strength # cert_subject SSL cert subject # verify SSL cert verified self.user = self.getsymval('{auth_authen}') if self.user: # Very simple SMTP AUTH policy by default: # any successful authentication is considered INTERNAL # Detailed authorization policy is configured in the access file below. self.internal_connection = True auth_type = self.getsymval('{auth_type}') ssl_bits = self.getsymval('{cipher_bits}') self.log( "SMTP AUTH:",self.user,"sslbits =",ssl_bits, auth_type, "ssf =",self.getsymval('{auth_ssf}'), "INTERNAL" ) # Detailed authorization policy is configured in the access file below. if authres: self.arresults.append( authres.SMTPAUTHAuthenticationResult(result = 'pass', result_comment = auth_type+' sslbits=%s'%ssl_bits, smtp_auth = self.user ) ) if self.getsymval('{verify}'): self.log("SSL AUTH:", self.getsymval('{cert_subject}'), "verify =",self.getsymval('{verify}') ) if self.reject: self.log("REJECT CANCELED") self.reject = None self.fp.write('From %s %s\n' % (self.canon_from,time.ctime())) self.internal_domain = False self.umis = None if len(t) == 2: user,domain = t for pat in internal_domains: if fnmatchcase(domain,pat): self.internal_domain = True break if srs and domain in srs_domain and user.lower().startswith('srs0'): try: newaddr = srs.reverse(self.canon_from) self.orig_from = newaddr self.efrom = newaddr self.log("Original MFROM:",newaddr) except: self.log("REJECT: bad MFROM signature",self.canon_from) self.setreply('550','5.7.1','Bad MFROM signature') return Milter.REJECT if self.internal_connection: if self.user: p = SPFPolicy('%s@%s'%(self.user,domain)) policy = p.getPolicy('smtp-auth:') p.close() else: policy = None # trust ourself not to be a zombie if self.trusted_relay or self.connectip.strip() == '127.0.0.1': policy = 'OK' if policy: if policy != 'OK': self.log("REJECT: unauthorized user",self.user, "at",self.connectip,"sending MAIL FROM",self.canon_from) self.setreply('550','5.7.1', 'SMTP user %s is not authorized to use MAIL FROM %s.' % (self.user,self.canon_from) ) return Milter.REJECT elif internal_domains and not self.internal_domain: self.log("REJECT: zombie PC at ",self.connectip, " sending MAIL FROM ",self.canon_from) self.setreply('550','5.7.1', 'Your PC is using an unauthorized MAIL FROM.', 'It is either badly misconfigured or controlled by organized crime.' ) return Milter.REJECT # effective from if self.orig_from: user,domain = self.orig_from.split('@') if isbanned(domain,banned_domains): self.log("REJECT: banned domain",domain) return self.delay_reject('550','5.7.1',template='bandom',domain=domain) if self.internal_connection: wl_users = config.whitelist_senders.get(domain,()) if user in wl_users or '' in wl_users: self.whitelist_sender = True self.rejectvirus = domain in reject_virus_from if user in wiretap_users.get(domain,()): self.add_recipient(wiretap_dest) self.smart_alias(wiretap_dest) if user in discard_users.get(domain,()): self.discard = True exempt_users = dspam_whitelist.get(domain,()) if user in exempt_users or '' in exempt_users: self.dspam = False else: self.rejectvirus = False domain = None if not self.hello_name: self.log("REJECT: missing HELO") self.setreply('550','5.7.1',"It's polite to say HELO first.") return Milter.REJECT self.spf = None self.policy = None if not (self.internal_connection or self.trusted_relay) \ and self.connectip and spf: rc = self.check_spf() if rc != Milter.CONTINUE: if rc != Milter.TEMPFAIL: self.offense() return rc # no point greylisting for MTAs where we trust Received header self.greylist = not self.trust_received and not self.whitelist else: if spf and internal_policy and self.internal_connection: q = spf.query(self.connectip,self.canon_from,self.hello_name, receiver=self.receiver,strict=False) q.result = 'pass' p = SPFPolicy(q.s) if self.need_cbv(p.getPassPolicy(),q,'internal'): self.log('REJECT: internal mail from',q.s) self.setreply('550','5.7.1', "We don't accept internal mail from %s" %q.o, "Your email from %s comes from an internal IP, however"%q.o, "internal senders are not authorized to use %s."%q.o ) return Milter.REJECT rc = Milter.CONTINUE res = self.spf and self.spf_guess hres = self.spf and self.spf_helo # Check whitelist and blacklist if auto_whitelist.has_key(self.efrom): self.greylist = False if res == 'pass' or self.trusted_relay: self.whitelist = True self.log("WHITELIST",self.efrom) else: self.dspam = False self.log("PROBATION",self.efrom) if res not in ('permerror','softfail'): self.cbv_needed = None elif cbv_cache.has_key(self.efrom) and cbv_cache[self.efrom] \ or self.efrom in blacklist: # FIXME: don't use cbv_cache for blacklist if policy is 'OK' if not self.internal_connection: self.offense(inc=2) if not dspam_userdir: if domain in blacklist: self.log('REJECT: BLACKLIST',self.efrom) return self.delay_reject('550','5.7.1', 'Sender email local blacklist') else: res = cbv_cache[self.efrom] desc = "CBV: %d %s" % res[:2] self.log('REJECT:',desc) return self.delay_reject('550','5.7.1',*desc.splitlines()) self.greylist = False # don't delay - use spam for training self.blacklist = True self.log("BLACKLIST",self.efrom) elif not self.reject: # REJECT delayed until after checking whitelist if self.policy in ('REJECT', 'BAN'): self.log('REJECT: no PTR, HELO or SPF') self.offense() # ban ip if too many bad MFROMs return self.delay_reject(template='anon', mfrom=self.efrom,helo=self.hello_name,ip=self.connectip) if domain and rc == Milter.CONTINUE \ and not (self.internal_connection or self.trusted_relay): rc = self.create_gossip(domain,res,hres) return rc def create_gossip(self,domain,res,hres): global gossip if gossip and gossip_node: if self.spf and self.spf.result == 'pass': qual = 'SPF' elif res == 'pass': qual = 'GUESS' elif hres == 'pass': qual = 'HELO' domain = self.spf.h else: # No good identity: blame purported domain. Qualify by SPF # result so NEUTRAL will get separate reputation from SOFTFAIL. qual = res try: umis = gossip.umis(domain+qual,self.id+time.time()) res = gossip_node.query(umis,domain,qual,1) if res: res,hdr,val = res self.add_header(hdr,val) a = val.split(',') self.reputation = int(a[-2]) self.confidence = int(a[-1]) self.umis = umis self.from_domain = domain self.from_qual = qual # We would like to reject on bad reputation here, but we # need to give special consideration to postmaster. So # we have to wait until envrcpt(). Perhaps an especially # bad reputation could be rejected here. if self.reputation < -70 and self.confidence > 5: self.log('REJECT: REPUTATION') return self.delay_reject('550','5.7.1',template='illrepute', domain=domain,score=self.reputation,qual=qual) if self.reputation > 40 and self.confidence > 0: self.greylist = False except: gossip = None raise return Milter.CONTINUE def check_spf(self): receiver = self.receiver for tf in trusted_forwarder: q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False) res,code,txt = q.check() if res == 'none': res,code,txt = q.best_guess('v=spf1 a mx') if res == 'pass': self.log("TRUSTED_FORWARDER:",tf) self.whitelist = True break else: q = spf.query(self.connectip,self.canon_from,self.hello_name, receiver=receiver,strict=False) q.set_default_explanation( 'SPF fail: see http://openspf.net/why.html?sender=%s&ip=%s' % (q.s,q.c)) res,code,txt = q.check() if authres: self.arresults.append( authres.SPFAuthenticationResult(result = res, result_comment = txt, smtp_mailfrom = self.canon_from, smtp_helo = self.hello_name ) ) q.result = res if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext: self.cbv_needed = (q,'permerror') # report SPF syntax error to sender res,code,txt = q.perm_error.ext # extended (lax processing) result txt = 'EXT: ' + txt p = SPFPolicy(q.s) # FIXME: try:finally to close policy db, or reuse with lock if res in ('error','temperror'): if self.need_cbv(p.getTempErrorPolicy(),q,'temperror'): self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt)) self.setreply(str(code),'4.3.0',txt, 'We cannot accept your email until the DNS server for %s' % q.o, 'is operational for TXT record queries.' ) return Milter.TEMPFAIL res,code,txt = 'none',250,'EXT: ignoring DNS error' hres = None if res != 'pass': if self.mailfrom != '<>': # check hello name via spf unless spf pass h = spf.query(self.connectip,'',self.hello_name,receiver=receiver) hres,hcode,htxt = h.check() # FIXME: in a few cases, rejecting on HELO neutral causes problems # for senders forced to use their braindead ISPs email service. hp = SPFPolicy(self.hello_name) policy = hp.getPolicy('helo-%s:'%hres) if not policy: if hres in ('deny','fail','neutral','softfail'): # Even the most idiotic admin that uses non-existent domains # for helo is not going to forge 'gmail.com'. So ban the IP too. if self.hello_name == 'gmail.com': policy = 'BAN' else: policy = 'REJECT' else: policy = 'OK' if self.need_cbv(policy,q,'heloerror'): self.log('REJECT: hello SPF: %s 550 %s' % (hres,htxt)) return self.delay_reject('550','5.7.1',htxt, "The hostname given in your MTA's HELO response is not listed", "as a legitimate MTA in the SPF records for your domain. If you", "get this bounce, the message was not in fact a forgery, and you", "should IMMEDIATELY notify your email administrator of the problem." ) if hres == 'none' and spf_best_guess \ and not dynip(self.hello_name,self.connectip): # HELO must match more exactly. Don't match PTR or zombies # will be able to get a best_guess pass on their ISPs domain. hres,hcode,htxt = h.best_guess('v=spf1 a mx') else: hres,hcode,htxt = res,code,txt ores = res if self.internal_domain and res == 'none': # we don't accept our own domains externally without an SPF record self.log('REJECT: spam from self',q.o) return self.delay_reject('550','5.7.1',"I hate talking to myself!") if spf_best_guess and res == 'none': #self.log('SPF: no record published, guessing') q.set_default_explanation( 'SPF guess: see http://openspf.net/why.html') # best_guess should not result in fail if self.missing_ptr: # ignore dynamic PTR for best guess res,code,txt = q.best_guess('v=spf1 a/24 mx/24') else: res,code,txt = q.best_guess() if res != 'pass' and hres == 'pass' and spf.domainmatch([q.h],q.o): res = 'pass' # get a guessed pass for valid matching HELO if self.missing_ptr and ores == 'none' and res != 'pass' \ and hres != 'pass': # this bad boy has no credentials whatsoever res = 'none' policy = p.getNonePolicy() if policy in ('CBV','DSN'): self.offense(inc=0,min=2) # ban ip if any bad recipient self.need_cbv(policy,q,'strike3') # REJECT delayed until after checking whitelist if res in ('deny', 'fail'): if self.need_cbv(p.getFailPolicy(),q,'fail'): self.log('REJECT: SPF %s %i %s' % (res,code,txt)) # A proper SPF fail error message would read: # forger.biz [1.2.3.4] is not allowed to send mail with the domain # "forged.org" in the sender address. Contact . if q.d in config.hello_blacklist: self.offense(inc=4) return self.delay_reject(str(code),'5.7.1',txt) elif res == 'softfail': if self.need_cbv(p.getSoftfailPolicy(),q,'softfail'): self.log('REJECT: SPF %s %i %s' % (res,code,txt)) return self.delay_reject('550','5.7.1', 'SPF softfail: If you get this Delivery Status Notice, your email', 'was probably legitimate. Your administrator has published SPF', 'records in a testing mode. The SPF record reported your email as', 'a forgery, which is a mistake if you are reading this. Please', 'notify your administrator of the problem immediately.' ) elif res == 'neutral': if self.need_cbv(p.getNeutralPolicy(),q,'neutral'): self.log('REJECT: SPF neutral for',q.s) return self.delay_reject('550','5.7.1', 'mail from %s must pass SPF: http://openspf.net/why.html' % q.o, 'The %s domain is one that spammers love to forge. Due to' % q.o, 'the volume of forged mail, we can only accept mail that', 'the SPF record for %s explicitly designates as legitimate.' % q.o, 'Sending your email through the recommended outgoing SMTP', 'servers for %s should accomplish this.' % q.o ) elif res == 'pass': if self.need_cbv(p.getPassPolicy(),q,'pass'): self.log('REJECT: SPF pass for',q.s) self.setreply('550','5.7.1', "We don't accept mail from %s" %q.o, "Your email from %s comes from an authorized server, however"%q.o, "we still don't want it - we just don't like %s."%q.o ) return Milter.REJECT elif res in ('unknown','permerror'): if self.need_cbv(p.getPermErrorPolicy(),q,'permerror'): self.log('REJECT: SPF %s %i %s' % (res,code,txt)) # latest SPF draft recommends 5.5.2 instead of 5.7.1 return self.delay_reject(str(code),'5.5.2',txt.replace('\0','^@'), 'There is a fatal syntax error in the SPF record for %s' % q.o, 'We cannot accept mail from %s until this is corrected.' % q.o ) kv = {} if hres and q.h != q.o: kv['helo_spf'] = hres if res != q.result: kv['bestguess'] = res self.add_header('Received-SPF',q.get_header(q.result,receiver,**kv),0) self.spf_guess = res self.spf_helo = hres self.spf = q return Milter.CONTINUE # hide_path causes a copy of the message to be saved - until we # track header mods separately from body mods - so use only # in emergencies. def envrcpt(self,to,*str): try: param = param2dict(str) self.notify = param.get('NOTIFY','FAILURE,DELAY').upper().split(',') if 'NEVER' in self.notify: self.notify = () # FIXME: self.notify needs to be by rcpt #self.rcpt_param = param except: self.log("REJECT: invalid PARAM:",to,str) self.setreply('550','5.7.1','Invalid RCPT PARAM') return Milter.REJECT # mail to MAILER-DAEMON is generally spam that bounced for daemon in ('MAILER-DAEMON','auto-notify'): if to.startswith('<%s@'%daemon): self.log('REJECT: RCPT TO:',to,str) self.setreply('550','5.7.1','%s does not accept mail'%daemon) return Milter.REJECT try: t = parse_addr(to) newaddr = False if len(t) == 2: t[1] = t[1].lower() user,domain = t if self.is_bounce and srs and domain in srs_domain: oldaddr = '@'.join(parse_addr(to)) try: if ses: newaddr = ses.verify(oldaddr) else: newaddr = oldaddr, if len(newaddr) > 1: newaddr = newaddr[0] self.log("ses rcpt:",newaddr) else: newaddr = srs.reverse(oldaddr) # Currently, a sendmail map reverses SRS. We just log it here. self.log("srs rcpt:",newaddr) self.dspam = False # verified as reply to mail we sent self.blacklist = False self.greylist = False self.delayed_failure = False except: if not (self.internal_connection or self.trusted_relay): if srsre.match(oldaddr): self.log("REJECT: srs spoofed:",oldaddr) self.setreply('550','5.7.1','Invalid SRS signature') return Milter.REJECT if oldaddr.startswith('SES='): self.log("REJECT: ses spoofed:",oldaddr) self.setreply('550','5.7.1','Invalid SES signature') return Milter.REJECT # reject for certain recipients are delayed until after DATA if auto_whitelist.has_precise_key(self.canon_from): self.log("WHITELIST: DSN from",self.canon_from) else: #if srs_reject_spoofed \ # and user.lower() not in ('postmaster','abuse'): # return self.forged_bounce(to) self.data_allowed = not srs_reject_spoofed if not self.internal_connection and domain in private_relay: self.log('REJECT: RELAY:',to) self.setreply('550','5.7.1','Unauthorized relay for %s' % domain) return Milter.REJECT # non DSN mail to SRS address will bounce due to invalid local part canon_to = '@'.join(t) if canon_to == 'postmaster@' + self.receiver: self.postmaster_reply = True self.recipients.append(canon_to) # FIXME: use newaddr to check rcpt users = check_user.get(domain) if self.discard: self.del_recipient(to) # don't check userlist if signed MFROM for now userl = user.lower() if users and not newaddr and not userl in users: self.log('REJECT: RCPT TO:',to,str) if gossip and self.umis: gossip_node.feedback(self.umis,1) self.umis = None return self.offense() # FIXME: should dspam_exempt be case insensitive? if user in block_forward.get(domain,()): self.forward = False exempt_users = dspam_exempt.get(domain,()) if user in exempt_users or '' in exempt_users: if self.blacklist: self.log('REJECT: BLACKLISTED, rcpt to',to,str) self.setreply('550','5.7.1','Sending domain has been blacklisted') return Milter.REJECT self.dspam = False if userl != 'postmaster' and self.umis \ and self.reputation < -50 and self.confidence > 3: domain = self.from_domain self.log('REJECT: REPUTATION, rcpt to',to,str) self.setreply('550','5.7.1','%s has been sending mostly spam'%domain) return Milter.REJECT if domain in hide_path: self.hidepath = True if not domain in dspam_reject: self.reject_spam = False except: self.log("rcpt to",to,str) raise if self.greylist and config.greylist \ and self.canon_from and not self.reject: # no policy for trusted or internal greylist = config.getGreylist() rc = greylist.check(self.connectip,self.canon_from,canon_to) if rc == 0: self.log("GREYLIST:",self.connectip,self.canon_from,canon_to) self.setreply('451','4.7.1', 'Greylisted: http://projects.puremagic.com/greylisting/', 'Please retry in %.1f minutes'%(greylist.greylist_time/60.0)) return Milter.TEMPFAIL self.log("GREYLISTED: %d"%rc) self.log("rcpt to",to,str) self.smart_alias(to) # get recipient after virtusertable aliasing #rcpt = self.getsymval("{rcpt_addr}") #self.log("rcpt-addr",rcpt); return Milter.CONTINUE # Heuristic checks for spam headers def check_header(self,name,val): lname = name.lower() # val is decoded header value if lname == 'subject': # check for common spam keywords for wrd in config.spam_words: if val.find(wrd) >= 0: self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','That subject is not allowed') return Milter.REJECT # even if we wanted the Taiwanese spam, we can't read Chinese if block_chinese: if not inCharSets(val,'iso-8859-1'): self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1',"We don't understand that charset") return Milter.REJECT # check for spam that claims to be legal lval = val.lower().strip() for adv in ("adv:","adv.","adv ", "","","[adv]","(adv)","advt:","advert:","[spam]"): if lval.startswith(adv): self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','No soliciting allowed') return Milter.REJECT for adv in ("adv","(adv)","[adv]","(non-spam)"): if lval.endswith(adv): self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','No soliciting allowed') return Milter.REJECT # check for porn keywords for w in config.porn_words: if lval.find(w) >= 0: self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','That subject is not allowed') return Milter.REJECT # check for annoying forwarders if not self.forward: if lval.startswith("fwd:") or lval.startswith("[fw"): self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','I find unedited forwards annoying') return Milter.REJECT # check for delayed bounce of CBV if self.postmaster_reply and srs: if refaildsn.search(lval): self.delayed_failure = val.strip() # if confirmed by finding our signed Message-ID, # original sender (encoded in Message-ID) is blacklisted elif lname == 'from' and self.dspam: fname,email = parseaddr(val) for w in config.spam_words: if fname.find(w) >= 0: self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','No soliciting') return self.bandomain() for w in config.from_words: if fname.find(w) >= 0: self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','No soliciting') return self.bandomain() # check for porn keywords lval = fname.lower().strip() for w in config.porn_words: if lval.find(w) >= 0: self.log('REJECT: %s: %s' % (name,val)) self.setreply('550','5.7.1','Watch your language') return self.bandomain() if email.lower().startswith('postmaster@'): # Yes, if From header comes last, this might not help much. # But this is a heuristic - if MTAs would send proper DSNs in # the first place, none of this would be needed. self.is_bounce = True # check for invalid message id elif lname == 'message-id' and len(val) < 4: self.log('REJECT: %s: %s' % (name,val)) return Milter.REJECT # check for common bulk mailers elif lname == 'x-mailer': mailer = val.lower() if mailer in ('direct email','calypso','mail bomber') \ or mailer.find('optin') >= 0: self.log('REJECT: %s: %s' % (name,val)) return Milter.REJECT return Milter.CONTINUE def forged_bounce(self,rcpt='-'): if self.mailfrom != '<>': self.log("REJECT: bogus DSN",rcpt) self.setreply('550','5.7.1', "I do not accept normal mail from %s." % self.mailfrom.split('@')[0], "All such mail has turned out to be Delivery Status Notifications", "which failed to be marked as such. Please send a real DSN if", "you need to. Use another MAIL FROM if you need to send me mail." ) else: self.log('REJECT: bounce with no SRS encoding',rcpt) self.setreply('550','5.7.1', "I did not send you that message. Please consider implementing SPF", "(http://openspf.net) to avoid bouncing mail to spoofed senders.", "Thank you." ) return Milter.REJECT def bandomain(self,wild=0): if self.spf and self.spf_guess == 'pass' and self.confidence == 0: domain = self.spf.o elif self.spf and self.spf.o == self.dkim_domain: domain = self.dkim_domain else: return Milter.REJECT if domain in config.email_providers: sender = self.spf.s blacklist[sender] = None self.greylist = False # don't delay - use spam for training self.blacklist = True self.log("BLACKLIST",sender) return Milter.REJECT if not isbanned(domain,banned_domains): m = RE_MULTIMX.match(domain) if m: orig = domain domain = '*.' + domain[m.end():] self.log('BAN DOMAIN:',orig,'->',domain) else: if wild: a = domain.split('.')[wild:] if len(a) > 1: domain = '*.'+'.'.join(a) self.log('BAN DOMAIN:',domain) try: fp = open('banned_domains','at') print >>fp,domain finally: fp.close() banned_domains.add(domain) return Milter.REJECT def data(self): if self.reject: self.log("DELAYED REJECT") args,kw = self.reject self.htmlreply(*args,**kw) return Milter.REJECT if not self.data_allowed: return self.forged_bounce() return Milter.CONTINUE def header(self,name,hval): if self.data() == Milter.REJECT: return Milter.REJECT lname = name.lower() # decode near ascii text to unobfuscate val = parse_header(hval) if not self.internal_connection and not (self.blacklist or self.whitelist): rc = self.check_header(name,val) if rc != Milter.CONTINUE: if gossip and self.umis: gossip_node.feedback(self.umis,1) return rc elif self.whitelist_sender: # check for AutoReplys if (lname == 'subject' and reautoreply.match(val)) \ or (lname == 'user-agent' and val.lower().startswith('vacation')): self.whitelist_sender = False self.log('AUTOREPLY: not whitelisted') # log selected headers if config.log_headers or lname in ('subject','x-mailer'): self.log('%s: %s' % (name,val)) elif self.trust_received and lname == 'received': self.trust_received = False self.trust_spf = False self.log('%s: %s' % (name,val.splitlines()[0])) elif self.trust_spf and lname == 'received-spf': self.trust_spf = False self.external_spf = val self.log('%s: %s' % (name,val.splitlines()[0])) elif dkim and lname == 'dkim-signature': self.has_dkim = True self.log('%s: %s' % (name,val.splitlines()[0])) elif self.trust_dkim and lname == 'authentication-results': self.trust_dkim = False self.external_dkim = val self.log('%s: %s' % (name,val.splitlines()[0])) # FIXME: keep both decoded and pristine headers. DKIM needs # pristine headers. if self.fp: try: val = val.encode('iso-8859-1') except: val = hval self.fp.write("%s: %s\n" % (name,val)) # add decoded header to buffer self.pristine_headers.write("%s: %s\n" % (name,hval)) return Milter.CONTINUE def eoh(self): if not self.fp: return Milter.TEMPFAIL # not seen by envfrom if self.data() == Milter.REJECT: return Milter.REJECT for name,val,idx in self.new_headers: self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer self.fp.write("\n") # terminate headers if not self.internal_connection: msg = None # parse headers only if needed if not self.delayed_failure: self.fp.seek(0) msg = email.message_from_file(self.fp) if msg.get_param('report-type','').lower() == 'delivery-status': self.is_bounce = True self.delayed_failure = msg.get('subject','DSN') # log when neither sender nor from domains matches mail from domain if supply_sender and self.mailfrom != '<>': if not msg: self.fp.seek(0) msg = email.message_from_file(self.fp) mf_domain = self.canon_from.split('@')[-1] for rn,hf in getaddresses(msg.get_all('from',[]) + msg.get_all('sender',[])): t = parse_addr(hf) if len(t) == 2: hd = t[1].lower() if hd == mf_domain or mf_domain.endswith('.'+hd): break else: for f in msg.get_all('from',[]): self.log('From:',f) sender = msg.get_all('sender') if sender: for f in sender: self.log('Sender:',f) else: self.log("NOTE: Supplying MFROM as Sender"); self.add_header('Sender',self.mailfrom) del msg # copy headers to a temp file for scanning the body self.fp.seek(0) headers = self.fp.getvalue() self.fp.close() fd,fname = tempfile.mkstemp(".defang") self.tempname = fname self.fp = os.fdopen(fd,"w+b") self.fp.write(headers) # IOError (e.g. disk full) causes TEMPFAIL self.body_start = self.fp.tell() # check if headers are really spammy if dspam_dict and not self.internal_connection: ds = dspam.dspam(dspam_dict,dspam.DSM_PROCESS, dspam.DSF_CHAINED|dspam.DSF_CLASSIFY) try: ds.process(headers) if ds.probability > 0.93 and self.dspam and not self.whitelist: self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability) self.setreply('550','5.7.1','Your Message looks spammy') return Milter.REJECT self.add_header('X-DSpam-HeaderScore','%f'%ds.probability) finally: ds.destroy() self.ioerr = None return Milter.CONTINUE @Milter.noreply def unknown(self, cmd): self.log('Invalid command sent: %s' % cmd) return Milter.CONTINUE @Milter.noreply def body(self,chunk): # copy body to temp file try: if self.fp: self.fp.write(chunk) # IOError causes TEMPFAIL in milter self.bodysize += len(chunk) except Exception,x: if not self.ioerr: self.ioerr = x self.log(x) self.fp = None return Milter.CONTINUE def _headerChange(self,msg,name,value): if value: # add header self.addheader(name,value) else: # delete all headers with name h = msg.getheaders(name) if h: for i in range(len(h),0,-1): self.chgheader(name,i-1,'') def _chk_ext(self,name): "Check a name for dangerous Winblows extensions." if not name: return name lname = name.lower() for ext in self.bad_extensions: if lname.endswith(ext): return name return None def _chk_attach(self,msg): "Filter attachments by content." # check for bad extensions mime.check_name(msg,self.tempname,ckname=self._chk_ext,scan_zip=scan_zip) # remove scripts from HTML if scan_html: mime.check_html(msg,self.tempname) # don't let a tricky virus slip one past us if scan_rfc822: msg = msg.get_submsg() if isinstance(msg,email.Message.Message): return mime.check_attachments(msg,self._chk_attach) return Milter.CONTINUE def alter_recipients(self,discard_list,redirect_list): for rcpt in discard_list: if rcpt in redirect_list: continue self.log("DISCARD RCPT: %s" % rcpt) # log discarded rcpt self.delrcpt(rcpt) for rcpt in redirect_list: if rcpt in discard_list: continue self.log("APPEND RCPT: %s" % rcpt) # log appended rcpt self.addrcpt(rcpt) if not blind_wiretap: self.addheader('Cc',rcpt) # def gossip_header(self): "Set UMIS from GOSSiP header." msg = email.message_from_file(self.fp) gh = msg.get_all('x-gossip') if gh: self.log('X-GOSSiP:',gh[0]) self.umis,_ = gh[0].split(',',1) elif self.spf: domain = self.spf.o if domain: self.create_gossip(domain,self.spf_guess,self.spf_helo) def sign_dkim(self): if self.canon_from: a = self.canon_from.split('@') if len(a) != 2: a.append('localhost.localdomain') user,domain = a elif config.dkim_domain: domain = config.dkim_domain if config.dkim_key and domain == config.dkim_domain: self.fp.seek(self.body_start) txt = self.pristine_headers.getvalue()+'\n'+self.fp.read() try: d = dkim.DKIM(txt,logger=milter_log) h = d.sign(config.dkim_selector,domain,config.dkim_key, canonicalize=('relaxed','simple')) name,val = h.split(':',1) self.addheader(name,val.strip().replace('\r\n','\n'),0) except dkim.DKIMException as x: self.log('DKIM: %s'%x) except Exception as x: milter_log.error("sign_dkim: %s",x,exc_info=True) def check_dkim(self): self.fp.seek(self.body_start) txt = self.pristine_headers.getvalue()+'\n'+self.fp.read() res = False result = 'fail' d = dkim.DKIM(txt,logger=milter_log,minkey=768) try: res = d.verify() if res: dkim_comment = 'Good %d bit signature.' % d.keysize result = 'pass' else: dkim_comment = 'Bad %d bit signature.' % d.keysize except dkim.DKIMException as x: dkim_comment = str(x) #self.log('DKIM: %s'%x) except Exception as x: dkim_comment = str(x) milter_log.error("check_dkim: %s",x,exc_info=True) self.dkim_domain = d.domain if authres: self.arresults.append( authres.DKIMAuthenticationResult(result=result, result_comment = dkim_comment, header_i=d.signature_fields.get(b'i'), header_d=d.signature_fields.get(b'd') ) ) if not res: fd,fname = tempfile.mkstemp(".dkim") with os.fdopen(fd,"w+b") as fp: fp.write(txt) self.log('DKIM: Fail (saved as %s)'%fname) return res # check spaminess for recipients in dictionary groups # if there are multiple users getting dspammed, then # a signature tag for each is added to the message. # FIXME: quarantine messages rejected via fixed patterns above # this will give a fast start to stats def check_spam(self): "return True/False if self.fp, else return Milter.REJECT/TEMPFAIL/etc" self.screened = False if not dspam_userdir: return False ds = Dspam.DSpamDirectory(dspam_userdir) ds.log = self.log ds.headerchange = self._headerChange modified = False for rcpt in self.recipients: if dspam_users.has_key(rcpt.lower()): user = dspam_users.get(rcpt.lower()) if user: try: self.fp.seek(0) txt = self.fp.read() if user in ('bandom','spam','falsepositive') \ and self.internal_connection: if spf and self.external_spf: q = spf.query('','','') p = q.parse_header(self.external_spf) self.spf_guess = p.get('bestguess',q.result) self.spf_helo = p.get('helo',None) self.log("External SPF:",self.spf_guess) self.spf = q else: self.spf = None if self.external_dkim: ar = authres.AuthenticationResultsHeader.parse_value( self.external_dkim) for r in ar.results: if r.method == 'dkim' and r.result == 'pass': for p in r.properties: if p.type == 'header' and p.name == 'd': self.dkim_domain = p.value if user == 'bandom' and self.internal_connection: if self.spf: if self.spf_guess == 'pass' or q.result == 'none' \ or self.spf.o == self.dkim_domain: self.confidence = 0 # ban regardless of reputation status s = rcpt.split('@')[0][-1] self.bandomain(wild=s.isdigit() and int(s)) user = 'spam' if user == 'spam' and self.internal_connection: sender = dspam_users.get(self.efrom) if sender: self.log("SPAM: %s" % sender) # log user for SPAM self.fp.seek(0) self.gossip_header() self.fp = None ds.add_spam(sender,txt) txt = None return Milter.DISCARD elif user == 'falsepositive' and self.internal_connection: sender = dspam_users.get(self.efrom) if sender: self.log("FP: %s" % sender) # log user for FP txt = ds.false_positive(sender,txt) self.fp = StringIO.StringIO(txt) self.gossip_header() self.delrcpt('<%s>' % rcpt) self.recipients = None self.rejectvirus = False return True elif not self.internal_connection or dspam_internal: if len(txt) > dspam_sizelimit: self.log("Large message:",len(txt)) if self.blacklist: self.log('REJECT: BLACKLISTED') self.setreply('550','5.7.1', '%s has been blacklisted.'%self.efrom) self.fp = None return Milter.REJECT return False if user == 'honeypot' and Dspam.VERSION >= '1.1.9': keep = False # keep honeypot mail self.fp = None if len(self.recipients) > 1: self.log("HONEYPOT:",rcpt,'SCREENED') if self.whitelist: # don't train when recipients includes honeypot return False if self.spf and self.mailfrom != '<>': # check that sender accepts quarantine DSN if self.spf_guess == 'pass': msg = mime.message_from_file(StringIO.StringIO(txt)) rc = self.send_dsn(self.spf,msg,'quarantine',fail=True) del msg else: rc = self.send_dsn(self.spf) if rc != Milter.CONTINUE: return rc ds.check_spam(user,txt,self.recipients,quarantine=True, force_result=dspam.DSR_ISSPAM) else: ds.check_spam(user,txt,self.recipients,quarantine=keep, force_result=dspam.DSR_ISSPAM) self.log("HONEYPOT:",rcpt) return Milter.DISCARD if self.whitelist: # Sender whitelisted: tag, but force as ham. # User can change if actually spam. txt = ds.check_spam(user,txt,self.recipients, force_result=dspam.DSR_ISINNOCENT) elif self.blacklist: txt = ds.check_spam(user,txt,self.recipients, force_result=dspam.DSR_ISSPAM) elif user in dspam_train: txt = ds.check_spam(user,txt,self.recipients) else: txt = ds.check_spam(user,txt,self.recipients,classify=True) if txt: self.add_header("X-DSpam-Score",'%f' % ds.probability) return False if not txt: # DISCARD if quarrantined for any recipient. It # will be resent to all recipients if they submit # as a false positive. self.log("DSPAM:",user,rcpt) self.fp = None return Milter.DISCARD self.fp = StringIO.StringIO(txt) modified = True except Exception,x: self.log("check_spam:",x) milter_log.error("check_spam: %s",x,exc_info=True) # screen if no recipients are dspam_users if not modified and dspam_screener and not self.internal_connection \ and self.dspam: self.fp.seek(0) txt = self.fp.read() if len(txt) > dspam_sizelimit: self.log("Large message:",len(txt)) return False screener = dspam_screener[self.id % len(dspam_screener)] if not ds.check_spam(screener,txt,self.recipients, classify=True,quarantine=False): if self.whitelist: # messages is whitelisted but looked like spam, Train on Error self.log("TRAIN:",screener,'X-Dspam-Score: %f' % ds.probability) # user can't correct anyway if really spam, so discard tag ds.check_spam(screener,txt,self.recipients, force_result=dspam.DSR_ISINNOCENT) return False if self.reject_spam and self.spf.result != 'pass': self.log("DSPAM:",screener, 'REJECT: X-DSpam-Score: %f' % ds.probability) self.setreply('550','5.7.1','Your Message looks spammy') self.fp = None return Milter.REJECT self.log("DSPAM:",screener,"SCREENED %f" % ds.probability) if self.spf and self.mailfrom != '<>': # check that sender accepts quarantine DSN self.fp.seek(0) if self.spf_guess == 'pass' or self.cbv_needed: msg = mime.message_from_file(self.fp) if self.spf_guess == 'pass': rc = self.send_dsn(self.spf,msg,'quarantine',fail=True) else: rc = self.do_needed_cbv(msg) del msg else: rc = self.send_dsn(self.spf) if rc != Milter.CONTINUE: self.fp = None return rc if not ds.check_spam(screener,txt,self.recipients,classify=True): self.fp = None return Milter.DISCARD # Message no longer looks spammy, deliver normally. We lied in the DSN. elif self.blacklist: # message is blacklisted but looked like ham, Train on Error self.log("TRAINSPAM:",screener,'X-Dspam-Score: %f' % ds.probability) ds.check_spam(screener,txt,self.recipients,quarantine=False, force_result=dspam.DSR_ISSPAM) self.fp = None self.setreply('550','5.7.1', 'Sender email local blacklist') return Milter.REJECT elif self.whitelist and ds.totals[1] < 1000: self.log("TRAIN:",screener,'X-Dspam-Score: %f' % ds.probability) # user can't correct anyway if really spam, so discard tag ds.check_spam(screener,txt,self.recipients, force_result=dspam.DSR_ISINNOCENT) return False # log spam score for screened messages self.add_header("X-DSpam-Score",'%f' % ds.probability) self.screened = True return modified # train late in eom(), after failed CBV # FIXME: need to undo if registered as ham with a dspam_user def train_spam(self): "Train screener with current message as spam" if not dspam_userdir: return if not dspam_screener: return ds = Dspam.DSpamDirectory(dspam_userdir) ds.log = self.log self.fp.seek(0) txt = self.fp.read() if len(txt) > dspam_sizelimit: self.log("Large message:",len(txt)) return screener = dspam_screener[self.id % len(dspam_screener)] # since message will be rejected, we do not quarantine ds.check_spam(screener,txt,self.recipients,force_result=dspam.DSR_ISSPAM, quarantine=False) self.log("TRAINSPAM:",screener,'X-Dspam-Score: %f' % ds.probability) def do_needed_cbv(self,msg): q,template_name = self.cbv_needed rc = self.send_dsn(q,msg,template_name) self.cbv_needed = None return rc def need_cbv(self,policy,q,tname): self.policy = policy if policy == 'CBV': if self.mailfrom != '<>' and not self.cbv_needed: self.cbv_needed = (q,None) elif policy == 'DSN': if self.mailfrom != '<>' and not self.cbv_needed: self.cbv_needed = (q,tname) elif policy == 'WHITELIST': self.whitelist = True elif policy != 'OK': if policy == 'BAN': self.offense(inc=3) elif self.offenses: self.offense() # multiple forged domains are extra evil return True return False def whitelist_rcpts(self): whitelisted = [] for canon_to in self.recipients: user,domain = canon_to.split('@') for pat in internal_domains: if fnmatchcase(domain,pat): break else: auto_whitelist[canon_to] = None whitelisted.append(canon_to) self.log('Auto-Whitelist:',canon_to) return whitelisted def eom(self): if self.ioerr: fname = tempfile.mktemp(".ioerr") # save message that caused crash os.rename(self.tempname,fname) self.tempname = None return Milter.TEMPFAIL if not self.fp: return Milter.ACCEPT # no message collected - so no eom processing if self.is_bounce and len(self.recipients) > 1: self.log("REJECT: DSN to multiple recipients") self.setreply('550','5.7.1', 'DSN to multiple recipients') return Milter.REJECT try: # check for delayed bounce if self.delayed_failure: self.fp.seek(0) sender = findsrs(self.fp) if sender: cbv_cache[sender] = 550,self.delayed_failure # make blacklisting persistent, since delayed DSNs are expensive blacklist[sender] = None try: # save message for debugging fname = tempfile.mktemp(".dsn") os.rename(self.tempname,fname) except: fname = self.tempname self.tempname = None self.log('BLACKLIST:',sender,fname) return Milter.DISCARD if not self.internal_connection and self.has_dkim: res = self.check_dkim() if self.dkim_domain: p = SPFPolicy(self.dkim_domain) policy = p.getPolicy('dkim-%s:'%res) p.close() if policy == 'REJECT': self.log('REJECT: DKIM',res,self.dkim_domain) self.setreply('550','5.7.1','DKIM %s for %s'%(res,self.dkim_domain)) return Milter.REJECT elif self.internal_connection: self.sign_dkim() # FIXME: don't sign until accepting # add authentication results header if self.arresults: h = authres.AuthenticationResultsHeader(authserv_id = self.receiver, results=self.arresults) name,val = str(h).split(': ',1) self.add_header(name,val,0) # analyze external mail for spam spam_checked = self.check_spam() # tag or quarantine for spam if not self.fp: if gossip and self.umis: gossip_node.feedback(self.umis,1) return spam_checked # analyze all mail for dangerous attachments and scripts self.fp.seek(0) msg = mime.message_from_file(self.fp) # pass header changes in top level message to sendmail msg.headerchange = self._headerChange # filter leaf attachments through _chk_attach assert not msg.ismodified() self.bad_extensions = ['.' + x for x in banned_exts] rc = mime.check_attachments(msg,self._chk_attach) except: # milter crashed trying to analyze mail, do some diagnostics exc_type,exc_value = sys.exc_info()[0:2] if dspam_userdir and exc_type == dspam.error: if not exc_value.strerror: exc_value.strerror = exc_value.args[0] if exc_value.strerror == 'Lock failed': milter_log.warn("LOCK: BUSY") # log filename self.setreply('450','4.2.0', 'Too busy discarding spam. Please try again later.') return Milter.TEMPFAIL fname = tempfile.mktemp(".fail") # save message that caused crash os.rename(self.tempname,fname) self.tempname = None if exc_type == email.Errors.BoundaryError: milter_log.warn("MALFORMED: %s",fname) # log filename if self.internal_connection: # accept anyway for now return Milter.ACCEPT self.setreply('554','5.7.7', 'Boundary error in your message, are you a spammer?') return Milter.REJECT if exc_type == email.Errors.HeaderParseError: milter_log.warn("MALFORMED: %s",fname) # log filename self.setreply('554','5.7.7', 'Header parse error in your message, are you a spammer?') return Milter.REJECT milter_log.error("FAIL: %s",fname) # log filename # let default exception handler print traceback and return 451 code raise if rc == Milter.REJECT: return rc if rc == Milter.DISCARD: return rc if rc == Milter.CONTINUE: rc = Milter.ACCEPT # for testbms.py compat defanged = msg.ismodified() if self.hidepath: del msg['Received'] if self.recipients == None: # false positive being recirculated self.recipients = msg.get_all('x-dspam-recipients',[]) if self.recipients: for rcptlist in self.recipients: for rcpt in rcptlist.split(','): self.addrcpt('<%s>' % rcpt.strip()) del msg['x-dspam-recipients'] else: self.addrcpt(self.mailfrom) else: self.alter_recipients(self.discard_list,self.redirect_list) # auto whitelist original recipients if not defanged and self.whitelist_sender: whitelisted = self.whitelist_rcpts() if whitelisted: for mx in config.whitelist_mx: try: self.send_rcpt(mx,whitelisted) self.log('Tell MX:',mx) except Exception,x: self.log('Tell MX:',mx,x) for name,val,idx in self.new_headers: try: try: self.addheader(name,val,idx) except TypeError: val = val.replace('\x00',r'\x00') self.addheader(name,val,idx) except Milter.error: self.addheader(name,val) # older sendmail can't insheader # Do not send CBV to internal domains (since we'll just get # the "Fraudulent MX" error). Whitelisted senders clearly do not # need CBV. However, whitelisted domains might (to discover # bogus localparts). Need a way to tell the difference. if self.cbv_needed and not self.internal_domain: rc = self.do_needed_cbv(msg) if rc == Milter.REJECT: # Do not feedback here, because feedback should only occur # for messages that have gone to DATA. Reputation lets us # reject before DATA for persistent spam domains, saving # cycles and bandwidth. # Do feedback here, because CBV costs quite a bit more than # simply rejecting before DATA. Bad reputation will acrue to # the IP or HELO, since we won't get here for validated MAILFROM. # See Proverbs 26:4,5 if gossip and self.umis: gossip_node.feedback(self.umis,1) self.train_spam() return Milter.REJECT if rc != Milter.CONTINUE: return rc if mail_archive: global _archive_lock if not _archive_lock: import thread _archive_lock = thread.allocate_lock() _archive_lock.acquire() try: fin = open(self.tempname,'r') fout = open(mail_archive,'a') shutil.copyfileobj(fin,fout,8192) finally: _archive_lock.release() fin.close() fout.close() if not defanged and not spam_checked: if gossip and self.umis and self.screened: gossip_node.feedback(self.umis,0) os.remove(self.tempname) self.tempname = None # prevent re-removal self.log("eom") return rc # no modified attachments # Body modified, copy modified message to a temp file if defanged: if self.rejectvirus and not self.hidepath: self.log("REJECT virus from",self.mailfrom) self.setreply('550','5.7.1','Attachment type not allowed.', 'You attempted to send an attachment with a banned extension.') self.tempname = None return Milter.REJECT self.log("Temp file:",self.tempname) self.tempname = None # prevent removal of original message copy out = tempfile.TemporaryFile() try: msg.dump(out) out.seek(0) # Since we wrote headers with '\n' (no CR), # the following header/body split should always work. msg = out.read().split('\n\n',1)[-1] self.replacebody(msg) # feed modified message to sendmail if spam_checked: if gossip and self.umis: gossip_node.feedback(self.umis,0) self.log("dspam") return rc finally: out.close() return Milter.TEMPFAIL ## Send recipients to primary MX for auto whitelisting def send_rcpt(self,mx,rcpts,timeout=30): if not srs: return # requires SRS for authentication sender = srs.forward(self.canon_from,self.receiver) smtp = smtplib.SMTP() toolate = time.time() + timeout smtp.connect(mx) code,resp = smtp.helo(self.receiver) if not (200 <= code <= 299): raise smtplib.SMTPHeloError(code, resp) code,resp = smtp.docmd('MAIL FROM: <%s>'%sender) if code != 250: raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%sender) badrcpts = {} for rcpt in rcpts: code,resp = smtp.rcpt(rcpt) if code not in (250,251): badrcpts[rcpt] = (code,resp)# permanent error smtp.quit() if badrcpts: return badrcpts return None def htmlreply(self,code='550',xcode='5.7.1',*msg,**kw): if 'template' in kw: template = kw['template'] del kw['template'] desc = "%s/%s?%s" % (config.errors_url,template,urllib.urlencode(kw)) self.setreply(code,xcode,desc.replace('%','%%'),*msg) else: try: self.setreply(code,xcode,*msg) except ValueError,x: self.log(x) def send_dsn(self,q,msg=None,template_name=None,fail=False): if fail: if not self.notify: template_name = None else: if 'DELAY' not in self.notify: template_name = None if template_name and template_name.startswith('helo'): sender = 'postmaster@'+q.h else: sender = q.s cached = cbv_cache.has_key(sender) if cached: self.log('CBV:',sender,'(cached)') res = cbv_cache[sender] else: m = None if template_name: fname = os.path.join(config.datadir,template_name+'.txt') try: template = file(fname).read() # from datadir m = dsn.create_msg(q,self.recipients,msg,template) self.log('CBV:',sender,'Using:',fname) except IOError: pass if not m: self.log('CBV:',sender,'PLAIN (%s)'%q.result) else: if srs: # Add SRS coded sender to various headers. When (incorrectly) # replying to our DSN, any of these which are preserved # allow us to track the source. msgid = srs.forward(sender,self.receiver) m.add_header('Message-Id','<%s>'%msgid) if 'x-mailer' in m: m.replace_header('x-mailer','"%s" <%s>' % (m['x-mailer'],msgid)) else: m.add_header('X-Mailer','"Python Milter" <%s>'%msgid) m.add_header('Sender','"Python Milter" <%s>'%msgid) m = m.as_string() print >>open(template_name+'.last_dsn','w'),m # if missing template, do plain CBV res = dsn.send_dsn(sender,self.receiver,m,timeout=timeout) if res: desc = "CBV: %d %s" % res[:2] if 400 <= res[0] < 500: self.log('TEMPFAIL:',desc) self.setreply('450','4.2.0',*desc.splitlines()) return Milter.TEMPFAIL cbv_cache[sender] = res self.log('REJECT:',desc) try: self.htmlreply(mfrom=sender,msg=res[1],template='dsnrefused') except TypeError: self.setreply('550','5.7.1',"Callback failure") return Milter.REJECT cbv_cache[sender] = res return Milter.CONTINUE def close(self): if self.tempname: os.remove(self.tempname) # remove in case session aborted if self.fp: self.fp.close() return Milter.CONTINUE def abort(self): if self.whitelist_sender and self.recipients and self.trusted_relay: self.whitelist_rcpts() else: self.log("abort after %d body chars" % self.bodysize) return Milter.CONTINUE def main(): if config.access_file: try: acf = anydbm.open(config.access_file,'r') acf.close() except: milter_log.error('Unable to read: %s',config.access_file) return # Banned ips and domains, and anything we forgot, are still in logdir # (And logdir and datadir are the same for old configs.) if config.logdir: print "chdir:",config.logdir os.chdir(config.logdir) try: global banned_ips banned_ips = set(addr2bin(ip) for fn in glob('banned_ips*') for ip in open(fn)) print len(banned_ips),'banned ips' except: milter_log.exception('Error reading banned_ips') try: global banned_domains banned_domains = set(dom.strip() for fn in glob('banned_domains*') for dom in open(fn)) print len(banned_domains),'banned domains' except: milter_log.exception('Error reading banned_domains') greylist = config.getGreylist() if greylist: print "Expired %d greylist records." % greylist.clean() greylist.close() Milter.factory = bmsMilter flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS if wiretap_dest or smart_alias or dspam_userdir: flags = flags + Milter.ADDRCPT if srs or len(discard_users) > 0 or smart_alias or dspam_userdir: flags = flags + Milter.DELRCPT Milter.set_flags(flags) socket.setdefaulttimeout(60) milter_log.info("bms milter startup") Milter.runmilter("pythonfilter",socketname,timeout) milter_log.info("bms milter shutdown") # force dereference of local data structures before shutdown getattr(local, 'whatever', None) if __name__ == "__main__": config = read_config(["/etc/mail/pymilter.cfg","milter.cfg"]) cbv_cache.load('send_dsn.log',age=30) auto_whitelist.load('auto_whitelist.log',age=120) blacklist.load('blacklist.log',age=60) if dspam_dict: import dspam # low level spam check if dspam_userdir: import dspam import Dspam # high level spam check try: dspam_version = Dspam.VERSION except: dspam_version = '1.1.4' assert dspam_version >= '1.1.5' main() milter-0.8.18/temperror.txt0000644000160600001450000000150111655257264014560 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: Critical DNS configuration error Auto-Submitted: auto-generated (configuration error) This is an automatically generated Delivery Status Notification. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Received-SPF: %(spf_result)s Your DNS server is not responding to TXT queries. In other words, it is BROKEN. You need to get somebody to fix it ASAP. We are attempting to do TXT queries to see if you have an SPF record. See http://openspf.net We are sending you this message to alert you to the fact that you have problems with your DNS. If you need further assistance, please do not hesitate to contact me again. Kind regards, postmaster@%(receiver)s milter-0.8.18/fail.txt0000644000160600001450000000213411357667524013462 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: SPF fail (EMAIL FORGERY) Auto-Submitted: auto-generated (configuration error) This is an automatically generated Delivery Status Notification. *** WARNING! YOU ARE SENDING FROM AN UNAUTHORIZED LOCATION *** The email administrator for '%(sender_domain)s' (YOUR administrator) has FORBIDDEN you to send email from this location. IMMEDIATELY contact your email administrator and follow his instructions to properly send mail. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Received-SPF: %(spf_result)s Your sender policy indicated that the above email was forged. Because we believe your policy is in error, we have accepted the email anyway. Please ask your email administrator to review your SPF policy. You may also have neglected to follow your postmaster's instructions for configuring outgoing email. If you need further assistance, please do not hesitate to contact me. Kind regards, Stuart D Gathman postmaster@%(receiver)s milter-0.8.18/strike3.txt0000644000160600001450000000476211655257264014141 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: Critical mail server configuration error Auto-Submitted: auto-generated (configuration error) This is an automatically generated Delivery Status Notification. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Someone at IP address %(connectip)s sent an email claiming to be from %(sender)s. If that wasn't you, then your domain, %(sender_domain)s, was forged - i.e. used without your knowlege or authorization by someone attempting to steal your mail identity. This is a very serious problem, and you need to provide authentication for your SMTP (email) servers to prevent criminals from forging your domain. The simplest step is usually to publish an SPF record with your Sender Policy. For more information, see: http://openspf.net I hate to annoy you with a DSN (Delivery Status Notification) from a possibly forged email, but since you have not published a sender policy, there is no other way of bringing this to your attention. If it *was* you that sent the email, then your email domain or configuration is in error. If you don't know anything about mail servers, then pass this on to your SMTP (mail) server administrator. We have accepted the email anyway, in case it is important, but we couldn't find anything about the mail submitter at %(connectip)s to distinguish it from a zombie (compromised/infected computer - usually a Windows PC). There was no PTR record for its IP address (PTR names that contain the IP address don't count). RFC2821 requires that your hello name be a FQN (Fully Qualified domain Name, i.e. at least one dot) that resolves to the IP address of the mail sender. In addition, just like for PTR, we don't accept a helo name that contains the IP, since this doesn't help to identify you. The hello name you used, %(heloname)s, was invalid. Furthermore, there was no SPF record for the sending domain %(sender_domain)s. We even tried to find its IP in any A or MX records for your domain, but that failed also. We really should reject mail from anonymous mail clients, but in case it is important, we are accepting it anyway. We are sending you this message to alert you to the fact that Either - Someone is forging your domain. Or - You have problems with your email configuration. Or - Possibly both. If you need further assistance, please do not hesitate to contact me again. Kind regards, postmaster@%(receiver)s milter-0.8.18/COPYING0000644000160600001450000004310310676070725013034 0ustar stuartbms GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. milter-0.8.18/test/0002755000160600001450000000000012121400433012735 5ustar stuartbmsmilter-0.8.18/test/virus70000644000160600001450000000404610247123425014133 0ustar stuartbmsFrom pandora.owner@pandora.cz Wed Mar 24 21:02:22 2004 Received: from pandora.cz (localhost [127.0.0.1]) by pandora3.mobil.cz (8.12.8/8.12.8) with ESMTP id i2O88iWu021270 for ; Wed, 24 Mar 2004 09:08:44 +0100 Message-Id: <200403240808.i2O88iWu021270@pandora3.mobil.cz> X-Sender: Pandora MIME-Version: 1.0 Date: Wed, 24 Mar 2004 09:08:44 +0100 From: "administrator@pandora.cz" To: "stuart@bmsi.com" Subject: Konferenceneexistuje Content-Type: multipart/mixed; boundary="Pandora3Bndry_1080115724426044878" --Pandora3Bndry_1080115724426044878 Content-Type: multipart/alternative; boundary="Pandora3Bndry_1080115724783315537" --Pandora3Bndry_1080115724783315537 Content-Type: text/plain; charset="ISO-8859-2" Konference '2003-07-46063' neexistuje. --Pandora3Bndry_1080115724783315537 Content-Type: text/html; charset="ISO-8859-2" Konference '2003-07-46063' neexistuje. --Pandora3Bndry_1080115724783315537-- --Pandora3Bndry_1080115724426044878 Content-Type: message/rfc822; boundary="----=_NextPart_000_0010_00000FFF.00007545" MIME-Version: 1.0 Date: Wed, 24 Mar 2004 09:03:28 +0100 From: "" To: "" <2003-07-46063@pandora.cz> Subject: =?ISO-8859-2?q?Re=3A_Your_software?= Content-Type: multipart/mixed; boundary="Pandora3Bndry_10801157231587976770" --Pandora3Bndry_10801157231587976770 Content-Type: text/plain; charset="Windows-1252" Content-Transfer-Encoding: 7bit See the attached file for details. --Pandora3Bndry_10801157231587976770 Content-Type: application/octet-stream; name="application.pif" Content-Disposition: attachment; filename="application.pif" Content-Transfer-Encoding: base64 TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAuAAAAKvnXsbvhjCV74Ywle+GMJVsmj6V44YwlQeZOpX2hjCV74YxlbiGMJVsjm2V AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --Pandora3Bndry_10801157231587976770-- --Pandora3Bndry_1080115724426044878-- milter-0.8.18/test/spam440000644000160600001450000005000110247123425013774 0ustar stuartbmsReceived: from smtp01.mrf.mail.rcn.net (smtp01.mrf.mail.rcn.net [207.172.4.60]) by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g42A1XGQ014740 for ; Thu, 2 May 2002 06:01:33 -0400 Received: from 66-44-42-109.s617.apx1.lnhdc.md.dialup.rcn.com ([66.44.42.109] helo=fjoneill) by smtp01.mrf.mail.rcn.net with smtp (Exim 3.33 #10) id 173DOu-0004vQ-00; Thu, 02 May 2002 06:01:26 -0400 From: "Francis J. O'Neill" To: "Atkinson, Steve" , "Blewett, John" , "Carroll, Matt & Jane" , "Donovan, Kathleen" , "Fitzpatrick, Vince" , "Flannery, Jessica & Beth" , "Fontaine, Gene" , "Fox, Bob" , "Gerken, K." , "Gerken, Kevin \(Home\)" , "Hagan, Carl & Jan" , "Hardcastle, Joe & Carol" , "Hardcastle, Joe" , "Hendrickson, Scott" , "Holl, Mike" , "Jaworski, Francis J" , "JC" , "Joe & Kathy Martin" , "Joe & Kathy Martin" , "Kendle, Greg" , , "pquell" , "Quinan, Phil" , "Quintana, G" , "Rannazzisi, Jim" , "Reed, Kathi" , "Serini, Pete" , "Sherry, Ed" , "Smith, T.J." , "Southard, Jack & Ann" , "Terza, Rick" , "White, Diane" , "Tisdale, David" , "Zilka, Skip & Adella Mae" , "Worrick, Matt & Dyanne" , "Worrick, Matt" , "Weaver Bob & Carol" , "Villa, Al & Jennifer" , "Van Doren, Frank & Joan" , "Trudeau, Tom & Jeri" , "Trowbridge, Paul" , "Trotter, Robert R." , "Tracy, Mike & Patty" , "Tonnessen, Jim & Maria" , "Templeton, Pat" , "Taylor, Michelle" , "Taylor, Fran & Janet" , "Summit, Adelaide" , "Stalker, Nicole" , "Snidal, Brian" , "Smith Danielle" , "Shorten, Jim & Marcia" , "Scoffone, Dave" , "Ryder, Tom & Kim" , "Ryder, Larry & Kate" , "Rossi, Ralph" , "Ross, Scott" , "Riley, Francis" , "Riley, Dave & Susan" , "Riley Tom & Marie" , "Reynolds, Tommy" , "Reynolds, Jim & Noreen" , "Quintana Dick" , "Purdy, Larry & Anne" , "Post, Harold" , "Podledsak, Tom" , "Pino, Ernie & Gloria" , "Pasieka, Tony & Katy" , "Partsch, Jerry & Monica" , "Ong, Ken" , "O'Neill, Mike" , "O'Neill, Frank" , "Oliver, John & Juanita" , "O'Hanlon, Peter \(Work\)" , "O'Hanlon Peter & Anne" , "Noonan, Tim & Bettie" , "Newton Bill" , "Nannery, Phil" , "Nannery, Alison" , "Myrum, Marc" , "Murphy, John & Karen" , "Mullen,OSB, Father Godfrey" , "McCusker, JP & Maggie" , "McCusker, J.P. & Maggie" , "Mathers, David & Kathy" , "Makurat, Dennis" , "Lord, Kevin & Gail" , "Linehan, Pat" , "Linehan, Kellie" , "linehan, Joe" , "Lewandowski, Matt & Mary" , "Lester Doug" , "Kurz, Al & Sandra" , "Koeppel Bruce & Carolyn" , "Kindergan Bob & Dee" , "Kerzner, Ken & Maureen" , "Keating, Russ & Julexy" , "Johnson, Laura" , "Johns, Milt & Shellie" , "Jacobeen, Dave & Maria" , "Hilchey, Paul" , "Head, Rich & Judy" , "Hart Bob & Lorraine" , "Harrington, Thom" , "Harrington Cathy" , "Hammersley, Ron & Ladavadee" , "Grimes, Li nda & Frank" , "Gregory, Glen" , "Gregory Bob & Peggy" , "Greco, Joe & Ann" , "Goodman, Bill & Marcia" , "Goble, Theresa" , "Goble Dick & Theresa" , "Glennon John" , "Gendron, Ray & Barbara" , "Gendron, Jerry" , "Gaynord, Bill & Linda" , "Gareis Charlie" , "Gagat, Ron & Judy" , "Ford, Bobby & Mauren" , "Fontaine, George & Jo" , "Flannery Bill" , "Fini Bob & Beth" , "Ferraro, Sonia & Jack" , "Ferraro, Jack & Sonia" , "Farquhar Butch & Rosa" , "Egitto, John & Ann" , "Economou, Tina" , "Drummond, Scott" , "Drummond, Cheryl" , "Dennin Bob & Mary Jane" , "Daudet, Darryl & Jean" , "Dale Charles" , "Conde, Norman & Josephine" , "Colgan, Charles" , "Clarke Russ & Pat" , "Charters, Nikki" , "Carta, Mike & Sallie" , "Carroll, Pat & Debbie" , "Capozoli, Tom" , "Capozoli, Patty" , "Campbell Michael" , "Callahan, Bob & Marge" , "Byrne, Paul" , "Byrne Kevin" , "Broad, Brian & Brenda" , "Brien, Hugh & Ann" , "Breault, Mike & Katy" , "Branigan Chris & Trish" , "Bland, John & Kerry" , "Berczek, Sr., John & Virginia" , "Barta, Lee" , "Ball, Ken" , "Aveni, Marc & Martha" , "Aveni, Fred & Judy" , "Arseneault, Joe & Jane" , "Alzona, Conrad" , "Aleksy, Rich & Agnes" , "Sebranek, Lyle & Donna" , "Thompson, Dan & Jan" , "Shipko, Dan" , "Robbins, Cecil" , "Pogash, John" , "Mcormack, Pat" , "Mayorga, Sergio" , "Marrin, Bill" , "Jacobeen, David" , "Italion" , "Grieshaber, Jim" , "Corbo, Tony" , "Blank, Bryan" , "Blank, Alaina" , "Webb, Scott & Jenine" , "Webb, Scott & Jenine" , "Gillespie, Erik" Subject: Friday Night at the Lounge Date: Thu, 2 May 2002 06:03:12 -0400 Message-ID: MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="----=_NextPart_000_0002_01C1F19F.0A763E60" X-Priority: 3 (Normal) X-MSMail-Priority: Normal X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2911.0) Importance: Normal X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2600.0000 This is a multi-part message in MIME format. ------=_NextPart_000_0002_01C1F19F.0A763E60 Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: 8bit “FRIDAY NIGHT AT THE GEORGE BRENT LOUNGE” The Lounge will be open this Friday, May 3rd. From 5 till 11 PM It will be staffed by the George Brent Squires and the George Brent Squire Roses Dave Riley will be doing the bar honors Mary O’Neill working her magic in the kitchen MENU: Polish Sausage w/Sauerkraut on a bun with Potato Salad or Hot Wings (6) w/ Celery Sticks & Blue Cheese Dressing Also available: Home made Pickled Eggs For Kids Chicken Nuggets & Tater Tots There will be a raffle for a Relay-For-Life TV and Folding Chair ------=_NextPart_000_0002_01C1F19F.0A763E60 Content-Type: text/html; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable

“FRIDAY NIGHT AT THE GEORGE BRENT = LOUNGE”

The Lounge will be open this Friday, May = 3rd.

From 5 till 11 = PM

It will be staffed by the George Brent = Squires

and the George Brent Squire = Roses

 

Dave Riley will be doing the bar = honors

Mary O’Neill working her magic in the = kitchen

MENU:

Polish Sausage w/Sauerkraut on a = bun

with Potato Salad 

or

Hot Wings (6) w/ Celery Sticks & Blue = Cheese Dressing

Also available: Home made Pickled = Eggs

 <= /p>

For = Kids

Chicken Nuggets & Tater = Tots

 <= /p>

There will be a raffle for a Relay-For-Life =

TV and Folding = Chair

 

 

 <= /p>

 

------=_NextPart_000_0002_01C1F19F.0A763E60-- milter-0.8.18/test/virus130000644000160600001450000001565510247123425014220 0ustar stuartbmsReceived: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41MmROS014480 for ; Wed, 1 May 2002 18:48:27 -0400 Received: from bmsred.bmsi.com (bmsred [219.109.11.50]) by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41MmFGR017812 for ; Wed, 1 May 2002 18:48:15 -0400 X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41M3hOS038584 for ; Wed, 1 May 2002 18:03:43 -0400 X-Received: from exp.dflinc.com (exppub [12.148.147.210]) by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41M3LGQ017812 for ; Wed, 1 May 2002 18:03:22 -0400 X-Received: from exp.dflinc.com (exp.dflinc.com [219.109.14.1]) by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g41M3JGT012258 for ; Wed, 1 May 2002 17:03:19 -0500 X-Received: from dns.intervip.psi.br (dns.intervip.psi.br [200.215.126.2]) by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g3NHlhGS032960 for ; Tue, 23 Apr 2002 12:47:44 -0500 X-Received: from Sncpyf (adsl-fnsbnu-055-k.brt.telesc.net.br [200.180.75.55]) by dns.intervip.psi.br (Postfix) with SMTP id 1FAEE24D18 for ; Tue, 23 Apr 2002 14:50:41 -0300 (BRT) From: enardelli To: lorraine@dflinc.com Subject: A special powful tool MIME-Version: 1.0 Content-Type: multipart/alternative; boundary=XQ4T5Cj14m5h2vQ69IpO4mCG Message-Id: <20020423175041.1FAEE24D18@dns.intervip.psi.br> Date: Tue, 23 Apr 2002 14:50:41 -0300 (BRT) X-ReSent-Date: Wed, 1 May 2002 17:03:03 -0500 (CDT) X-ReSent-From: Gwen Bartelle X-ReSent-To: ed@bmsi.com X-ReSent-Subject: A special powful tool X-ReSent-Message-ID: ReSent-Date: Wed, 1 May 2002 18:47:52 -0400 (EDT) ReSent-From: Ed Bond ReSent-To: Stuart Gathman ReSent-Subject: A special powful tool ReSent-Message-ID: --XQ4T5Cj14m5h2vQ69IpO4mCG Content-Type: text/html; Content-Transfer-Encoding: quoted-printable Hi,This is a special powful tool
I wish you would enjoy it.
--XQ4T5Cj14m5h2vQ69IpO4mCG Content-Type: audio/x-midi; name=hom1;tile=1;ord=3354010700499224[1].scr Content-Transfer-Encoding: base64 Content-ID: TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA CMDDePe/RHj3v5IT+r+Pe/e/kHr3v9Fv97/1Gfq/93H3v1Yc+r/3dve/oGj3v8sK+r+sx/e/ Nyz5v7Hu+b98HD== --XQ4T5Cj14m5h2vQ69IpO4mCG --XQ4T5Cj14m5h2vQ69IpO4mCG Content-Type: application/octet-stream; name=hom1;tile=1;ord=3354010700499224[1].htm Content-Transfer-Encoding: base64 Content-ID: PGh0bWw+PGhlYWQ+PHRpdGxlPkNsaWNrIGhlcmUgdG8gZmluZCBvdXQgbW9yZSE8L3RpdGxl PjwvaGVhZD4NCjxib2R5PjxTQ1JJUFQgTEFOR1VBR0U9SmF2YVNjcmlwdD4KPCEtLQp2YXIg U2hvY2tNb2RlID0gMDsKaWYgKG5hdmlnYXRvci5taW1lVHlwZXMgJiYgbmF2aWdhdG9yLm1p bWVUeXBlc1siYXBwbGljYXRpb24veC1zaG9ja3dhdmUtZmxhc2giXSAmJiBuYXZpZ2F0b3Iu bWltZVR5cGVzWyJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFzaCJdLmVuYWJsZWRQbHVn aW4pIHsKaWYgKG5hdmlnYXRvci5wbHVnaW5zICYmIG5hdmlnYXRvci5wbHVnaW5zWyJTaG9j a3dhdmUgRmxhc2giXSkKU2hvY2tNb2RlID0gMTsKfQplbHNlIGlmIChuYXZpZ2F0b3IudXNl ckFnZW50ICYmIG5hdmlnYXRvci51c2VyQWdlbnQuaW5kZXhPZigiTVNJRSIpPj0wIAomJiAo bmF2aWdhdG9yLnVzZXJBZ2VudC5pbmRleE9mKCJXaW5kb3dzIDkiKT49MCB8fCBuYXZpZ2F0 b3IudXNlckFnZW50LmluZGV4T2YoIldpbmRvd3MgTlQiKT49MCkpIHsKZG9jdW1lbnQud3Jp dGUoJzxTQ1JJUFQgTEFOR1VBR0U9VkJTY3JpcHRcPiBcbicpOwpkb2N1bWVudC53cml0ZSgn b24gZXJyb3IgcmVzdW1lIG5leHQgXG4nKTsKZG9jdW1lbnQud3JpdGUoJ1Nob2NrTW9kZSA9 IChJc09iamVjdChDcmVhdGVPYmplY3QoIlNob2Nrd2F2ZUZsYXNoLlNob2Nrd2F2ZUZsYXNo LjMiKSkpICcpOwpkb2N1bWVudC53cml0ZSgnPFwvU0NSSVBUXD4gJyk7Cn0KaWYgKCBTaG9j a01vZGUgKSB7CmRvY3VtZW50LndyaXRlKCc8T0JKRUNUIGNsYXNzaWQ9ImNsc2lkOkQyN0NE QjZFLUFFNkQtMTFjZi05NkI4LTQ0NDU1MzU0MDAwMCInKTsKZG9jdW1lbnQud3JpdGUoJyBj b2RlYmFzZT0iaHR0cDovL2FjdGl2ZS5tYWNyb21lZGlhLmNvbS9mbGFzaDIvY2Ficy9zd2Zs YXNoLmNhYiN2ZXJzaW9uPTMsMCwwLDAiJyk7CmRvY3VtZW50LndyaXRlKCcgSUQ9YmFubmVy IFdJRFRIPTIzMCBIRUlHSFQ9MjIwPicpOwpkb2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1F PW1vdmllIFZBTFVFPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5ici9hZHMvcG9wXzIzMHgyMjBf Z3Z0X3RlbGVmb25lLnN3Zj9jbGlja3RhZz1odHRwOi8vYWQuYnIuZG91YmxlY2xpY2submV0 L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0wJTNCMCUzQjY2NjEw MDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0dHAlM2ElMmYlMmZ3 d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFsLmpzcCI+ICcpOwpk b2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1FPXF1YWxpdHkgVkFMVUU9YXV0b2hpZ2g+ICcp Owpkb2N1bWVudC53cml0ZSgnPEVNQkVEIFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIv YWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5zd2Y/Y2xpY2t0YWc9aHR0cDovL2FkLmJy LmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUz QjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8NTA5MjQ0fDElM0IlM0Il M2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3Bv cnRhbC5qc3AiJyk7CmRvY3VtZW50LndyaXRlKCcgc3dMaXZlQ29ubmVjdD1GQUxTRSBXSURU SD0yMzAgSEVJR0hUPTIyMCcpOwpkb2N1bWVudC53cml0ZSgnIFFVQUxJVFk9YXV0b2hpZ2gn KTsKZG9jdW1lbnQud3JpdGUoJyBUWVBFPSJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFz aCIgUExVR0lOU1BBR0U9Imh0dHA6Ly93d3cubWFjcm9tZWRpYS5jb20vc2hvY2t3YXZlL2Rv d25sb2FkL2luZGV4LmNnaT9QMV9Qcm9kX1ZlcnNpb249U2hvY2t3YXZlRmxhc2giPicpOwpk b2N1bWVudC53cml0ZSgnPC9FTUJFRD4nKTsKZG9jdW1lbnQud3JpdGUoJzwvT0JKRUNUPicp Owp9IGVsc2UgaWYgKCEobmF2aWdhdG9yLmFwcE5hbWUgJiYgbmF2aWdhdG9yLmFwcE5hbWUu aW5kZXhPZigiTmV0c2NhcGUiKT49MCAmJiBuYXZpZ2F0b3IuYXBwVmVyc2lvbi5pbmRleE9m KCIyLiIpPj0wKSl7CmRvY3VtZW50LndyaXRlKCc8QSBIUkVGPSJodHRwOi8vYWQuYnIuZG91 YmxlY2xpY2submV0L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0w JTNCMCUzQjY2NjEwMDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0 dHAlM2ElMmYlMmZ3d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFs LmpzcCIgVEFSR0VUPSJfYmxhbmsiPjxJTUcgU1JDPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5i ci9hZHMvcG9wXzIzMHgyMjBfZ3Z0X3RlbGVmb25lLmdpZiIgV0lEVEg9MjMwIEhFSUdIVD0y MjAgQk9SREVSPTA+PC9BPicpOwp9Ci8vLS0+CjwvU0NSSVBUPgo8Tk9FTUJFRD48QSBIUkVG PT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8 JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8 NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9w dXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1HIFNSQz0iaHR0 cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5naWYi IFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PRU1CRUQ+CjxOT1NDUklQ VD48QSBIUkVGPT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8 MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAl M0I1MDk5MTd8NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIv bWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1H IFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxl Zm9uZS5naWYiIFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PU0NSSVBU PjwvYm9keT4NCjwvaHRtbD --XQ4T5Cj14m5h2vQ69IpO4mCG-- milter-0.8.18/test/access0000644000160600001450000000045112117767020014134 0ustar stuartbmsSPF-Pass:example.com OK SPF-Neutral:example.com REJECT HELO-Neutral:example.com OK SPF-Permerror:foo@bad.example.com OK SPF-Permerror: REJECT SMTP-Auth:good@example.com OK SMTP-Auth:example.com REJECT SMTP-Auth:bad@localhost.localdomain REJECT SMTP-Test: REJECT milter-0.8.18/test/test10000644000160600001450000000427310247123425013736 0ustar stuartbmsFrom kinga.huszka@wellsfargo.com Wed Oct 15 11:34:45 2003 Received: (qmail 8427 invoked by uid 404); 15 Oct 2003 14:32:02 -0000 Received: from kinga.huszka@aesfargo.com by coyote.nextra.hu by uid 401 with qmail-scanner-1.15 (Clear:. Processed in 3.378056 secs); 15 Oct 2003 14:32:02 -0000 Received: from adsl9.adsl.nextra.hu (HELO marcus.movemany.info) (213.134.24.9) by 0 with SMTP; 15 Oct 2003 14:31:58 -0000 Received: from [192.168.1.12] (cargo2.movemany.info [192.168.1.12]) by marcus.movemany.info (MoveMany Postfix-based Mail Daemon) with ESMTP id 087211F230 for ; Wed, 15 Oct 2003 16:31:55 +0200 (CEST) Subject: Rate Request from Fri 10 Oct 2003 to TIA From: Kinga Fuzz To: World Transportation Systems / Heather Lammy Content-Type: multipart/mixed; boundary="=-mkF0Ur/S0HaYfa60OEsM" Organization: ABC Cargo Message-Id: <1066228317.986.549.camel@cargo2> Mime-Version: 1.0 X-Mailer: Ximian Evolution 1.2.4 Date: 15 Oct 2003 16:31:57 +0200 --=-mkF0Ur/S0HaYfa60OEsM Content-Type: multipart/alternative; boundary="=-VowfKaQaEHb81enMCUlR" --=-VowfKaQaEHb81enMCUlR Content-Type: text/plain Content-Transfer-Encoding: 7bit Dear Heather, First of all, I would like to ask you to send your emails to our general email and its associated attachments is strictly prohibited. --=-VowfKaQaEHb81enMCUlR Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: 7bit Dear Heather,
--=-VowfKaQaEHb81enMCUlR-- --=-mkF0Ur/S0HaYfa60OEsM Content-Disposition: attachment; filename*0="14676 World Transportation Systems OF, from arrival TIA term"; filename*1="inal to door and from Durres port to TIA.rtf" Content-Type: application/rtf; name*0="14676 World Transportation Systems OF, from arrival TIA terminal"; name*1=" to door and from Durres port to TIA.rtf" Content-Transfer-Encoding: 7bit {\rtf1\ansi\deff1\adeflang1025 \par } --=-mkF0Ur/S0HaYfa60OEsM-- milter-0.8.18/test/bounce0000644000160600001450000001177710247123425014160 0ustar stuartbmsReceived: from localhost (localhost) by bmsaix.bmsi.com (8.12.9/8.12.6) id h62JqW5p030912; Wed, 2 Jul 2003 15:52:32 -0400 Date: Wed, 2 Jul 2003 15:52:32 -0400 From: Mail Delivery Subsystem Message-Id: <200307021952.h62JqW5p030912@bmsaix.bmsi.com> To: MIME-Version: 1.0 Content-Type: multipart/report; report-type=delivery-status; boundary="h62JqW5p030912.1057175552/bmsaix.bmsi.com" Subject: Returned mail: see transcript for details Auto-Submitted: auto-generated (failure) This is a MIME-encapsulated message --h62JqW5p030912.1057175552/bmsaix.bmsi.com The original message was received at Fri, 27 Jun 2003 15:28:03 -0400 from IDENT:ndcHoBWTR9Bf/rEFYJRejRoPTaRDgSCl@bmsweb.bmsi.com [192.168.9.81] ----- The following addresses had permanent fatal errors ----- makurat@erols.com (reason: 452 4.3.0 Filter failure) (expanded from: ) ----- Transcript of session follows ----- ... while talking to [192.168.9.81]: >>> DATA <<< 452 4.3.0 Filter failure makurat@erols.com... Deferred: 452 4.3.0 Filter failure Message could not be delivered for 5 days Message will be deleted from queue --h62JqW5p030912.1057175552/bmsaix.bmsi.com Content-Type: message/delivery-status Reporting-MTA: dns; bmsaix.bmsi.com Arrival-Date: Fri, 27 Jun 2003 15:28:03 -0400 Final-Recipient: RFC822; makurat@bmsi.com X-Actual-Recipient: RFC822; makurat@erols.com Action: failed Status: 4.4.7 Remote-MTA: DNS; [192.168.9.81] Diagnostic-Code: SMTP; 452 4.3.0 Filter failure Last-Attempt-Date: Wed, 2 Jul 2003 15:52:32 -0400 --h62JqW5p030912.1057175552/bmsaix.bmsi.com Content-Type: message/rfc822 Return-Path: Received: from spidey.bmsi.com (IDENT:ndcHoBWTR9Bf/rEFYJRejRoPTaRDgSCl@bmsweb.bmsi.com [192.168.9.81]) by bmsaix.bmsi.com (8.12.9/8.12.6) with ESMTP id h5RJS3Vi042394 for ; Fri, 27 Jun 2003 15:28:03 -0400 Received: from sunlong.com ([202.105.130.54]) by spidey.bmsi.com (8.11.6/8.11.6) with SMTP id h5RJS2o03547 for ; Fri, 27 Jun 2003 15:28:02 -0400 Message-Id: <200306271928.h5RJS2o03547@spidey.bmsi.com> Received: from mx06.mail.bellsouth.net([218.104.6.10]) by sunlong.com(JetMail 2.5.3.0) with SMTP id jma73efca64b; Fri, 27 Jun 2003 19:23:44 -0000 To: From: "Stacy McClain" Subject: Defy Gravity in 15 minutes Date: Sat, 28 Jun 2003 03:34:15 -1600 MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_646C_00001D33.00000BE1" Reply-To: annagh000@bellsouth.net X-AntiAbuse: : This header was added to track abuse, please include it with any abuse report X-AntiAbuse: Primary Hostname - 210.222.2.13 X-Originating-Host: : 210.188.201.159 ------=_NextPart_000_646C_00001D33.00000BE1 Content-Type: text/html; charset="iso-8859-1" Content-Transfer-Encoding: base64 PGh0bWw+DQoNCjxoZWFkPg0KPHRpdGxlPjwvdGl0bGU+DQo8L2hlYWQ+DQoNCjxib2R5Pg0KDQo8cD4NCjxhIGhyZWY9Imh0dHA6Ly9zcmQueWFob28uY29tL2Ryc3QvNzQxMjQzMjM1LypodHRwOi93d3cuZnJ5YmVlLmNvbS8iPg0KPGltZyBzcmM9Imh0dHA6Ly8yMTAuMTUuNTEuOTUvcGljX3dlbGwvZ3YyLmdpZiIgYm9yZGVyPSIwIiB3aWR0aD0iNDA1IiBoZWlnaHQ9IjI3MCI+PC9hPjwvcD4NCg0KPHA+DQo8YSBocmVmPSJodHRwOi8vc3JkLnlhaG9vLmNvbS9kcnN0Lzc0MTQxNjg4Mjc3NzcvKmh0dHA6L3d3dy5mcnliZWUuY29tL3BhZ2UvYS5odG1sIj4NCjxpbWcgc3JjPSJodHRwOi8vY2xpY2suanVzdGZvcnlvdS1tYWlsLmNvbS9pbWFnZXMvRjEuZ2lmIiB3aWR0aD0iNDEwIiBoZWlnaHQ9IjE0IiBib3JkZXI9IjAiPjwvYT48L3A+DQoNCjxwIGFsaWduPSJsZWZ0Ij4NCiZuYnNwOzwvcD4NCg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6IDA7IG1hcmdpbi1ib3R0b206IDAiPg0KJm5ic3A7PC9wPg0KDQo8cCBzdHlsZT0ibWFyZ2luLXRvcDogMDsgbWFyZ2luLWJvdHRvbTogMCI+DQombmJzcDs8L3A+DQoNCjxwIHN0eWxlPSJtYXJnaW4tdG9wOiAwOyBtYXJnaW4tYm90dG9tOiAwIj4NCiZuYnNwOzwvcD4NCg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6IDA7IG1hcmdpbi1ib3R0b206IDAiPjxmb250IHNpemU9IjEiPnFhd3NteXp0ciBxYXdzYW9lZHRhZ2ZwdiANCnFhd3N5ZmRhb3FqIHFhd3NjaSBxYXdzY! 212Z3ZrIHFhd3NvaW55d3pkbyBxYXdzbXVxYXdza29jIA0KcWF3c2hobmVkZCBxYXdzZWllbiBxYXdzemlnZ3hucGN2cyBxYXdzd3lkZSBxYXdzeWFwIHFhd3NxamVkeWhxYXdzZmt1bSANCnFhd3NmbSBxYXdzdW11Ym1mYmR3IHFhd3Nkc29ka2xvIHFhd3Nhc2VtayBxYXdzZXdzIHFhd3NxdWRneGVvcWF3c3J6IA0KcWF3c290dSBxYXdzcHplbnJoZW1xYSBxYXdzdXplcmpqcWZxIHFhd3NydWFucyBxYXdzbnBjcGFoZ2pwIHFhd3NxYXdoZHJxYXdzYmFscXNxaiANCnFhd3N5bmggcWF3c2VrIHFhd3N0YmNndGd0IHFhd3N0ZnhzeHd4ICBxYXdzandlcHFhd3NsYmN6ZWRuIHFhd3NzcW1nb3YgDQpxYXdzZ3phdiBxYXdzZ2N2aCBxYXdzd21sYWt1bW5sbiBxYXdzZHpqcW9yeCBxYXdzdGhvbHRmaWxmeHFhd3NpcGJneSANCnFhd3NpbHp5Znd2dnMgIHFhd3NpdmJwdmNiIHFhd3NrZXRpYmtocGRhIHFhd3N6ZmJqYm1yayBxYXdzbWZvZ29ucWF3c2FvIA0KcWF3c21vcXggcWF3c3FkeWVuaCBxYXdzYnMgcWF3c2l5aXBkYWx4IHFhd3N6aXlpbyBxYXdzaWZ6dXFyamltcSANCnFhd3NuayBxYXdza3dhciBxYXdzanNleHNmc2IgcWF3c3RxaWlhY2cgcWF3c2p0YnFobnFlIHFhd3Niam1pcGpxYXdzaHl4anNwbXhuIA0KIHFhd3NqcmJlbnIgcWF3c3p6b3p0ZndydyBxYXdzZ25uaHdjIHFhd3NrdXkgcWF3c3ZwcWF3c25qbmd5eHl1eCBxYXdzd3lvc2EgDQpxYXdzb2lnIHFhd3Nub25rcm5pbWcgcWF3c2NtcGdxemtwcm! U8L2ZvbnQ+PC9wPg0KDQo8L2JvZHk+DQoNCjwvaHRtbD48L3RpdGxlPg0K ------=_NextPart_000_646C_00001D33.00000BE1-- --h62JqW5p030912.1057175552/bmsaix.bmsi.com-- milter-0.8.18/test/spam80000644000160600001450000002172710247123425013731 0ustar stuartbmsReceived: from mail.pro-send.com (smtp12.pro-send.com [65.124.197.229]) by www.bmsi.com (8.12.3/8.12.3) with ESMTP id g927mSVA017008 for ; Wed, 2 Oct 2002 03:48:29 -0400 Received: from pro-send.com [65.124.197.226] by mail.pro-send.com (SMTPD32); Wed, 2 Oct 2002 02:11:02 -0500 DATE: 02 Oct 02 2:11:02 CDT FROM: John Oglesby Reply-To: John Oglesby TO: Lindsay Shrader SUBJECT: Lindsay Shrader Message-Id: <2002100202.RS11@mail.pro-send.com> MIME-Version: 1.0 Content-Type: multipart/alternative; boundary=1002029 --1002029 Content-Type: text/plain; charset=us-ascii A SYSTEM for FREEDOM Don't call in Sick... Call in WELL... Extremely Well! If you want to see how, Click Here. Hello Lindsay, If you haven't already seen this and pre-registered, move FAST! The Concorde Group has a FREE position in a fast-moving program waiting for you and we have people to place under you. We'll notify you when you have a CHECK WAITING. This FREE position is waiting for Lindsay Shrader. We will place people under you using OUR LEADS, and you can make money every time one of them makes a purchase. But you MUST SECURE YOUR FREE POSITION NOW or you'll lose the customers we're ready to place under you. Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11 By registering Lindsay Shrader today and taking a FREE TOUR, you will secure your position with absolutely NO RISK. Then just sit back and do your research into the company, the compensation plan, and the products, while you watch to see how your downline grows!! Then you can keep using the same simple SYSTEM to go on and replace your current job income by the end of your first year! Take Your Free Tour Now: Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11 Yours in Success, John Oglesby joglesby2@msn.com 1+(877)-868-0143 Home 972-878-2683 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HOW DID WE LEARN ABOUT YOUR INTEREST IN A HOME-BASED BUSINESS? You responded to one of our ads. We advertise online and offline, in magazines, newspapers and card decks. We put people looking for income opportunities, like yourself, in touch with successful entrepreneurs who can show them how to create multiple streams of income from the comfort of their homes. Hopefully that answers your question. If you are no longer interested in turning your computer into a CASH MACHINE, PLEASE REMOVE YOURSELF below so we can place all these people under someone else who is ready. ____________________________________________________________ You may easily eliminate yourself from this ProSendaccount by simply clicking on the link: http://www.pro-send.com/x/?6C6938E41D1OR go to: http://www.pro-send.com/x/and enter this code when prompted: 6C6938E41D1____________________________________________________________ --1002029 Content-Type: text/html; A SYSTEM for FREEDOM

Don't call in Sick...

Call in WELL... Extremely Well!

Click Here

If you want to see how, Click Here.

Hello Lindsay,

If you haven't already seen this and pre-registered, move FAST!

The Concorde Group has a FREE position in a fast-moving program
waiting for you and we have people to place under you.

We'll notify you when you have a CHECK WAITING.

This FREE position is waiting for Lindsay Shrader.

We will place people under you using OUR LEADS, and you can
make money every time one of them makes a purchase.
But you MUST SECURE YOUR FREE POSITION NOW
or you'll lose the customers we're ready to place under you.

Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11

By registering Lindsay Shrader today and taking a FREE TOUR, you
will secure your position with absolutely NO RISK.

Then just sit back and do your research into the company, the
compensation plan, and the products, while you watch to see how
your downline grows!!

Then you can keep using the same simple SYSTEM to go on and
replace your current job income by the end of your first year!

Take Your Free Tour Now:
Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11

Yours in Success,

John Oglesby
joglesby2@msn.com
1+(877)-868-0143
Home 972-878-2683
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
HOW DID WE LEARN ABOUT YOUR INTEREST IN A HOME-BASED BUSINESS?

You responded to one of our ads. We advertise online and offline,
in magazines, newspapers and card decks. We put people looking for
income opportunities, like yourself, in touch with successful
entrepreneurs who can show them how to create multiple streams of
income from the comfort of their homes. Hopefully that answers your
question.

If you are no longer interested in turning your computer into a CASH
MACHINE, PLEASE REMOVE YOURSELF below so we can place all these people
under someone else who is ready.



____________________________________________________________

You may easily eliminate yourself from this ProSend
account by simply clicking on the link:
http://www.pro-send.com/x/?6C6938E41D1
OR go to:
http://www.pro-send.com/x/
and enter this code when prompted: 6C6938E41D1
____________________________________________________________
--1002029-- milter-0.8.18/test/test80000644000160600001450000001072410247123425013743 0ustar stuartbmsReceived: from mail pickup service by hotmail.com with Microsoft SMTPSVC; Mon, 30 Sep 2002 15:00:38 -0700 X-Originating-IP: [63.157.17.3] From: "Debbie Morrison" To: "Ann & Richard Black" , "Bill/Dorothy" , "Cindy Kohr" , "Debbie Morrison" , "DONNA MORRISON" , "Glenda/Johnny Holmes" , "HAROLDMAXINE STROUD" , "Janis & Bob Mathis" , "Sherry Bigham" , "Mark Bigham" Subject: Fw: Fw: ILLUSIONS Date: Thu, 26 Sep 2002 06:48:47 -0700 MIME-Version: 1.0 X-Mailer: MSN Explorer 7.02.0005.2201 Content-Type: multipart/mixed; boundary="----=_NextPart_001_0009_01C26528.C39C68E0" Message-ID: X-OriginalArrivalTime: 30 Sep 2002 22:00:38.0335 (UTC) FILETIME=[CF7AE4F0:01C268CC] X-IMAPbase: 1033583964 1 Status: RO X-Status: X-Keywords: X-UID: 1 ------=_NextPart_001_0009_01C26528.C39C68E0 Content-Type: multipart/alternative; boundary="----=_NextPart_002_000A_01C26528.C39C68E0" ------=_NextPart_002_000A_01C26528.C39C68E0 Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable Keep opening on the forwards. Cool =20 =20 ----- Original Message ----- From: Got2Fish42@aol.com Sent: Tuesday, September 24, 2002 3:16 PM To: dugiew@cox-internet.com; txnrnt@yahoo.com; mbrock@tstar.net; DendyDl@= swbell.net; sdickey@att.net; deasley@vzinet.com; fmmorrison@msn.com; mama= jack4@juno.com; DMorr42886@aol.com; LStra415@aol.com; wrwebster@juno.com;= GWIL@tjc.edu Subject: Fwd: Fw: ILLUSIONS Get more from the Web. FREE MSN Explorer download : http://explorer.msn= .com ------=_NextPart_002_000A_01C26528.C39C68E0 Content-Type: text/html; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable
Keep opening o= n the forwards.  Cool 
 
= ----- Original Message -----
From: Got2Fish42@aol.com
Sent: Tuesday, September 24, 2002 3:16 P= M
To: dugiew@cox-internet.co= m; txnrnt@yahoo.com; mbrock@tstar.net; DendyDl@swbell.net; sdickey@att.ne= t; deasley@vzinet.com; fmmorrison@msn.com; mamajack4@juno.com; DMorr42886= @aol.com; LStra415@aol.com; wrwebster@juno.com; GWIL@tjc.edu
Subject: Fwd: Fw: ILLUSIONS
&= nbsp;



Get more fr= om the Web. FREE MSN Explorer download : http://explorer.msn.com

------=_NextPart_002_000A_01C26528.C39C68E0-- ------=_NextPart_001_0009_01C26528.C39C68E0 Content-Type: message/rfc822; name="Fwd_ Fw_ ILLUSIONS.email" Content-Disposition: attachment; filename="Fwd_ Fw_ ILLUSIONS.email" Content-Transfer-Encoding: quoted-printable Return-path: From: Bclc48@aol.com Full-name: Bclc48 Message-ID: <42.2de5cbf8.2ac10b39@aol.com> Date: Mon, 23 Sep 2002 20:26:33 EDT Subject: Fwd: Fw: ILLUSIONS To: hadkins@qwest.net, Bardojm@aol.com, swa_tom@swbell.net, eve@mixedmediaoutdoor.com, ArthurJaharris11@aol.com, j.gual@worldnet.att.net, JOSEFUR@cs.com, AR2976@aol.com, CCcaro@aol.com, Zgirlnan@aol.com, Got2Fish42@aol.com MIME-Version: 1.0 Content-Type: multipart/mixed; boundary=3D"part2_46.2e38b118.2ac10b39_bou= ndary" X-Mailer: AOL 7.0 for Windows US sub 10641 --part2_46.2e38b118.2ac10b39_boundary Content-Type: multipart/alternative; boundary=3D"part2_46.2e38b118.2ac10b39_alt_boundary" --part2_46.2e38b118.2ac10b39_alt_boundary Content-Type: text/plain; charset=3D"US-ASCII" Content-Transfer-Encoding: 7bit this is good --part2_46.2e38b118.2ac10b39_alt_boundary Content-Type: text/html; charset=3D"US-ASCII" Content-Transfer-Encoding: 7bit --part2_46.2e38b118.2ac10b39_alt_boundary-- --part2_46.2e38b118.2ac10b39_boundary-- ------=_NextPart_001_0009_01C26528.C39C68E0-- milter-0.8.18/test/amazon0000644000160600001450000013571710247123425014173 0ustar stuartbmsFrom stuart@bmsi.com Wed May 1 14:37:14 2002 Return-Path: Received: from bmsi.com (IDENT:stuart@localhost [127.0.0.1]) by gathman.bmsi.com (8.11.6/8.11.6) with ESMTP id g41IbCF01796 for ; Wed, 1 May 2002 14:37:13 -0400 Sender: stuart@gathman.bmsi.com Message-ID: <3CD035D7.18ADF27F@bmsi.com> Date: Wed, 01 May 2002 14:37:11 -0400 From: "Stuart D. Gathman" Organization: Business Management Systems, Inc. X-Mailer: Mozilla 4.78 [en] (X11; U; Linux 2.4.9-21 i586) X-Accept-Language: en MIME-Version: 1.0 To: stuart@gathman.bmsi.com Subject: Amazon.com--Earth's Biggest Selection Content-Type: multipart/mixed; boundary="------------59A46341C90BA737DD47867B" This is a multi-part message in MIME format. --------------59A46341C90BA737DD47867B Content-Type: multipart/alternative; boundary="------------0B098FB91956AC123C61B151" --------------0B098FB91956AC123C61B151 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065 -- Stuart D. Gathman Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154 "Confutatis maledictis, flamis acribus addictis" - background song for a Microsoft sponsored "Where do you want to go from here?" commercial. --------------0B098FB91956AC123C61B151 Content-Type: text/html; charset=us-ascii Content-Transfer-Encoding: 7bit http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065
-- 
              Stuart D. Gathman 
Business Management Systems Inc.  Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flamis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.
  --------------0B098FB91956AC123C61B151-- --------------59A46341C90BA737DD47867B Content-Type: text/html; charset=us-ascii; name="103-3111065-2579065" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="103-3111065-2579065" Content-Base: "http://www.amazon.com/exec/obidos/subs t/home/redirect.html/103-3111065-25 79065" Content-Location: "http://www.amazon.com/exec/obidos/subs t/home/redirect.html/103-3111065-25 79065" Amazon.com--Earth's Biggest Selection
     
Hello, Stuart D. Gathman. We have recommendations for you. (If you're not Stuart D. Gathman, click here.)

Search Amazon.com

Browse Amazon.com
• Books
• Electronics
• Baby &
   Baby Registry
• Music
• Health & Beauty
• DVD
• Software
• Kitchen &
   Housewares
• Tools &
   Hardware
• Computers
• Camera & Photo
• Movie Showtimes
• Computer &
   Video Games
• Toys & Games
• Cell Phones
   & Service
• Video
• Magazine
   Subscriptions
• Outdoor Living
• Travel
• Cars
• Gifts &
   Gift Certificates
• Auctions
• zShops
• Outlet
• Corporate
   Accounts

Browse Partners
• Target
• Toysrus.com
• Babiesrus.com

Special Features

Associates
Sell books, music, videos, and more from your Web site. Start earning today!


 



Pre-order the Oscar®-winning blockbuster The Lord of the Rings: The Fellowship of the Ring, arriving on DVD and video August 6.

In Gifts
Mother's Day Is May 12
We've made it fun and easy to buy the perfect present for Mom. Shop by recipient or price, browse top sellers, or order flowers. Visit Gifts for these and more great ideas for expressing your love and appreciation.
 

Your Recommendations
War in Heaven
Amazon.com
"The telephone was ringing wildly," begins Charles Williams's novel War in Heaven, "but without result, since there was no-one in the room but the corpse." From this abrupt--and darkly humorous--start, Williams takes us on a 20th-century version of the Grail quest, with an Archdeacon, a Duke, and an... Read more | (Why was I recommended this?)

More Recommendations
Icon Reliable Linux by Iain Campbell (Why?)
Icon Programming PHP by Rasmus Lerdorf, et al (Why?)
Icon Descent into Hell by Charles W. Williams (Why?)
Icon Network Troubleshooting Tools (O'Reilly System Administration) by Joseph D. Sloan (Why?)

Your Music Store
Isaac Freeman, et al, Beautiful Stars
Great African American gospel music has an indisputable power, rooted in the audible faith of its performers and the beauty of their voices. As the bass singer of the Fairfield Four, an a cappella group that started more than a half century ago,... Read more


More Stores:

IconYour Electronics Store: iRiver SlimX iMP-350 CD/MP3 Player with 8 minutes ASP and Upgradeable Firmware by iRiver
IconYour Video Store: Ocean's Eleven VHS ~ George Clooney

Listmania!
(What is this?)

Best Linux Security books: A list by J. Parker, Administrator, hacker.
(7 item list)

Networking: A list by gakis, Engineer
(13 item list)

In Travel
Your Next Vacation Starts Here
Save up to 70% on hotels from Vegas to New York and everywhere in between on Expedia.com. Book a flight during Hotwire's major-airline Spring Sale through May 2 and fly the big-name airlines at no-name airline prices. The Vacation Store is offering seven-day Holland America Caribbean cruises from just $599.
 




New For You
Stuart, check out what's New for You:
(If you're not Stuart D. Gathman, click here.)

Your Message Center
! You have 5 new messages.

Your Shopping Cart
Shopping Cart You have 0 items in your Shopping Cart.

Your New Releases
Icon Pop
Icon Christian & Gospel
Icon Computers & Internet
Icon Cookware
Icon Action & Adventure
More New Releases

Movers & Shakers
Up 974%
Icon Dorothy L. Sayers Mysteries (Strong Poison / Have His Carcass / Gaudy Night) DVD
~ Dorothy L. Sayers
Up 2,415%
Icon Artemis Fowl
by Eoin Colfer
More Movers & Shakers

Where's My Stuff?
• Track your recent orders.
• View or change your orders in Your Account.
Shipping & Returns
• See our shipping rates & policies.
Return an item (here's our Returns Policy).
Need Help?
• Forgot your password? Click here.
Redeem or buy a gift certificate.
Visit our Help department.
Search    for     

Stuart D. Gathman, make $310.61
Sell your past purchases at Amazon.com today!

Text Only Top of Page

Directory of All Stores

Our International Sites: United Kingdom   |   Germany   |   Japan   |   France

Help  |   Shopping Cart  |   Your Account  |   Sell Items  |   1-Click Settings

About Amazon.com  |   Join Our Staff  |   Join Associates  |   Join Advantage  |   Join Honor System

Conditions of Use | Privacy Notice © 1996-2002, Amazon.com, Inc. or its affiliates
--------------59A46341C90BA737DD47867B-- milter-0.8.18/test/virus10000644000160600001450000000567310247123425014134 0ustar stuartbmsReceived: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) by bmsaix.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA42304 for ; Thu, 4 May 2000 05:22:03 -0400 Received: from camco.celestial.com (root@dagney.celestial.com [192.136.111.7]) by www.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA21364 for ; Thu, 4 May 2000 05:22:01 -0400 Received: (12482 bytes) by camco.celestial.com via sendmail with P:stdio/D:lists/R:inet_hosts/T:smtp (sender: owner: ) id for flexfax-outbound; Thu, 4 May 2000 02:15:30 -0700 (PDT) (Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13) Received: from sgi.com(sgi.SGI.COM[192.48.153.1]) (12116 bytes) by camco.celestial.com via sendmail with P:esmtp/D:aliases/T:pipe (sender: owner: ) id for ; Thu, 4 May 2000 02:13:16 -0700 (PDT) (Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13) Received: from proxy.internet ([195.184.42.82]) by sgi.com (980327.SGI.8.8.8-aspam/980304.SGI-aspam: SGI does not authorize the use of its proprietary systems or networks for unsolicited or bulk email from the Internet.) via ESMTP id CAA02330 for ; Thu, 4 May 2000 02:13:10 -0700 (PDT) mail_from (orum@ditas.dk) Received: from [172.16.96.14] by proxy.daab.dkproxy.internet (NTMail 4.30.0013/NU4152.00.32401f35) with ESMTP id zmlyaaaa for ; Thu, 4 May 2000 11:13:09 +0200 Received: by mars with Internet Mail Service (5.5.2650.21) id ; Thu, 4 May 2000 11:11:13 +0100 Message-ID: <9704D2AA604ED311BF6D0008C79F0A990B57BE@mars> From: =?iso-8859-1?Q?Peter_=D8rum?= To: "'flexfax@sgi.com'" Subject: flexfax: ILOVEYOU Date: Thu, 4 May 2000 11:11:11 +0100 MIME-Version: 1.0 X-Mailer: Internet Mail Service (5.5.2650.21) Content-Type: multipart/mixed; boundary="----_=_NextPart_000_01BFB5B1.13228432" Sender: owner-flexfax@celestial.com Precedence: bulk This message is in MIME format. Since your mail reader does not understand this format, some or all of this message may not be legible. ------_=_NextPart_000_01BFB5B1.13228432 Content-Type: text/plain kindly check the attached LOVELETTER coming from me. ------_=_NextPart_000_01BFB5B1.13228432 Content-Type: application/octet-stream; name="LOVE-LETTER-FOR-YOU.TXT.vbs" Content-Transfer-Encoding: quoted-printable Content-Disposition: attachment; filename="LOVE-LETTER-FOR-YOU.TXT.vbs" rem barok -loveletter(vbe) rem by: spyder / ispyder@mail.com / @GRAMMERSoft Group / = Manila,Philippines On Error Resume Next set b=3Dfso.CreateTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM") b.close set d=3Dfso.OpenTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM",2) d.write dt5 d.write join(lines,vbcrlf) d.write vbcrlf d.write dt6 d.close end sub ------_=_NextPart_000_01BFB5B1.13228432-- milter-0.8.18/test/spam70000644000160600001450000000211710247123425013720 0ustar stuartbmsReceived: from mail pickup service by hotmail.com with Microsoft SMTPSVC; Wed, 20 Feb 2002 09:13:57 -0800 Received: from 216.144.70.231 by lw7fd.law7.hotmail.msn.com with HTTP; Wed, 20 Feb 2002 17:13:44 GMT X-Originating-IP: [216.144.70.231] From: "jim simmons" Bcc: Subject: Just another "Crappy Day in Paradise" here @ the Ranch Date: Wed, 20 Feb 2002 10:13:44 -0700 Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_4e56_490d_48e3" Message-ID: X-OriginalArrivalTime: 20 Feb 2002 17:13:57.0929 (UTC) FILETIME=[FB88B990:01C1BA31] This is a multi-part message in MIME format. ------=_NextPart_000_4e56_490d_48e3 Content-Type: text/html Test ------=_NextPart_000_4e56_490d_48e3 Content-Type: image/pjpeg; name="Jim&amp;Girlz.jpg" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="Jim&amp;Girlz.jpg" /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0N UUUAFFFFABRRRQB//9k= ------=_NextPart_000_4e56_490d_48e3-- milter-0.8.18/test/bound0000644000160600001450000000560410247123425014004 0ustar stuartbmsFrom dspam Mon Sep 29 16:36:23 2003 Received: from orcon.net.nz (port-219-88-129-82.orcon.net.nz [219.88.129.82]) by spidey.planet.com (8.11.6/8.11.6) with SMTP id h8Q85c414321 for ; Fri, 26 Sep 2003 04:05:39 -0400 Date: Fri, 26 Sep 2003 20:05:56 +1200 From: Mail Delivery Subsystem Message-Id: <200309262005.IEI23104@mx1.orcon.net.nz> To: MIME-Version: 1.0 Content-Type: multipart/report; report-type=delivery-status; boundary="IEI23104.1064534400/mx1.orcon.net.nz" Subject: Returned mail: User unknown Auto-Submitted: auto-generated (failure) X-DSpam-HeaderScore: 0.007433 This is a MIME-encapsulated message --IEI23104.1064534400/mx1.orcon.net.nz The original message was received at Fri, 26 Sep 2003 20:05:56 +1200 from ----- The following addresses had permanent fatal errors ----- (expanded from: ) ----- Transcript of session follows ----- mail.local: unknown name: mike-liz 550 ... User unknown --IEI23104.1064534400/mx1.orcon.net.nz Content-Type: message/delivery-status Reporting-MTA: dns; mx1.orcon.net.nz Received-From-MTA: DNS; Arrival-Date: Fri, 26 Sep 2003 20:05:56 +1200 Final-Recipient: RFC822; X-Actual-Recipient: RFC822; mike-liz@orcon.net.nz Action: failed Status: 5.1.1 Last-Attempt-Date: Fri, 26 Sep 2003 20:05:56 +1200 --IEI23104.1064534400/mx1.orcon.net.nz Content-Type: message/rfc822 Return-Path: Received: from global_1.bugle.com ([12.4.120.82]) by dbmail-mx3.orcon.co.nz (8.12.6/8.12.6/Debian-7) with ESMTP id h8O6CRJ8015038 for ; Wed, 24 Sep 2003 18:12:28 +1200 From: postmaster@bugle.com To: mike-liz@orcon.net.nz Date: Wed, 24 Sep 2003 02:13:53 -0400 MIME-Version: 1.0 Content-Type: multipart/report; report-type=delivery-status; boundary="9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle." X-DSNContext: 335a7efd - 4457 - 00000001 - 80040546 Message-ID: Subject: Delivery Status Notification (Failure) X-Spam-Score: 3.5 (***) BANG_MONEY,CASHCASHCASH,EXCUSE_10,EXCUSE_14,MAILTO_TO_SPAM_ADDR,NO_REAL_NAME,SENT_IN_COMPLIANCE X-Scanned-By: MIMEDefang 2.32 (www . roaringpenguin . com / mimedefang) This is a MIME-formatted message. Portions of this message may be unreadable without a MIME-capable mail program. --9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle. Content-Type: text/plain; charset=unicode-1-1-utf-7 This is an automatically generated Delivery Status Notification. Delivery to the following recipients failed. jholt@bugle.com --9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle. Content-Type: message/delivery-status Reporting-MTA: dns;global_1.bugle.com Received-From-MTA: dns;gts.bugle.com --IEI23104.1064534400/mx1.orcon.net.nz-- milter-0.8.18/test/virus60000644000160600001450000000220010247123425014120 0ustar stuartbmsFrom mdb@go2net.com Tue Sep 18 10:31:34 2001 Received: from aglnss01.grupoagrisal.net ([172.16.0.1]) by agntss05 (Lotus Domino Release 5.07a) with ESMTP id 2001120416164050:5294 ; Tue, 4 Dec 2001 16:16:40 -0600 Subject: MAEU XSS025786 - ORDER 1251 - CONTAINER MAEU 6053725 To: kathyp@jsconnor.com Cc: Blanca@ace-of-hearts.net X-Mailer: Lotus Notes Release 5.07a May 14, 2001 Message-ID: From: sherrera.dco.lc@agrisal.com Date: Tue, 4 Dec 2001 16:11:48 -0600 MIME-Version: 1.0 X-MIMETrack: Serialize by Router on AGLNSS01/AGRISAL(Release 5.07a |May 14, 2001) at 04/12/2001 04:11:57 p.m., Itemize by SMTP Server on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at 12/04/2001 04:16:41 PM, Serialize by Router on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at 12/04/2001 04:16:51 PM Content-type: application/octet-stream; name="FAX20.exe" Content-Disposition: attachment; filename="FAX20.exe" Content-Transfer-Encoding: base64 TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAKJsVAAAACIAACIAACIBr6AQA milter-0.8.18/test/virus30000644000160600001450000000501610247123425014125 0ustar stuartbmsReceived: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EMUxS24174 for ; Fri, 14 Sep 2001 18:30:59 -0400 Received: from bmsred.bmsi.com (bmsred [219.109.11.50]) by www.bmsi.com (8.9.1/8.9.1) with ESMTP id SAA12740 for ; Fri, 14 Sep 2001 18:30:58 -0400 X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EESNW28934 for ; Fri, 14 Sep 2001 10:28:23 -0400 X-Received: from bwi.bwicorp.com (bwi.bwicorp.com [209.116.254.106]) by www.bmsi.com (8.9.1/8.9.1) with ESMTP id KAA34262 for ; Fri, 14 Sep 2001 10:28:20 -0400 X-Received: from bwicorp.com (bwi3 [192.168.3.22]) by bwi.bwicorp.com (8.9.1/8.9.1) with ESMTP id KAA42970 for ; Fri, 14 Sep 2001 10:33:54 -0400 Date: Fri, 14 Sep 2001 10:33:54 -0400 From: Mary Smith Message-Id: <200109141433.KAA42970@bwi.bwicorp.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="==i3.9.0oisdboibsd((kncd" ReSent-Date: Fri, 14 Sep 2001 18:30:47 -0400 (EDT) ReSent-From: Ed Bond ReSent-To: Stuart Gathman ReSent-Subject: Resent mail.... ReSent-Message-ID: --==i3.9.0oisdboibsd((kncd Content-Type: application/octet-stream; name="READER_DIGEST_LETTER.TXT.pif" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="READER_DIGEST_LETTER.TXT.pif" TVpQAAIAAAAEAA8A//8AALgAAAAAAAAAQAAaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAEAALoQAA4ftAnNIbgBTM0hkJBUaGlzIHByb2dyYW0gbXVzdCBiZSBydW4gdW5kZXIgV2lu MzINCiQ3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBFAABMAQQA5ijojgAAAAAAAAAA4ACOgQsBAhkA FAAAAAYAAAAAAAAAEAAAABAAAAAwAAAAAEAAABAAAAACAAABAAAAAAAAAAMACgAAAAAAAMAAAAAE AAAAAAAAAgAAAAAAEAAAIAAAAAAQAAAQAAAAAAAAEAAAAAAAAAAAAAAAAEAAAIoAAAAAUAAAAAYA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ09ERQAAAAAA IAAAABAAAAAUAAAABgAAAAAAAAAAAAAAAAAAIAAA4ERBVEEAAAAAABAAAAAwAAAAAgAAABoAAAAA AAAAAAAAAAAAAEAAAMAuaWRhdGEAAAAQAAAAQAAAAAIAAAAcAAAAAAAAAAAAAAAAAABAAADALnJz cmMAAAAAgAAAAFAAAAAwAAAAHgAAAAAAAAAAAAAAAAAAQAAA0AAAAAAAAAAAAAAAAAAAAAAAAAAA RDY5alLDAJCK/jLsU0G8R03PAwt5DjEcFVK3ICRNw5dh2gxwqg7aZ3VtO1ynbZr2zAD///////// /////6IDEwBbAAggAAAA --==i3.9.0oisdboibsd((kncd-- milter-0.8.18/test/samp10000644000160600001450000000323710247123425013716 0ustar stuartbmsReturn-Path: Received: from foobar.com (localhost [127.0.0.1]) by hemholt.foobar.com (8.9.3/8.8.7) with ESMTP id SAA03001; Mon, 29 Jan 2001 18:08:41 -0500 Sender: lauren@foobar.com Message-ID: <3A75F7F6.CBF9E75@foobar.com> Date: Mon, 29 Jan 2001 18:08:39 -0500 From: Lauren Hemholz Organization: Hemholtz Family X-Mailer: Mozilla 4.76 [en] (X11; U; Linux 2.2.16-3 i586) X-Accept-Language: en MIME-Version: 1.0 To: Jriser13@aol.com Subject: Re: P.B.S kids References: Content-Type: multipart/alternative; boundary="------------7EC2082FC4F651D73FCD6FE1" Status: O --------------7EC2082FC4F651D73FCD6FE1 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit Dear Agent 1 I hope you can read this. Whenever you write label it P.B.S kids. Eliza doesn't know a thing about P.B.S kids. got to go by agent one. --------------7EC2082FC4F651D73FCD6FE1 Content-Type: text/html; charset=us-ascii Content-Transfer-Encoding: 7bit Dear Agent 1
I hope you can read this.  Whenever you write label it  P.B.S kids.
   Eliza doesn't know a thing about  P.B.S kids.   got to go by
agent one. --------------7EC2082FC4F651D73FCD6FE1-- milter-0.8.18/test/bounce10000644000160600001450000000504510247123425014230 0ustar stuartbmsReceived: from zuul.kastle.com (root@localhost) by zuul.kastle.com with ESMTP id h7JGdwn27534 for ; Tue, 19 Aug 2003 12:39:58 -0400 (EDT) Received: from kastle.com (netgate.kastle.com [172.17.2.8]) by zuul.kastle.com with ESMTP id h7JGdwV27530 for ; Tue, 19 Aug 2003 12:39:58 -0400 (EDT) Received: by kastle.com with XWall v3.27 ; Tue, 19 Aug 2003 12:45:41 -0400 From: System Administrator To: "amy@koger.bmsi.com" Subject: Non delivery report: 5.9.5 (Blocked attachment) Date: Tue, 19 Aug 2003 12:45:41 -0400 X-Mailer: XWall v3.27 Mime-Version: 1.0 Content-Type: multipart/report; report-type=delivery-status; boundary="_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb" This is a multi part message in MIME format. --_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Your message From: amy@koger.bmsi.com To: lwilliams@kastle.com Subj: Thank you! Sent: 2003-08-19 08:51 has encountered a delivery problem. Reason: Blocked attachment One of the attachment(s) in the message is blocked. For security reasons the message was not or not completely delivered to the recipient. Additional info: The blocked attachment is: thank_you.pif --_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb Content-Type: message/xdelivery-status ; name="delivery-status.txt" Reporting-MTA: dns; kastle.com Received-From-MTA: dns; zuul.kastle.com Arrival-Date: Tue, 19 Aug 2003 12:45:41 -0400 Final-Recipient: rfc822; lwilliams@kastle.com Action: failed Status: 5.9.5 --_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb Content-Type: message/rfc822 Received: from zuul.kastle.com [172.17.2.100] by kastle.com with XWall v3.27 ; Tue, 19 Aug 2003 12:45:41 -0400 Received: from zuul.kastle.com (root@localhost) by zuul.kastle.com with ESMTP id h7JGduo27526 for ; Tue, 19 Aug 2003 12:39:56 -0400 (EDT) Received: from 1333AVE2 (wan-vc8f35e.norva3.biz.mindspring.com [216.135.140.174]) by zuul.kastle.com with ESMTP id h7JGdqS27522 for ; Tue, 19 Aug 2003 12:39:53 -0400 (EDT) Message-Id: <200308191639.h7JGdqS27522@zuul.kastle.com> From: To: Subject: Thank you! Date: Tue, 19 Aug 2003 12:51:38 --0400 X-MailScanner: Found to be clean Importance: Normal X-Mailer: Microsoft Outlook Express 6.00.2600.0000 X-MSMail-Priority: Normal X-Priority: 3 (Normal) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="_NextPart_000_062C48F7" --_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb-- milter-0.8.18/rhsbl.m40000644000160600001450000000304210247124246013343 0ustar stuartbmsdivert(-1) # # Copyright (c) 2002 Derek J. Balling # All rights reserved. # # Permission to use granted for all purposes. If modifications are made # they are requested to be sent to for inclusion in future # versions # # Allows (hopefully) for checking of access.db whitelisting now. This ONLY # works on sendmail-8.12.x ... use on any other version may require tinkering # by you the downloader. # # Incorporates many changes by Sergey S. Mokryshev # # divert(0) ifdef(`_RHSBL_R_',`dnl',`dnl VERSIONID(`$Id: rhsbl.m4,v 1.1.1.1 2005/05/31 18:10:46 customdesigned Exp $') define(`_RHSBL_R_',`') ifdef(`_DNSBL_R_',`dnl',`dnl LOCAL_CONFIG # map for DNS based blacklist lookups based on the sender RHS Kdnsbl host -T')') divert(-1) define(`_RHSBL_SRV_', `_ARG_')dnl define(`_RHSBL_MSG_', `ifelse(len(X`'_ARG2_),`1',`"550 Mail from " $`'&{RHS} " refused by blackhole site '_RHSBL_SRV_`"',`_ARG2_')')dnl define(`_RHSBL_MSG_TMP_', `ifelse(_ARG3_,`t',`"451 Temporary lookup failure of " $`'&{RHS} " at '_RHSBL_SRV_`"',`_ARG3_')')dnl MAILER_DEFINITIONS SLocal_check_mail # DNS based RHS spam list blackholes.bmsi.com R$* $: $>CanonAddr $1 R $*<@$+.> $: $1<@$2.> $| $>SearchList <+ rhs> $| <> R $* $| <$={Accept}> $: OKSOFAR R $*<@$+.> $| $* $: $(dnsbl $2._RHSBL_SRV_. $: OK $) $(macro {RHS} $@ $2 $) R OK $: OKSOFAR R $*<@$*> $: OKSOFAR ifelse(len(X`'_ARG3_),`1', `R$+ $: TMPOK', `R$+ $#error $@ 4.7.1 $: _RHSBL_MSG_TMP_') R$+ $#error $@ 5.7.1 $: _RHSBL_MSG_ milter-0.8.18/errors.py0000755000160600001450000000113011722265767013671 0ustar stuartbms#!/usr/bin/python2.6 import cgi import cgitb; cgitb.enable() import os import re import sys template_re = re.compile(r'\%([A-Za-z0-9_]*)') R = re.compile(r'%+') def output(DATA): def getfield(name): return R.sub(lambda t: '%'*((t.end()-t.start())//2),DATA.getfirst(name,'')) print "Content-type: text/html\n" print "" filename = "/var/www/html/python/errors%s.html" % os.environ["PATH_INFO"] with open(filename,'r') as FILE: print template_re.sub(lambda m: getfield(m.expand(r'\1')), FILE.read()) print "" form = cgi.FieldStorage() output(form) milter-0.8.18/spfmilter.py0000644000160600001450000002710512117767020014355 0ustar stuartbms# A simple SPF milter. # You must install pyspf for this to work. # http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html # Author: Stuart D. Gathman # Copyright 2007 Business Management Systems, Inc. # This code is under GPL. See COPYING for details. import sys import Milter import spf import syslog import anydbm from Milter.config import MilterConfigParser from Milter.utils import iniplist,parse_addr,ip4re syslog.openlog('spfmilter',0,syslog.LOG_MAIL) class Config(object): "Hold configuration options." def __init__(self): self.internal_connect = () self.trusted_relay = () self.trusted_forwarder = () self.access_file = None def read_config(list): "Return new config object." cp = MilterConfigParser() cp.read(list) if cp.has_option('milter','datadir'): os.chdir(cp.get('milter','datadir')) conf = Config() conf.socketname = cp.getdefault('milter','socketname', '/tmp/spfmiltersock') conf.miltername = cp.getdefault('milter','name','pyspffilter') conf.trusted_relay = cp.getlist('milter','trusted_relay') conf.internal_connect = cp.getlist('milter','internal_connect') conf.untrapped_exception = cp.getdefault('milter','untrapped_exception', 'CONTINUE') if cp.has_option('spf','trusted_forwarder'): conf.trusted_forwarder = cp.getlist('spf','trusted_forwarder') else: # backward compatibility with config typo conf.trusted_forwarder = cp.getlist('spf','trusted_relay') conf.access_file = cp.getdefault('spf','access_file',None) return conf class SPFPolicy(object): "Get SPF policy by result from sendmail style access file." def __init__(self,sender,access_file=None): self.sender = sender self.domain = sender.split('@')[-1].lower() if access_file: try: acf = anydbm.open(access_file,'r') except: acf = None else: acf = None self.acf = acf def close(self): if self.acf: self.acf.close() def __enter__(self): return self def __exit__(self,t,v,b): self.close() def getPolicy(self,pfx): acf = self.acf if not acf: return None try: return acf[pfx + self.sender] except KeyError: try: return acf[pfx + self.domain] except KeyError: try: return acf[pfx] except KeyError: return None class spfMilter(Milter.Base): "Milter to check SPF. Each connection gets its own instance." def log(self,*msg): syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg]))) def __init__(self): self.mailfrom = None self.id = Milter.uniqueID() # we don't want config used to change during a connection self.conf = config # addheader can only be called from eom(). This accumulates added headers # which can then be applied by alter_headers() def add_header(self,name,val,idx=-1): self.new_headers.append((name,val,idx)) self.log('%s: %s' % (name,val)) @Milter.noreply def connect(self,hostname,unused,hostaddr): self.internal_connection = False self.trusted_relay = False self.hello_name = None # sometimes people put extra space in sendmail config, so we strip self.receiver = self.getsymval('j').strip() if hostaddr and len(hostaddr) > 0: ipaddr = hostaddr[0] if iniplist(ipaddr,self.conf.internal_connect): self.internal_connection = True if iniplist(ipaddr,self.conf.trusted_relay): self.trusted_relay = True else: ipaddr = '' self.connectip = ipaddr if self.internal_connection: connecttype = 'INTERNAL' else: connecttype = 'EXTERNAL' if self.trusted_relay: connecttype += ' TRUSTED' self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype)) return Milter.CONTINUE def hello(self,hostname): self.hello_name = hostname self.log("hello from %s" % hostname) if not self.internal_connection: # Allow illegal HELO from internal network, some email enabled copier/fax # type devices (Toshiba) have broken firmware. if ip4re.match(hostname): self.log("REJECT: numeric hello name:",hostname) self.setreply('550','5.7.1','hello name cannot be numeric ip') return Milter.REJECT return Milter.CONTINUE # multiple messages can be received on a single connection # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # of each message. def envfrom(self,f,*str): self.log("mail from",f,str) self.new_headers = [] if not self.hello_name: self.log('REJECT: missing HELO') self.setreply('550','5.7.1',"It's polite to say helo first.") return Milter.REJECT self.mailfrom = f t = parse_addr(f) if len(t) == 2: t[1] = t[1].lower() domain = t[1] else: domain = 'localhost.localdomain' self.canon_from = '@'.join(t) # Check SMTP AUTH, also available: # auth_authen authenticated user # auth_author (ESMTP AUTH= param) # auth_ssf (connection security, 0 = unencrypted) # auth_type (authentication method, CRAM-MD5, DIGEST-MD5, PLAIN, etc) # cipher_bits SSL encryption strength # cert_subject SSL cert subject # verify SSL cert verified self.user = self.getsymval('{auth_authen}') if self.user: # Very simple SMTP AUTH policy by default: # any successful authentication is considered INTERNAL # Detailed authorization policy is configured in the access file below. self.internal_connection = True self.log( "SMTP AUTH:",self.user, self.getsymval('{auth_type}'), "sslbits =",self.getsymval('{cipher_bits}'), "ssf =",self.getsymval('{auth_ssf}'), "INTERNAL" ) # Restrict SMTP AUTH users to authorized domains authsend = '@'.join((self.user,domain)) with SPFPolicy(authsend,access_file=self.conf.access_file) as p: policy = p.getPolicy('smtp-auth:') if policy: if policy != 'OK': self.log("REJECT: SMTP user",self.user, "at",self.connectip,"not authorized for domain",domain) self.setreply('550','5.7.1', 'SMTP user %s is not authorized to send from domain %s.' % (self.user,domain) ) return Milter.REJECT self.log("SMTP authorized user",self.user,"sending from domain",domain) if not (self.internal_connection or self.trusted_relay) and self.connectip: return self.check_spf() return Milter.CONTINUE def eom(self): for name,val,idx in self.new_headers: try: self.addheader(name,val,idx) except: self.addheader(name,val) # older sendmail can't insheader return Milter.CONTINUE def check_spf(self): receiver = self.receiver for tf in self.conf.trusted_forwarder: q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False) res,code,txt = q.check() if res == 'pass': self.log("TRUSTED_FORWARDER:",tf) break else: q = spf.query(self.connectip,self.canon_from,self.hello_name, receiver=receiver,strict=False) q.set_default_explanation( 'SPF fail: see http://openspf.net/why.html?sender=%s&ip=%s' % (q.s,q.i)) res,code,txt = q.check() if res not in ('pass','temperror'): if self.mailfrom != '<>': # check hello name via spf unless spf pass h = spf.query(self.connectip,'',self.hello_name,receiver=receiver) hres,hcode,htxt = h.check() with SPFPolicy(self.hello_name,self.conf.access_file) as hp: policy = hp.getPolicy('helo-%s:'%hres) #print 'helo-%s:%s %s'%(hres,self.hello_name,policy) if not policy: if hres in ('deny','fail','neutral','softfail'): policy = 'REJECT' else: policy = 'OK' if policy != 'OK': self.log('REJECT: hello SPF: %s 550 %s' % (hres,htxt)) self.setreply('550','5.7.1',htxt, "The hostname given in your MTA's HELO response is not listed", "as a legitimate MTA in the SPF records for your domain. If you", "get this bounce, the message was not in fact a forgery, and you", "should IMMEDIATELY notify your email administrator of the problem." ) return Milter.REJECT else: hres,hcode,htxt = res,code,txt else: hres = None with SPFPolicy(q.s,self.conf.access_file) as p: if res == 'fail': policy = p.getPolicy('spf-fail:') if not policy or policy == 'REJECT': self.log('REJECT: SPF %s %i %s' % (res,code,txt)) self.setreply(str(code),'5.7.1',txt) # A proper SPF fail error message would read: # forger.biz [1.2.3.4] is not allowed to send mail with the domain # "forged.org" in the sender address. Contact . return Milter.REJECT elif res == 'softfail': policy = p.getPolicy('spf-softfail:') if policy and policy == 'REJECT': self.log('REJECT: SPF %s %i %s' % (res,code,txt)) self.setreply(str(code),'5.7.1',txt) # A proper SPF fail error message would read: # forger.biz [1.2.3.4] is not allowed to send mail with the domain # "forged.org" in the sender address. Contact . return Milter.REJECT elif res == 'permerror': policy = p.getPolicy('spf-permerror:') if not policy or policy == 'REJECT': self.log('REJECT: SPF %s %i %s' % (res,code,txt)) # latest SPF draft recommends 5.5.2 instead of 5.7.1 self.setreply(str(code),'5.5.2',txt, 'There is a fatal syntax error in the SPF record for %s' % q.o, 'We cannot accept mail from %s until this is corrected.' % q.o ) return Milter.REJECT elif res == 'temperror': policy = p.getPolicy('spf-temperror:') if not policy or policy == 'REJECT': self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt)) self.setreply(str(code),'4.3.0',txt) return Milter.TEMPFAIL elif res == 'neutral' or res == 'none': policy = p.getPolicy('spf-neutral:') if policy and policy == 'REJECT': self.log('REJECT NEUTRAL:',q.s) self.setreply('550','5.7.1', "%s requires an SPF PASS to accept mail from %s. [http://openspf.net]" % (receiver,q.s)) return Milter.REJECT elif res == 'pass': policy = p.getPolicy('spf-pass:') if policy and policy == 'REJECT': self.log('REJECT PASS:',q.s) self.setreply('550','5.7.1', "%s has been blacklisted by %s." % (q.s,receiver)) return Milter.REJECT self.add_header('Received-SPF',q.get_header(res,receiver),0) if hres and q.h != q.o: self.add_header('X-Hello-SPF',hres,0) return Milter.CONTINUE if __name__ == "__main__": Milter.factory = spfMilter Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS) global config config = read_config(['spfmilter.cfg','/etc/mail/spfmilter.cfg']) ue = config.untrapped_exception.upper() if ue == 'CONTINUE': Milter.set_exception_policy(Milter.CONTINUE) elif ue == 'REJECT': Milter.set_exception_policy(Milter.REJECT) elif ue == 'TEMPFAIL': Milter.set_exception_policy(Milter.TEMPFAIL) else: print("WARNING: invalid untrapped_exception policy: %s"%ue) miltername = config.miltername socketname = config.socketname print("""To use this with sendmail, add the following to sendmail.cf: O InputMailFilters=%s X%s, S=local:%s See the sendmail README for libmilter. spfmilter startup""" % (miltername,miltername,socketname)) sys.stdout.flush() Milter.runmilter(miltername,socketname,240) print "spfmilter shutdown" milter-0.8.18/MANIFEST.in0000644000160600001450000000051312116721242013521 0ustar stuartbmsinclude COPYING include TODO include CREDITS include ChangeLog include MANIFEST.in include testbms.py include testspf.py include test.py include spfmilter.py include dkim-milter.py include bms.py include ban2zone.py include setup.py include test/* include doc/* include *.spec include *.cfg include *.txt include *.m4 include *.rc milter-0.8.18/quarantine.txt0000644000160600001450000000150210774753602014707 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: DELIVERY STATUS (POSSIBLE SPAM) Auto-Submitted: auto-generated (content analysis) This is an automatically generated Delivery Status Notification. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Received-SPF: %(spf_result)s A statistical analysis of your message has classified it as junk mail, and it has been quarantined. Eventually, the recipients will review their quarantined mail and may notice your message. If your message is important, please contact them via other means. You may also try sending them a simple plain text message. If you need further assistance, please do not hesitate to contact me. Kind regards, postmaster@%(receiver)s milter-0.8.18/ChangeLog0000644000160600001450000002502611456413664013557 0ustar stuartbms# Revision 1.99 2007/03/23 22:39:10 customdesigned # Get SMTP-Auth policy from access_file. # # Revision 1.98 2007/03/21 04:02:13 customdesigned # Properly log From: and Sender: # # Revision 1.97 2007/03/18 02:32:21 customdesigned # Gossip configuration options: client or standalone with optional peers. # # Revision 1.96 2007/03/17 21:22:48 customdesigned # New delayed DSN pattern. Retab (expandtab). # # Revision 1.95 2007/03/03 19:18:57 customdesigned # Fix continuing findsrs when srs.reverse fails. # # Revision 1.94 2007/03/03 18:46:26 customdesigned # Improve delayed failure detection. # # Revision 1.93 2007/02/07 23:21:26 customdesigned # Use re for auto-reply recognition. # # Revision 1.92 2007/01/26 03:47:23 customdesigned # Handle null in header value. # # Revision 1.91 2007/01/25 22:47:25 customdesigned # Persist blacklisting from delayed DSNs. # # Revision 1.90 2007/01/23 19:46:20 customdesigned # Add private relay. # # Revision 1.89 2007/01/22 02:46:01 customdesigned # Convert tabs to spaces. # # Revision 1.88 2007/01/19 23:31:38 customdesigned # Move parse_header to Milter.utils. # Test case for delayed DSN parsing. # Fix plock when source missing or cannot set owner/group. # # Revision 1.87 2007/01/18 16:48:44 customdesigned # Doc update. # Parse From header for delayed failure detection. # Don't check reputation of trusted host. # Track IP reputation only when missing PTR. # # Revision 1.86 2007/01/16 05:17:29 customdesigned # REJECT after data for blacklisted emails - so in case of mistakes, a # legitimate sender will know what happened. # # Revision 1.85 2007/01/11 04:31:26 customdesigned # Negative feedback for bad headers. Purge cache logs on startup. # # Revision 1.84 2007/01/10 04:44:25 customdesigned # Documentation updates. # # Revision 1.83 2007/01/08 23:20:54 customdesigned # Get user feedback. # # Revision 1.82 2007/01/06 04:21:30 customdesigned # Add config file to spfmilter # # Revision 1.81 2007/01/05 23:33:55 customdesigned # Make blacklist an AddrCache # # Revision 1.80 2007/01/05 23:12:12 customdesigned # Move parse_addr, iniplist, ip4re to Milter.utils # # Revision 1.79 2007/01/05 21:25:40 customdesigned # Move AddrCache to Milter package. # # Revision 1.78 2007/01/04 18:01:10 customdesigned # Do plain CBV when template missing. # # Revision 1.77 2006/12/31 03:07:20 customdesigned # Use HELO identity if good when MAILFROM is bad. # # Revision 1.76 2006/12/30 18:58:53 customdesigned # Skip reputation/whitelist/blacklist when rejecting on SPF. Add X-Hello-SPF. # # Revision 1.75 2006/12/28 01:54:32 customdesigned # Reject on bad_reputation or blacklist and nodspam. Match valid helo like # PTR for guessed SPF pass. # # Revision 1.74 2006/12/19 00:59:30 customdesigned # Add archive option to wiretap. # # Revision 1.73 2006/12/04 18:47:03 customdesigned # Reject multiple recipients to DSN. # Auto-disable gossip on DB error. # # Revision 1.72 2006/11/22 16:31:22 customdesigned # SRS domains were missing srs_reject check when SES was active. # # Revision 1.71 2006/11/22 01:03:28 customdesigned # Replace last use of deprecated rfc822 module. # # Revision 1.70 2006/11/21 18:45:49 customdesigned # Update a use of deprecated rfc822. Recognize report-type=delivery-status # Revision 1.69 2006/11/04 22:09:39 customdesigned # Another lame DSN heuristic. Block PTR cache poisoning attack. # # Revision 1.68 2006/10/04 03:46:01 customdesigned # Fix defaults. # # Revision 1.67 2006/10/01 01:44:06 customdesigned # case_sensitive_localpart option, more delayed bounce heuristics, # optional smart_alias section. # # Revision 1.66 2006/07/26 16:42:26 customdesigned # Support CBV timeout # # Revision 1.65 2006/06/21 22:22:00 customdesigned # Handle multi-line headers in delayed dsns. # # Revision 1.64 2006/06/21 21:12:04 customdesigned # More delayed reject token headers. # Don't require HELO pass for CBV. # # Revision 1.63 2006/05/21 03:41:44 customdesigned # Fail dsn # # Revision 1.61 2006/05/17 21:28:07 customdesigned # Create GOSSiP record only when connection will procede to DATA. # # Revision 1.60 2006/05/12 16:14:48 customdesigned # Don't require SPF pass for white/black listing mail from trusted relay. # Support localpart wildcard for white and black lists. # # Revision 1.59 2006/04/06 18:14:17 customdesigned # Check whitelist/blacklist even when not checking SPF (e.g. trusted relay). # # Revision 1.58 2006/03/10 20:52:49 customdesigned # Use re to recognize failure DSNs. # # Revision 1.57 2006/03/07 20:50:54 customdesigned # Use signed Message-ID in delayed reject to blacklist senders # # Revision 1.56 2006/02/24 02:12:54 customdesigned # Properly report hard PermError (lax mode fails also) by always setting # perm_error attribute with PermError exception. Improve reporting of # invalid domain PermError. # # Revision 1.55 2006/02/17 05:04:29 customdesigned # Use SRS sign domain list. # Accept but do not use for training whitelisted senders without SPF pass. # Immediate rejection of unsigned bounces. # # Revision 1.54 2006/02/16 02:16:36 customdesigned # User specific SPF receiver policy. # # Revision 1.53 2006/02/12 04:15:01 customdesigned # Remove spf dependency for iniplist # # Revision 1.52 2006/02/12 02:12:08 customdesigned # Use CIDR notation for internal connect list. # # Revision 1.51 2006/02/12 01:13:58 customdesigned # Don't check rcpt user list when signed MFROM. # # Revision 1.50 2006/02/09 20:39:43 customdesigned # Use CIDR notation for trusted_relay iplist # # Revision 1.49 2006/01/30 23:14:48 customdesigned # put back eom condition # # Revision 1.48 2006/01/12 20:31:24 customdesigned # Accelerate training via whitelist and blacklist. # # Revision 1.47 2005/12/29 04:49:10 customdesigned # Do not auto-whitelist autoreplys # # Revision 1.46 2005/12/28 20:17:29 customdesigned # Expire and renew AddrCache entries # # Revision 1.45 2005/12/23 22:34:46 customdesigned # Put guessed result in separate header. # # Revision 1.44 2005/12/23 21:47:07 customdesigned # Move Received-SPF header to top. # # Revision 1.43 2005/12/09 16:54:01 customdesigned # Select neutral DSN template for best_guess # # Revision 1.42 2005/12/01 22:42:32 customdesigned # improve gossip support. # Initialize srs_domain from srs.srs config property. Should probably # always block unsigned DSN when signing all. # # Revision 1.41 2005/12/01 18:59:25 customdesigned # Fix neutral policy. pobox.com -> openspf.org # # Revision 1.40 2005/11/07 21:22:35 customdesigned # GOSSiP support, local database only. # # Revision 1.39 2005/10/31 00:04:58 customdesigned # Simple implementation of trusted_forwarder list. Inefficient for # more than 1 or 2 entries. # # Revision 1.38 2005/10/28 19:36:54 customdesigned # Don't check internal_domains for trusted_relay. # # Revision 1.37 2005/10/28 09:30:49 customdesigned # Do not send quarantine DSN when sender is DSN. # # Revision 1.36 2005/10/23 16:01:29 customdesigned # Consider MAIL FROM a match for supply_sender when a subdomain of From or Sender # # Revision 1.35 2005/10/20 18:47:27 customdesigned # Configure auto_whitelist senders. # # Revision 1.34 2005/10/19 21:07:49 customdesigned # access.db stores keys in lower case # # Revision 1.33 2005/10/19 19:37:50 customdesigned # Train screener on whitelisted messages. # # Revision 1.32 2005/10/14 16:17:31 customdesigned # Auto whitelist refinements. # # Revision 1.31 2005/10/14 01:14:08 customdesigned # Auto whitelist feature. # # Revision 1.30 2005/10/12 16:36:30 customdesigned # Release 0.8.3 # # Revision 1.29 2005/10/11 22:50:07 customdesigned # Always check HELO except for SPF pass, temperror. # # Revision 1.28 2005/10/10 23:50:20 customdesigned # Use logging module to make logging threadsafe (avoid splitting log lines) # # Revision 1.27 2005/10/10 20:15:33 customdesigned # Configure SPF policy via sendmail access file. # # Revision 1.26 2005/10/07 03:23:40 customdesigned # Banned users option. Experimental feature to supply Sender when # missing and MFROM domain doesn't match From. Log cipher bits for # SMTP AUTH. Sketch access file feature. # # Revision 1.25 2005/09/08 03:55:08 customdesigned # Handle perverse MFROM quoting. # # Revision 1.24 2005/08/18 03:36:54 customdesigned # Don't innoculate with SCREENED mail. # # Revision 1.23 2005/08/17 19:35:27 customdesigned # Send DSN before adding message to quarantine. # # Revision 1.22 2005/08/11 22:17:58 customdesigned # Consider SMTP AUTH connections internal. # # Revision 1.21 2005/08/04 21:21:31 customdesigned # Treat fail like softfail for selected (braindead) domains. # Treat mail according to extended processing results, but # report any PermError that would officially result via DSN. # # Revision 1.20 2005/08/02 18:04:35 customdesigned # Keep screened honeypot mail, but optionally discard honeypot only mail. # # Revision 1.19 2005/07/20 03:30:04 customdesigned # Check pydspam version for honeypot, include latest pyspf changes. # # Revision 1.18 2005/07/17 01:25:44 customdesigned # Log as well as use extended result for best guess. # # Revision 1.17 2005/07/15 20:25:36 customdesigned # Use extended results processing for best_guess. # # Revision 1.16 2005/07/14 03:23:33 customdesigned # Make SES package optional. Initial honeypot support. # # Revision 1.15 2005/07/06 04:05:40 customdesigned # Initial SES integration. # # Revision 1.14 2005/07/02 23:27:31 customdesigned # Don't match hostnames for internal connects. # # Revision 1.13 2005/07/01 16:30:24 customdesigned # Always log trusted Received and Received-SPF headers. # # Revision 1.12 2005/06/20 22:35:35 customdesigned # Setreply for rejectvirus. # # Revision 1.11 2005/06/17 02:07:20 customdesigned # Release 0.8.1 # # Revision 1.10 2005/06/16 18:35:51 customdesigned # Ignore HeaderParseError decoding header # # Revision 1.9 2005/06/14 21:55:29 customdesigned # Check internal_domains for outgoing mail. # # Revision 1.8 2005/06/06 18:24:59 customdesigned # Properly log exceptions from pydspam # # Revision 1.7 2005/06/04 19:41:16 customdesigned # Fix bugs from testing RPM # # Revision 1.6 2005/06/03 04:57:05 customdesigned # Organize config reader by section. Create defang section. # # Revision 1.5 2005/06/02 15:00:17 customdesigned # Configure banned extensions. Scan zipfile option with test case. # # Revision 1.4 2005/06/02 04:18:55 customdesigned # Update copyright notices after reading article on /. # # Revision 1.3 2005/06/02 02:09:00 customdesigned # Record timestamp in send_dsn.log # # Revision 1.2 2005/06/02 01:00:36 customdesigned # Support configurable templates for DSNs. milter-0.8.18/doc/0002755000160600001450000000000012121400433012523 5ustar stuartbmsmilter-0.8.18/doc/requirements.html0000644000160600001450000002267012120724406016151 0ustar stuartbms Requirements
  

Requirements

  • While the miltermodule will work with python 1.5, you probably want to use python 2.0 or better. The python code uses a number of python 2 features. The email support requires python 2.4.
  • Python must be configured with thread support. This is because pymilter uses sendmail's libmilter which requires thread support.
  • You must compile sendmail with libmilter enabled. In versions of sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and is not installed by default. Sendmail 8.12 still does not enable libmilter by default. You must explicitly select the "MILTER" option when compiling.
  • When compiling Python milter against sendmail versions earlier than 8.13, you must set MAX_ML_REPLY to 1 in setup.py. There is no way to tell from the libmilter includes that smfi_setmlreply is not supported.
  • You probably want to use sendmail-8.13, since that supports multi-line SMTP error descriptions and SOCKETMAP. You want SOCKETMAP for use with pysrs.
  • Python milter has been tested against sendmail-8.11 through sendmail-8.13.
  • Python milter must be compiled for the specific version of sendmail it will run with. (Since the result is dynamically loaded, there could conceivably be multiple versions available and selected at startup - but that will have to wait.) This situation may only exist for sendmail versions prior to 8.12. The protocol seems designed for backward compatibility - and 8.12 is the first official milter release.
  • Mea Culpa! After reading the Python Style guide, I realize that my Python code is not up to snuff. Apparently mixed tabs and spaces are anathema to those using Windows editors, where tabs can be expanded using any arbitrary algorithm. Other than that, my intuition matched Guido's pretty well - although I like to indent by 2 rather than 4. I will arrange to have tabs expanded to spaces when exporting new versions. Until then, beware!
  • AIX 4.1.5 Requirements

    To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files for sendmail-8.11.5 or sendmail-8.12.3. If you have not already set it up, I use a dummy RPM package to represent the stuff that comes with AIX. You might also want my python-2.1.1 spec file for AIX. It does not include Tk or curses modules, sorry. If y'all trust me, you can download rpms for AIX 4.x from my AIX RPM directory.

    Sendmail-8.12 renames libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system shared library. Therefore, I rename libsm.a back to libsmutil.a for AIX. This presents a problem for setup.py.

    RedHat 7.2 Requirements

    If you are running Redhat 7.2, the distributed version of sendmail now enables libmilter by default. RedHat 7.2 bundles the development libraries with the main sendmail package, so there is no sendmail-devel package. However, they forgot to include the headers! So you'll have to get the SRPM and modify it. I suggest moving the static libs to a devel package and adding the headers. If this is too much trouble, you can get the mfapi.h header for sendmail-8.6.11 from here and manually install it as /usr/include/libmilter/mfapi.h.

    If you do modify the SRPM, I suggest renaming libsmutil.a to libsm.a - just like sendmail-8.12 will. If you manually install mfapi.h or don't rename libsmutil.a, you'll need to force libs = ["milter", "smutil"] in setup.py.

    If you have installed python2, and want python-milter to use python2, add python=python2 to setup.cfg and build with python2 setup.py bdist_rpm.

    Redhat 6.2 Requirements

    If you are running Redhat 6.2, the distributed version of sendmail does not enable libmilter. You can download the Redhat 7.2 sendmail.spec modified to compile on RedHat 6.2: sendmail-rhmilter.spec. The SRPM for sendmail-8.11.6 is available from Redhat under Errata for RH6.2. But that doesn't include the latest security patches since RH6.2 is no longer supported.

    If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2 from my linux downloads directory. The lastest RPMs were built by taking a RH7.2 SRPMS and removing some RPM features from the spec file that RH6.2 doesn't support, then recompiling on RH6.2. You can check this by installing the RH7.2 SRPM, then diffing my sendmail.spec with theirs. Then run "rpm -bb sendmail-rhmilter.spec" when you are satisfied.

    If you have installed python2, and want python-milter to use python2, add python=python2 to setup.cfg and build with python2 setup.py bdist_rpm. You'll need to install the sendmail-devel package to compile milter.

    milter-0.8.18/doc/changes.ht0000644000160600001450000003324612006600003014473 0ustar stuartbmsTitle: Recent Changes

    Recent Changes for the pymilter project

    0.9.7

    Raise RuntimeError when result != CONTINUE for @noreply and @nocallback decorators. Remove redundant table in miltermodule (low level change).

    0.9.6

    Raise ValueError on unescaped '%' passed to setreply (setreply arg is ultimately passed to printf by libmilter, usually resulting in a coredump if it contains % escapes).

    0.9.5

    Print milter.error for invalid callback return type. (Since stacktrace is empty, the TypeError exception is confusing.) Fix milter-template.py. It is not in the test suite and stopped working at some point - not good for example code.

    0.9.4

    Handle IP6 in Milter.utils.iniplist(). Support (and require for RPM packages) python-2.6.

    0.9.3

    Handle source routes in Milter.util.parse_addr(). Fix unitialized optional arg in chgfrom(). Disable negotiate callback when libmilter < 8.14.3 (runtime API version 1.0.1)

    0.9.2

    Change result of @noreply callbacks to NOREPLY when so negotiated. Cache callback negotiation. Add new callback support: data,negotiate,unknown. Auto-negotiate protocol steps. Fix missing address of optional param to addrcpt().

    0.9.0

    Spec file change for Fedora: stop using INSTALLED_FILES to make Fedora happy, remove config flag from start.sh glue, own /var/log/milter, use _localstatedir.

    pymilter-0.9.0 is the first version after separating milter and pymilter. This will allow easier reuse by other projects using pymilter to wrap libmilter. In addition, we now support chgfrom and addrcpt_par in the milter API. NS records are now supported by Milter.dns. I suspect that it might be useful to track reputation by nameserver to fight throwaway domains.

    Recent Changes for the milter project

    0.8.15

    Support (and require for RPM packages) Python2.6.

    0.8.14

    Ignore zero length keywords (from_words, porn_words) - a disastrous typo. Ban generic domains for common subdomains. Allow illegal HELO from internal network for braindead copiers. Don't ban for multiple anonymous MFROM. Trust localhost not to be a zombie - sendmail sends from queue on localhost. Ban domains on best_guess pass.

    0.8.13

    Default internal_policy to off. Experimental banned domain list. Block DSN from internal connections, except for listed internal MTAs. BAN policy in access file bans connect IP. Use DATA callback to improve SRS check.

    0.8.12

    Use the pid file in the initscript. Fix bugs with greylisting config and adjust demerits for HELO fail. Add an SPF Pass policy. Can be used to ban a domain.

    0.8.11

    Greylisting is now supported. Messages from the 'vacation' program are now recognized as autoreplies. IPs of trusted relays (secondary MXes, for instance) are never banned. Added ban2zone.py to convert banned IP lists to BIND zonefile data.

    0.8.10

    SRS rejections now log the recipient. I have finally implemented plain CBV (no DSN). The CBV policy will do a plain CBV from now on, and the DSN policy is required if you want to send a DSN. I started checking the MAIL FROM fullname (human readable part of an email) for porn keywords. There is now a banned IP database. IPs are banned for too many bad MAIL FROMs or RCPT TOs, and remain banned for 7 days.

    0.8.9

    I use the %ifarch hack to build milter and milter-spf packages as noarch, while pymilter is built as native. I removed the spf dependency from dsn.py, so pymilter can be used without installing pyspf, and added a Milter.dns module to let python milters do general DNS lookups without loading pyspf.

    0.8.8

    Programs do not belong in the /var/log directory. I moved the milter apps to /usr/lib/pymilter. Since having the programs and data in the same directory is convenient for debugging, it will still use an executable present in the datadir. Several general utility classes and functions are now in the Milter package for possible use by other python milters. In addition to the trivial example milter, a simple SPF only milter is included as a realistic example. The spec file now build 3 RPMs:
    • pymilter is the milter module and Milter package for use by all python milters.
    • milter is the all-singing, all-dancing python milter application, with supporting /etc/init.d, logrotate and other scripts.
    • milter-spf is the simple SPF only milter application.

    0.8.7

    The spf module has been moved to the pyspf package. Download here.

    0.8.6

    Python milter has been moved to pymilter Sourceforge project for development and release downloads.

    0.8.5

    Release 0.8.5 fixes some build bugs reported by Stephen Figgins. It fixes many small things, like not auto-whitelisting recipients of outgoing mail when the subject contains "autoreply:". There is a simple trusted forwarder implementation. If you have more than 2 or so forwarders, we will need a way to "compile" SPF records into an IP set and TTL for it to be efficient (like libspf2 does).

    GOSSiP

    An alpha release of pygossip has been commited to CVS, module pygossip. A version of the bms.py milter has been commited to CVS which supports calling GOSSiP to track domain reputation in a local database.

    New website design

    Hey, I'm no artist, so I just used the ht2html package by Barry Warsaw. The mascot is by Christian Hafner, or maybe his wife. I chose Maxwell's daemon because it tirelessly and invisibly sorts molecules, just as milters sort mail. Christian has also provided a fun simulation that lets you try your hand at sorting molecules.

    0.8.4

    Release 0.8.4 makes configuring SPF policy via access.db actually work. The honeypot idea is enhanced by auto-whitelisting recipients of email sent from selected domains. Whitelisted messages are then used to train the honeypot. This makes the honeypot screener entirely self training. The smfi_progress() API is now automatically supported when present. An optional idx parameter to milter.addheader() invokes smfi_insheader().

    0.8.3

    Release 0.8.3 uses the standard logging module, and supports configuring more detailed SPF policy via the sendmail access map. SMTP AUTH connections are considered INTERNAL. Preventing forgery between internal domains is just a matter of specifying the user-domain map - I'll define something for the next version. We now send DSNs when mail is quarantined (rejecting if DSN fails) and for SPF syntax errors (PermError). There is an experimental option to add a Sender header when it is missing and the From domain doesn't match the MAIL FROM domain. Next release, we may start renaming and replacing an existing Sender header when neither it nor the From domain matches MAIL FROM. Since bogus MAIL FROMs are rejected (to varying degrees depending on the configured SPF policy), and both Sender and From and displayed by default in many email clients, this provides some phishing protection without rejecting mail based on headers.

    0.8.2

    Release 0.8.2 has changes to SPF to bring it in line with the newly official RFC. It adds SES support (the original SES without body hash) for pysrs-0.30.10, and honeypot support for pydspam-1.1.9. There is a new method in the base milter module. milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.

    0.8.0

    Release 0.8.0 is the first Sourceforge release. It supports Python-2.4, and provides an option to accept mail that gets an SPF softfail or fails the 3 strikes rule, provided the alleged sender accepts a DSN explaining the problem. Python-2.3 is no longer supported by the reworked mime.py module, although API changes could be backported. There are too many incompatible changes to the python email package.

    Older Releases

    Release 0.7.2 tightens the authentication screws with a "3 strikes and you're out" policy. A sender must have a valid PTR, HELO, or SPF record to send email. Specific senders can be whitelisted using the "delegate" option in the spf configuration section by adding a default SPF record for them. The PTR and HELO are required by RFC anyway, so this is not an unreasonable requirement. There is now a coherent policy for an SPF softfail result. A softfail is accepted if there is a valid PTR or HELO, or if the domain is listed in the "accept_softfail" option of the spf configuration section. A neutral result is accepted by default if there is a valid PTR or HELO, (and the SPF record was not guessed), unless the domain is listed in the "reject_neutral" option. Common forms of PTR records for dynamic IPs are recognized, and do not count as a valid PTR. This does not prevent anyone from sending mail from a dynamic IP - they just need to configure a valid HELO name or publish an SPF record.

    As SPF adoption continues to rise, forged spam is not getting through. So spammers are publishing their SPF records as predicted. The 0.7.2 RPM now provides the rhsbl sendmail hack so that spammer domains can be blacklisted. With the RPM installed, add a line like the following to your sendmail.mc.

    HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
    

    Of course, spammers are now starting to register throwaway domains. The next thing we need is a custom DNS server, in Python, that can recognize patterns. For instance, one spammer registers ded304.com, ded305.com, ded306.com, etc. We also need the custom DNS server to let SPF classic clients check SES (which will be part of pysrs). The Twisted Python framework provides a custom DNS server - but I would like a smaller implementation for our use.

    The RPM for release 0.7.0 moves the config file and socket locations to /etc/mail and /var/run/milter respectively. We now parse Microsoft CID records - but only hotmail.com uses them. They seem to have applied for a patent on the brilliant idea of examining the mail headers to see who the message is from. We aren't doing that here, so not to worry - but I am not a lawyer, so if you are worried, change spf.py around line 626 to return None instead of calling CIDParser(). There is a new option to reject mail with no PTR and no SPF.

    Microsoft is pushing an anti-opensource license for their pending patent along with their sender-ID proposal before the IETF. It is royalty free - but requires anyone distributing a binary they've compiled from source to sign a license agreement. The Apache Software Foundation explains the problem with sender-ID, and Debian concurs. Since the Microsoft license is incompatible with free software in general and the GPL in particular, Python milter will not be able to implement sender-ID in its current form. This was, no doubt, Microsoft's intent all along.

    Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers. Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I recommend ignoring it and continuing to implement and improve SPF until a working and unencumbered proposal for RFC2822 headers surfaces.

    SPF logo Release 0.6.6 adds support for SPF, a protocol to prevent forging of the envelope from address. SPF support requires pydns. The included spf.py module is an updated version of the original 1.6 version at wayforward.net. The updated version tracks the draft RFC and test suite.

    The FAQ addresses how to get started with SPF.

    Release 0.6.1 adds a full milter based dspam application.

    I have selected the dspam bayes filter project and packaged it for python. Release 0.6.0 offers a simple application of dspam I call "header triage", which rejects messages with spammy headers. To use header triage, you must have DSPAM installed, and select a dictionary that is well moderated by someone who gets lots of spam. That dictionary can be used to block spam that is obvious from the headers (e.g. X-Mailer and Subject) before it ties up any more resources. I have yet to see any false positives from this approach (check the milter log), but if there are, the sender will get a REJECT with the message "Your message looks spammy." milter-0.8.18/doc/logmsgs.ht0000644000160600001450000000674211631757720014565 0ustar stuartbmsTitle: Python Milter Log Documentation

    Milter Log Documentation

    The milter log from the bms.py application has a variety of "tags" in it that indicate what it did.
    DSPAM: honeypot SCREENED
    message was quarantined to the honeypot quarantine
    REJECT: hello SPF: fail 550 access denied
    REJECT: hello SPF: softfail 550 domain in transition
    REJECT: hello SPF: neutral 550 access neither permitted nor denied
    message was rejected because there was an SPF policy for the HELO name, and it did not pass.
    CBV: sender-17-44662668-643@bluepenmagic.com
    we performed a call back verification
    dspam
    dspam identifier was added to the message
    REJECT: spam from self: jsconnor.com
    message was reject because HELO was us (jsconnor.com)
    INNOC: richh
    message was used to update richh's dspam dictionary
    HONEYPOT: pooh@bwicorp.com
    message was sent to a honeypot address (pooh@bwicorp.com), the message was added to the honeypot dspam dictionary as spam
    REJECT: numeric hello name: 63.217.19.146
    message was rejected because helo name was invalid (numeric)
    eom
    message was successfully received
    TEMPFAIL: CBV: 450 No MX servers available
    we tried to do a call back verification but could not look up MX record, we told the sender to try again later
    CBV: info@emailpizzahut.com (cached)
    call back verification was needed, we had already done it recently
    abort after 0 body chars
    sender hung up on us
    REJECT: SPF fail 550 SPF fail: see http://openspf.com/why.html?sender=m.hendersonxk@163.net&ip=213.47.161.100
    message was reject because its sender's spf policy said to
    REJECT: Subject: Cialis - No prescription needed!
    message was rejected because its subject contained a bad expression
    REJECT: zombie PC at 192.168.3.37 sending MAIL FROM seajdr@amritind.com
    message was rejected because the connect ip was internal, but the sender was not. This is usually because a Windows PC is infected with malware.
    X-Guessed-SPF: pass
    When the SPF result is NONE, we guess a result based on the generic SPF policy "v=spf1 a/24 mx/24 ptr".
    DSPAM: tonyc tonyc@example.com
    message was sent to tonyc@example.com and it was identified as spam and placed in the tonyc dspam quarantine
    REJECT: CBV: 550 calvinalstonis@ix.netcom.com...User unknown
    REJECT: CBV: 553 sorry, that domain isn't in my list
    REJECT: CBV: 554 delivery error: dd This user doesn't have an account
    message was rejected because call back verification gave us a fatal error
    Auto-Whitelist: user@example.com
    recipient has been added to auto_whitelist.log because the message was sent from an internal IP and the recipient is not internal.
    WHITELIST user@example.com
    message is whitelisted because sender appears in auto_whitelist.log
    BLACKLIST user@example.com
    message is blacklisted because sender appears in blacklist.log or failed a CBV test.
    TRAINSPAM: honeypot X-Dspam-Score: 0.002278
    message was used to train screener dictionary as spam
    TRAIN: honeypot X-Dspam-Score: 0.980203
    message was used to train screener dictionary as ham

    milter-0.8.18/doc/policy.html0000644000160600001450000004060312120724406014721 0ustar stuartbms Python Milter Mail Policy
      

    Python Milter Mail Policy

    The milter package is a flexible milter built using pymilter that emphasizes authentication. It helps prevent forgery of legitimate mail, and most spam is rejected as forged, or because of poor reputation when not forged.

    These are the policies implemented by the bms.py application in the milter package. The milter and Milter modules in the pymilter package do not implement any policies themselves.

    Classify connection

    When the SMTP client connects, the connection IP address is saved for later verification, and the connection is classified as INTERNAL or EXTERNAL by matching the ip address against the internal_connect configuration. IP addresses with no PTR, and PTR names that look like the kind assigned to dynamic IPs (as determined by a heuristic algorithm) are flagged as DYNAMIC. IPs that match the trusted_relay configuration are flagged as TRUSTED.

    Examples from the log file (not the SMTP error message returned):

    2005Jul29 13:56:53 [71207] connect from p50863492.dip0.t-ipconnect.de at ('80.134.52.146', 1858) EXTERNAL DYN
    2005Jul29 18:10:15 [74511] connect from foopub at ('1.2.3.4', 46513) EXTERNAL TRUSTED
    2005Jul29 14:41:00 [71805] connect from foobar at ('192.168.0.1', 41205) INTERNAL
    2005Jul29 14:41:15 [71806] connect from cncln.online.ln.cn at ('218.25.240.137', 35992) EXTERNAL
    

    Certain obviously evil PTR names are blocked at this point: "localhost" (when IP is not 127.*) and ".".

    2005Jul29 14:49:50 [71918] connect from localhost at ('221.132.0.6', 50507) EXTERNAL
    2005Jul29 14:49:50 [71918] REJECT: PTR is localhost
    

    HELO Check

    The HELO name provided by the client is saved for later verification (for example by SPF). We could validate the HELO at this point by verifying that an A record for the HELO name matches the connect ip. However, currently we only block certain obvious problems. HELO names that look like an IP4 address and ones that match the hello_blacklist configuration are immediately rejected. The hello_blacklist typically contains the current MTAs own HELO name or email domains. Clients that attempt to skip HELO are immediately rejected.
    2005Jul29 18:10:15 [74512] hello from example.com
    2005Jul29 18:10:15 [74512] REJECT: spam from self: example.com
    2005Jul29 18:17:09 [74581] hello from 80.191.244.69
    2005Jul29 18:17:09 [74581] REJECT: numeric hello name: 80.191.244.69
    

    MAIL FROM Check

    Before calling our milter, sendmail checks a DNS blacklist to block banned sender domains. We never see a blocked domain.

    The MAIL FROM address is saved for possible use by the smart-alias feature. First, the internal_domains is used for a simple screening if defined. If the MAIL FROM for an INTERNAL connection is NOT in internal_domains, then it is rejected (the PC is most likely infected and attempting to send out spam). If the MAIL FROM for an EXTERNAL connection IS in internal_domains, then the message is immediately rejected. This is quick and effective for most small company MTAs. For more complex mail networks, it is too simplistic, and should not be defined. SPF will handle the complex cases.

    wiretap

    The wiretap feature can screen and/or monitor mail to/from certain users. If the MAIL FROM is being wiretapped, the recipients are altered accordingly.

    SPF check

    The MAIL FROM, connect IP, and HELO name are checked against any SPF records published via DNS for the alleged sender (MAIL FROM) to determine the official SPF policy result. The offical SPF result is then logged in the Received-SPF header field, but certain results are subjected to further processing to create an effective result for policy purposes.

    If the official result is 'none', we try to turn it into an effective result of 'pass' or 'fail'. First, we check for a local substitute SPF record under the domain defined in the [spf]delegate configuration. It is often useful to add local SPF records for correspondents that are too clueless to add their own. If there is no local substitute, we use a "best guess" SPF record of "v=spf1 a/24 mx/24 ptr" for MAIL FROM or "v=spf1 a/24 mx/24" for HELO. In addition, a HELO that is a subdomain of MAIL FROM and resolves to the connect IP results in an effective result of 'pass'.

    If there is no local SPF record, and the effective result is still not 'pass', we check for either a valid HELO name or a valid PTR record for the connect IP. A valid HELO or PTR cannot look like a dynamic name as determined by the heuristic in Milter.dynip.

    If HELO has an SPF record, and the result is anything but pass, we reject the connection:

    2005Jul30 19:45:16 [93991] connect from [221.200.41.54] at ('221.200.41.54', 3581) EXTERNAL DYN
    2005Jul30 19:45:18 [93991] hello from adelphia.net
    2005Jul30 19:45:19 [93991] mail from  ()
    2005Jul30 19:45:19 [93991] REJECT: hello SPF: fail 550 access denied
    
    Note that HELO does not have any forwarding issues like MAIL FROM, and so any result other than 'pass' or 'none' should be treated like 'fail'.

    Only if nothing about the SMTP envelope can be validated does the effective result remain 'none. I call this the "3 strikes" rule.

    If the official result is 'permerror' (a syntax error in the sender's policy), we use the 'lax' option in pyspf to try various heuristics to guess what they really meant. For instance, the invalid mechanism "ip:1.2.3.4" is treated as "ip4:1.2.3.4". The result of lax processing is then used as the effective result for policy purposes.

    With an effective SPF result in hand, we consult the sendmail access database to find our receiver policy for the sender.
    REJECT Reject the sender with a 550 5.7.1 SMTP code. The SMTP rejection includes a detailed description of the problem.
    CBV Do a Call Back Validation by connecting to an MX of the sender and checking that using the sender as the RCPT TO is not rejected. We quit the CBV connection before actualling sending a message. If the CBV is rejected, our SMTP connection is rejected with the same error code and message. CBV results are cached.
    DSN Do a Call Back Validation by connecting to an MX of the sender and checking that using the sender as the RCPT TO is not rejected. Unlike a CBV, we continue on to data and send a detailed message explaining the problem. This can be useful for reporting PermError or SoftFail to the sender. Keep in mind that for any result other than 'pass', the sender could be forged, and your DSN could annoy the wrong person. However, a SoftFail result is requesting such feedback for debugging and a PermError result needs to be fixed by the sender ASAP whether forged or not. DSN results are cached so that senders are annoyed only weekly.
    OK Accept the sender. The message may still be rejected via reputation or content filtering.

    SPF policy syntax

    First, the full sender is checked:
    SPF-Fail:abeb@adelphia.net     DSN
    
    This says to accept mail from that adelphia.net user despite the SPF fail, but only after annoying them with a DSN about their ISP's broken policy.

    If there is no match on the full sender, the domain is checked:

    SPF-Neutral:aol.com     REJECT
    
    This says to reject mail from AOL with an SPF result of neutral. This means AOL users can't use their AOL address with another mail service to send us mail. This is good because the other mail service is likely a badly configured greeting card site or a virus.

    Finally, a default policy for the result is checked. While there are program defaults, you should have defaults in the access database for SPF results:

    SPF-Neutral:            CBV
    SPF-Softfail:           DSN
    SPF-PermError:          DSN
    SPF-TempError:          REJECT
    SPF-None:               REJECT
    SPF-Fail:               REJECT
    SPF-Pass:               OK
    

    Reputation

    If the sender has not been rejected by this point, and if a GOSSiP server is configured, we consult GOSSiP for the reputation score of the sender and SPF result. The score is a number from -100 to 100 with a confidence percentage from 0 to 100. A really bad reputation (less than -50 with confidence greater than 3) is rejected. Note that the reputation is tracked independently for each SPF result and sender combination. So aol.com:neutral might have a really bad reputation, while aol.com:pass would be ok. Furthermore, when a sender finally publishes an SPF policy and starts getting SPF pass, their reputation is effectively reset.

    Whitelists and Blacklists

    The administrator can whitelist or blacklist senders and sending domains by appending them to ${datadir}/auto_whitelist.log or ${datadir}/blacklist.log respectively. In addition, recipients of internal senders (except for automatic replies like vacation messages and return receipts) are automatically whitelisted for 60 days, and senders that fail CBV or DSN checks are automatically blacklisted for 30 days. Whitelisted and blacklisted senders are used to automatically train the bayesian content filter before being delivered or rejected, respectively.

    Real Soon Now users will be able to maintain their own whitelist and blacklist that applies only when they are the recipient.

    Recipient Check

    When the pysrs package is installed and configured, outgoing mail is "signed" by adding a cryto-cookie to MAIL FROM. All DSNs (null MAIL FROM) must be sent to a MAIL FROM address only, so a DSN without a validated cookie in RCPT is immediately rejected. Forwarded domains can have a list of valid recipients configured, and invalid recipients are rejected. The MTA rejects invalid local RCPTs. Four or more invalid RCPTs cause the IP to be blacklisted.

    Content Filter

    Most messages have been rejected or delivered by now, but spammers are always finding new places to send their junk from. For instance, we get around 10000 emails a day, of which around 500 are first time spam senders. A bayesian filter is trained by the whitelists and blacklists, and scores the message. What is likely spam is either rejected or quarantined. If the sender is an effective SPF pass, then they get a DSN notifying them that their message has been quarantined. (A DSN failure gets the sender auto blacklisted.) Else, if the reject_spam option is set, the message is rejected. Otherwise, a CBV is done (failure gets the sender auto blacklisted) and the message is silently quarantined.

    Normally, you don't want email messages to silently disappear into a black hole, so you should set the reject_spam option. However, if you don't want your correspondent's email to get rejected, you can check your quarantine frequently instead.

    Honeypot

    You can also blacklist recipients by listing them as aliases of the 'honeypot' dspam user. These are collectively called the honeypot. Any email to these recipients is used to train the spam filter as spam and chalk up a reputation demerit for the sender, then discarded. It might be a good idea to blacklist the sender if it has SPF pass as well, but I'm afraid of accidents.

    Reputation

    Reputation is tracked by sending domain and effective SPF result. The GOSSiP server tracks the spam/ham status of the last 1024 messages for each domain:result combination. When the server is queried during the SMTP envelope phase (MAIL FROM), it also queries any configured peers, and the scores are combined. Domains with a history of spam for a given SPF result are rejected at MAIL FROM. The GOSSiP system has a command line utility to reset (delete) a reputation for cases where a sender that was infected with malware is repaired. In addition, the confidence score of a reputation decays with time, so a bad sender will eventually be able to try again without manual intervention.
    milter-0.8.18/doc/license.html0000644000160600001450000007012312120724406015044 0ustar stuartbms GNU Documentation License
      

    GNU Free Documentation License

    Version 1.3, 3 November 2008

    Copyright © 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. <http://fsf.org/>

    Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

    0. PREAMBLE

    The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.

    This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.

    We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

    1. APPLICABILITY AND DEFINITIONS

    This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.

    A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.

    A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.

    The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.

    The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.

    A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".

    Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.

    The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.

    The "publisher" means any person or entity that distributes copies of the Document to the public.

    A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.

    The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.

    2. VERBATIM COPYING

    You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.

    You may also lend copies, under the same conditions stated above, and you may publicly display copies.

    3. COPYING IN QUANTITY

    If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.

    If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.

    If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

    It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

    4. MODIFICATIONS

    You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:

    • A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.
    • B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement.
    • C. State on the Title page the name of the publisher of the Modified Version, as the publisher.
    • D. Preserve all the copyright notices of the Document.
    • E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.
    • F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.
    • G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice.
    • H. Include an unaltered copy of this License.
    • I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.
    • J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.
    • K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.
    • L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.
    • M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version.
    • N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section.
    • O. Preserve any Warranty Disclaimers.

    If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.

    You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties—for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.

    You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.

    The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

    5. COMBINING DOCUMENTS

    You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.

    The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.

    In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements".

    6. COLLECTIONS OF DOCUMENTS

    You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.

    You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

    7. AGGREGATION WITH INDEPENDENT WORKS

    A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.

    If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.

    8. TRANSLATION

    Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.

    If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.

    9. TERMINATION

    You may not copy, modify, sublicense, or distribute the Document except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, or distribute it is void, and will automatically terminate your rights under this License.

    However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.

    Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.

    Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, receipt of a copy of some or all of the same material does not give you any rights to use it.

    10. FUTURE REVISIONS OF THIS LICENSE

    The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.

    Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. If the Document specifies that a proxy can decide which future versions of this License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Document.

    11. RELICENSING

    "Massive Multiauthor Collaboration Site" (or "MMC Site") means any World Wide Web server that publishes copyrightable works and also provides prominent facilities for anybody to edit those works. A public wiki that anybody can edit is an example of such a server. A "Massive Multiauthor Collaboration" (or "MMC") contained in the site means any set of copyrightable works thus published on the MMC site.

    "CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0 license published by Creative Commons Corporation, a not-for-profit corporation with a principal place of business in San Francisco, California, as well as future copyleft versions of that license published by that same organization.

    "Incorporate" means to publish or republish a Document, in whole or in part, as part of another Document.

    An MMC is "eligible for relicensing" if it is licensed under this License, and if all works that were first published under this License somewhere other than this MMC, and subsequently incorporated in whole or in part into the MMC, (1) had no cover texts or invariant sections, and (2) were thus incorporated prior to November 1, 2008.

    The operator of an MMC Site may republish an MMC contained in the site under CC-BY-SA on the same site at any time before August 1, 2009, provided the MMC is eligible for relicensing.

    ADDENDUM: How to use this License for your documents

    To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:

        Copyright (C)  YEAR  YOUR NAME.
        Permission is granted to copy, distribute and/or modify this document
        under the terms of the GNU Free Documentation License, Version 1.3
        or any later version published by the Free Software Foundation;
        with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
        A copy of the license is included in the section entitled "GNU
        Free Documentation License".
    

    If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with … Texts." line with this:

        with the Invariant Sections being LIST THEIR TITLES, with the
        Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
    

    If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation.

    If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.

    milter-0.8.18/doc/requirements.ht0000644000160600001450000001256711631757720015637 0ustar stuartbmsTitle: Requirements

    Requirements

  • While the miltermodule will work with python 1.5, you probably want to use python 2.0 or better. The python code uses a number of python 2 features. The email support requires python 2.4.
  • Python must be configured with thread support. This is because pymilter uses sendmail's libmilter which requires thread support.
  • You must compile sendmail with libmilter enabled. In versions of sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and is not installed by default. Sendmail 8.12 still does not enable libmilter by default. You must explicitly select the "MILTER" option when compiling.
  • When compiling Python milter against sendmail versions earlier than 8.13, you must set MAX_ML_REPLY to 1 in setup.py. There is no way to tell from the libmilter includes that smfi_setmlreply is not supported.
  • You probably want to use sendmail-8.13, since that supports multi-line SMTP error descriptions and SOCKETMAP. You want SOCKETMAP for use with pysrs.
  • Python milter has been tested against sendmail-8.11 through sendmail-8.13.
  • Python milter must be compiled for the specific version of sendmail it will run with. (Since the result is dynamically loaded, there could conceivably be multiple versions available and selected at startup - but that will have to wait.) This situation may only exist for sendmail versions prior to 8.12. The protocol seems designed for backward compatibility - and 8.12 is the first official milter release.
  • Mea Culpa! After reading the Python Style guide, I realize that my Python code is not up to snuff. Apparently mixed tabs and spaces are anathema to those using Windows editors, where tabs can be expanded using any arbitrary algorithm. Other than that, my intuition matched Guido's pretty well - although I like to indent by 2 rather than 4. I will arrange to have tabs expanded to spaces when exporting new versions. Until then, beware!
  • AIX 4.1.5 Requirements

    To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files for sendmail-8.11.5 or sendmail-8.12.3. If you have not already set it up, I use a dummy RPM package to represent the stuff that comes with AIX. You might also want my python-2.1.1 spec file for AIX. It does not include Tk or curses modules, sorry. If y'all trust me, you can download rpms for AIX 4.x from my AIX RPM directory.

    Sendmail-8.12 renames libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system shared library. Therefore, I rename libsm.a back to libsmutil.a for AIX. This presents a problem for setup.py.

    RedHat 7.2 Requirements

    If you are running Redhat 7.2, the distributed version of sendmail now enables libmilter by default. RedHat 7.2 bundles the development libraries with the main sendmail package, so there is no sendmail-devel package. However, they forgot to include the headers! So you'll have to get the SRPM and modify it. I suggest moving the static libs to a devel package and adding the headers. If this is too much trouble, you can get the mfapi.h header for sendmail-8.6.11 from here and manually install it as /usr/include/libmilter/mfapi.h.

    If you do modify the SRPM, I suggest renaming libsmutil.a to libsm.a - just like sendmail-8.12 will. If you manually install mfapi.h or don't rename libsmutil.a, you'll need to force libs = ["milter", "smutil"] in setup.py.

    If you have installed python2, and want python-milter to use python2, add python=python2 to setup.cfg and build with python2 setup.py bdist_rpm.

    Redhat 6.2 Requirements

    If you are running Redhat 6.2, the distributed version of sendmail does not enable libmilter. You can download the Redhat 7.2 sendmail.spec modified to compile on RedHat 6.2: sendmail-rhmilter.spec. The SRPM for sendmail-8.11.6 is available from Redhat under Errata for RH6.2. But that doesn't include the latest security patches since RH6.2 is no longer supported.

    If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2 from my linux downloads directory. The lastest RPMs were built by taking a RH7.2 SRPMS and removing some RPM features from the spec file that RH6.2 doesn't support, then recompiling on RH6.2. You can check this by installing the RH7.2 SRPM, then diffing my sendmail.spec with theirs. Then run "rpm -bb sendmail-rhmilter.spec" when you are satisfied.

    If you have installed python2, and want python-milter to use python2, add python=python2 to setup.cfg and build with python2 setup.py bdist_rpm. You'll need to install the sendmail-devel package to compile milter. milter-0.8.18/doc/credits.ht0000644000160600001450000000467711631757720014554 0ustar stuartbmsTitle: Credits

    CREDITS

    Jim Niemira wrote the original C module and some quick and dirty python to use it. Stuart D. Gathman took that kludge and added threading and context objects to it, wrote a proper OO wrapper (Milter.py) that handles attachments, did lots of testing, packaged it with distutils, and generally transformed it from a quick hack to a real, usable Python extension.

    Other contributors (in random order):

    Christian Hafner
    for the pymilter mascot image of Maxwell's daemon
    Stephen Figgins
    for reporting problems building with sendmail-8.12, and when building milter.so for the first time.
    Dave MacQuigg
    for noticing that smfi_insheader wasn't supported, and creating a template to help first time pymilter users create their own milter.
    Terence Way
    for providing a Python port of SPF
    Scott Kitterman
    for doing lots of testing and debugging of SPF against draft standard, and for putting up a web page that validates SPF records using spf.py
    Alexander Kourakos
    for plugging several memory leaks
    George Graf at Vienna University of Economics and Business Administration
    for handling None passed to setreply and chgheader.
    Deron Meranda
    for IPv6 patches
    Jason Erikson
    for handling NULL hostaddr in connect callback.
    John Draper
    for porting Python milter to OpenBSD, and starting to work on tutorials then pointing out that it would be easier to just write the MTA in Python.
    Eric S. Johansson
    for helpful design discussions while working on camram
    Alex Savguira
    for finding bugs with international headers and suggesting the scan_zip option.
    Business Management Systems
    for hosting the website, and providing paying clients who need milter service so I can work on it as part of my day job.
    If I have left anybody out, send me a reminder: stuart@bmsi.com milter-0.8.18/doc/postfix-logo.jpg0000644000160600001450000000711511246122255015673 0ustar stuartbmsÿØÿàJFIFÿþmCREATOR: XV Version 3.10a Rev: 12/29/94 (jp-extension 5.3.3 + PNG patch 1.2d) Quality = 75, Smoothing = 0 ÿÛC    $.' ",#(7),01444'9=82<.342ÿÛC  2!!22222222222222222222222222222222222222222222222222ÿÀbÌ"ÿÄ ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ $4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?÷ª(¢‘!ENêö;t%˜ MØi\´Îrj¼·ðÅ÷˜ ÇžòW É'’÷W«·áP…)tˆ(þüœ±ü:T¹F™¬uxGMÇÜ-3ûa1»d›sŒí=k [‰ãhâóeWâ0ß.M[‚Þé¥1KpÁÀåU³ŠžbýœMTÕàf’¤ôÜ1W#¹ŽA•`~•%Ò(ùÖdÃ"çõªÙò“ºÔçœ|ʪär&tàƒKXöú‹)XçÀ$d0<ëZ‘Èpj“¹œ Ñ%QL€¢Š3@›‡­-¢ŒZ(¢Š(¢Š(¢Š(¢Šd­±  i\­{v°FI<ö½c;Lór™†pyòÇøÒ\\–‘çÎv‘/«zþË)UüµTüìÇ'ßÖ³léŒ,†¥¼Æ|¯ŸšW<±ô¦­ZÜÇw€À$ ãñÕ¬WVâefŸ2àýÓP×|S\«­Ó¯Í G‡ÿhúT–ßrœš[õZZ0xä`K+úZ·¦Ç=´Í+°š)ÛzÊ<àŽÕn6i-ü§”ˆÛï ~µn *(#''8¦‘’Wæá×j®Ð[Œ±àVsÍtîéä$ wFUÛðX‚†-ÓwJÄ’Òá77Øß¼MœþT6^c%¸–Ñ]ÞÖA 8u`p=Ç¥iXêi"4›£qº7õöúÖ[ÌÈ4·íä‹~×s5¤2)òÜ.`8¥sIBêçV·Ñ7Œýi%¾Dsì9®nÒS5 O&8Øct™ç"­½Ì1°9cuMW1›¥g±zMQ¶nXÜŽ™éPÍ}r²™ãBÃ;‹d/Ö³üøä%"ŽiûàE=I©„ÅtV6ññÕŽj«;˶ Ö6’НÛ<çñ©ÏÙOÜÓånÿ¼—îM“èÄ–ög_-î•Tœí„sH!órÂÞâcݘy¼’."ŽÚØ{.æüéŸhžn ÷2ç²éHvk¢D¿a˜c ve1òðÁjÿwì—'Ôfšm¥P?Ñ®—0O[È.û¡ R[˜=YsN[éT[°ãû³.qM[™àÊ­Äñÿ³ ÍH/æ`w-¬¹ë•Á vot˜ÿ¶Lø& 6#Ø¥R€6Åfª?Ù¦†‰ˆoìÈIÝ| ?v¤¦B?Å&ißÌžUü£ÅÌm!‘î9ý×·§ÆU˜“Ô·SXBæHÈ–°.s€¹?­lé®—2ïvêi£:‹MQ¯E éKZ£+3T\ù_õÐ:Óª:’3[±_¼¼ŠRظnsѦÝKí.B®çÜG#&£P uo"õÜ-Âãž•kÏX_ªËþûÕy§ûFÂÞi•…È,AãšÈëVøŸcWO+%ÓÍ‘ó1øVàé\fŸ{%ÄsL-¼‰­¥òÜ'Ýe®¶ CÄjâÎz‘&¢Š*ÌLÍHaCïÊr `>£r‡Ú sž7 È®ƒQ `À óÒ°žîF—/ok7® ¬¤uÒW[\¥†L³é±1õñšÓO+ÊÞDOP9§î².ZKKˆxÿ–O `[IdH¢ÔdYð>ôt]О]—îÀ»»aÚ GÐuæ¥ÛdF½úgƒìþd§<"¡l{SÙHù¯gséxÍ!]wc’O,î†ÆÞ#ýéãHד†¾ÛíŒ[ÿË;)eÿjyò§}¢drT[[‚0\‘OæìˆÃãs5ÅÐÏRsAg·,÷kîsSE,Ž6›÷`~˜©¥i]A†‘H5슫¨N¼ ÅqÜJ”ñ+?/ek/º|´áspI­¦RA@3MÙ:yêb’üÂÝÐÜ[ÿ¡L„ÿrN”í¶üÿ¡\9?ß—ŠM¶½…ìgÛš\[œþúõ‡`Py‚ü€°·L‡q­‹ÒIæ»!b1„ È '2Éèe–µtàK†òÒ1€¥4DöØÙ)iJZÑŒ)’¦ô"ŸAéM‚Ñœ½Õ¯ß„ýäËÄ}}EG³:BþT3IhÇ1“Ö¶ïí|äÜ¿yy²D¦&im#ýlC·ûCÚ²hꌮŠ^C ›uÁŽmò¹é¸ûw5nÎümsopÒù`GM§¢«\*\[¼ /•™<ÄŒ®} Fˆ¶qÎâq<ò¦ÌÆ0¨½él[WVêuq^!‰\°ŒóS$é'Ý"¹ %¹‰"´¯Ë òm,;ѳeKó hP B#$tI˜Ê𾆕ülè ã 硬)ʳ·›§ÆÀ÷‰°kvõ]áãŸP;ÖÇÙT±k[¤lÿ çñ¤Ê§k9°@~k¸ 7LX@¹‡T‡s‚ˆÒ¦¹¾UµÔ{ÒýÒ-ÛYfŸLÓ¾Ãtóy˜µ™ ‰ýÑSêk̶æ ?"5öÈú™©ÎÐ>}F=#RMH-®Tãì Ÿ]Â.qÉKh}ÞA‘L»È‹ý#i¹'¯ðRŃå†ÎçïKó54lÉó.Ì‚[®yúÕˆ!r¨ƒã–v§©2hzÚܘLlÑ*¨€To‚ÇnØõ§öy l:dqš£5œ¤® \½ƒ÷¨d)"£#õ–PJwn%NÒi8ƒ±ò®¢R8ÚÁ‚š° "Öhþ’df®Ç¼"î7~(³’èfÇ· <ò/¯îºÔè®TÜ=õ!•ò1ä¶6Î=jäRÛ§µ; É™’Ã|ئM´W¬"!÷ŒJ6–É>õvEýÖ@梵¼uÆ9Í X—+¢øéKE¡ÎQE!ÖuÝ–÷Fv¸èkJŒÒh¨ÊÇ/,ù‡Én¹ÆQ¿ÂœÚ&¢Råä}k¡’Ý$`Ö«ÿfÄ•úTr›*§/wok%ÜÞYù²Ã²#`ü½2;Ö•Œ‘¬óO–Ä»çê=«BM9nßB¹¤l™ÎWw®Ú,78ØŽkÉެÇUEF¸}¬D§œíûª~µ¤4¸ÜæA¸žµnXáPÒŸ-ÉçKc=4˜äÁ™Cc¢ãôæÐíH€ú V­ùHö²2…nàñþÑ©SEµ^|°O¿5¥E>T/i"²YCÝ@*eW §ÑEœ› ö¥¢˜†ùkéAOjuX.ÊæÒ"rPf¤ À©(¥`æbm`Ô´S°\(¢ŠQEQEQEQEQEQEQEQEQEQEQEQEQEÿÙmilter-0.8.18/doc/links.h0000644000160600001450000000335212120724406014025 0ustar stuartbms

    Subsections

  • Introduction
  • Changes
  • Requirements
  • Download
  • GPG-KEY
  • FAQ
  • Policies
  • Log Messages
  • Mailing List
  • CREDITS
  • LICENSE
  • SourceForge.net Logo

    Links

  • C API
  • Milter.Org
  • Python.Org
  • Sendmail.Org
  • SPF
  • pymilter
  • pysrs
  • pyspf
  • pydkim
  • pydns
  • pygossip
  • pydspam
  • libdspam

    Translations

  • Belorussian milter-0.8.18/doc/python55.gif0000644000160600001450000000524310327523043014720 0ustar stuartbmsGIF89a/7÷ÿÿÿ÷÷÷ïïïçççÞÞÞÖÖÖÎÎÎÆÆÆ½½½µµµ­­­¥¥¥œœœ”””ŒŒŒ„„„{{{ssskkkcccZZZRRRJJJBBB999111)))!!!ÞÖÖÖÎνµµœ”””ŒŒŒ„„{ssskkZRRbYYƒrrA88)!!(A( Q0 ( 9(!kI9cA1„YB0 ¬rQjQAŒbJÕ“j¤rQÍ‹b{Q9I0 sI1‹Y8zbQÕ›rÍ“jÅ‹bœjJ´zQ”bB¬rI¤jAB)­Œs”sZŒkRݤz¤zYsR9Ý›j¼ƒYÕ“bkJ1ÎŒZƒY8Æ„RcB)½”sÕ¤zÅ“j½‹cœsRݤrÕ›j´ƒY”kJÍ“b¬zQŒcBç¥kÆ‹ZÝ›b¤rI¼ƒQœjB´zI{R1öÝÅA80I8(A0 Q8 öæÕZRJRJBïÍ­Ö´”„kR­‹kœzZݬz”rRµ‹cÕ¤rkR9ŒjJ¬ƒYÍ›jç¬sÅ“b¤zQ„bBcJ1ݤj¼‹YœrJÕ›b{Y9YA(´ƒQ”jBΓZç¤c¬zIsQ18(¤rA¼ƒIkI)›j80 IA8( ç¬kÆ“Z¤zIÖ›Z¤ƒYç¬cœrB´ƒIî¬YsQ)¬›ƒïÕµ”zZ›‹rQA( 91!( ­›1½½µÅż{{sƒƒzkkc991 YY8zƒ‹BJRrzƒ¬´Å 08Q zjb îrîî¼!)½”Öö””œ„„Œkkscck­­½¥¥µYYb››¬““¤„„”BBJ´´Í¬¬Åkk{119))1zz“rr‹!!)ƒƒ¤88ƒB!ù,/7GÿH° A.Ø¢pâ‚€ƒ#D`Î!+H]:1¢9 ø ŠR±b‰(͹c…Ô$RV4aN–,8qt%@É@†ŠJÔÈðÄbC/^Ü4D3§í—¶l×Ìy0·d¬)E”´ºÀs'••®€1ç† 18Ê€¡Â¦ %JJ¬Ì³ˆ£j¨L!óÈ3aÌ=hÁš2r2aq!dÎæÚ¤CdL0SvØð‹†J$Xj(Y¡RÆJ%,j€˜£Á"Òs 0K s2!¡rĆ©+¢¥i$Äœ"*ºŽ)Re‘ )Ç-UÐâTN9ñ£K$/`Àôÿ˜C¦bu‰+ŽêÁÈ¥¹“Í퀂(£)vž‡èwP—“L’F€Œ<‡9wtrKI!°ß@õÑÃS°T3Ë“ 3Ç4ƒ 2ÅCŒ9®´¡†%H<Æ jˆaŽ™c†LAG)V$D&º¨‘C_Œf‰,F¬I_1D#TQž æ„!¡8RÄkæl’‰sTÄ á™óHZHI†fü 8˜“æ‚0‚Ä2ˆÑƒ`=Õ bxqCÜ ’t¶F_‘߃ PQ_Q@ñH˜#]F$©9]"†%^̱|æ|†%{‚¡ƒÿ4€92€ÑŠ(¤”b A l€©àðZ¼aN±PÑX.MòÞA˜sCtà¢@²M`NyøŠ ð¶(°‰Š"J~lÐ9lÓ 7ИÃÁTa˜Áætð(O€jNZXaÎ æ<ÓMØØ;ÕÏ8SQ3ÐF¯IQ„9teˆ1”_—0X æ ¢ &kP‘‰FÂD$"‰9cP Q æÈñº(qŸ"Fðà‹ÁH"7 ‚ˆ a !ÅrÈI›Ü†FHáA’ʆæìârô@Æ›UD'–dâ†j˜ÓŠ2˜ƒC&k`ÿQDxH qHqƒ‰9Ѐ9k€1ˆ9œ`bÆ&y1EVX"å_H„I¨aFež"`„‘wA†?Væ8ò£ˆ|!Fð[Q%Wå‘ÈS1ˆ ‰L†TTO¸˜#D’\ñˆ#D8ÏË9O{„QÃPÑä´À@` %H0ÁÐ~OÐÂ`+Ž(‰þög Ø ./”„ `Ž8! ^h… *²;ࢠ’Á"ÌAÀ ˜C `0HñX įøQ…8üaƒL­ä@C|ÂYÔHEÿ¶ ŠUdyЃ96,ì¢EÃ$N1Š ¤c¬x…§:$ÀŠ: Ê ˆmâ x…À@ RÔ í3Ç\¾@Š4œâEIÁ¤ä¨‚ýUrh£€^±€P„‚Ø> .bÐ$h ƒ!`AfãH È%b˜J†°‡(bƒ%¡CE®Õ©ˆ "°¸$~e ^Ö˜†4¦_˜c8ÂÖ˜€Èê4@s $~`T˜#*õÚFE®Q kP)‡+Šð5!cɤDRÕXFPÍ È¡ aæG0ÂáoŒ¡Â#taÿ†*‚@(O`Ž#aŒd“ž9P •äb0Y%” …6 ±KDA90Ä™€ !N, 3Ç(øà„'Dá™Ê@I0âjPäÌq‰p "¶àÄ^`PL?­¨D+ÔÐ…"¨ ”hCÐÐ sLF0‚!°—º1b–ãA)§$„a.Ð…#Ø@ÜfÈ*q„5ä‹T@Ã’ ‹Šˆ EÂ&vaý‘eH‚_Rôˆ€l”ÃÔ FÜÀEðÃÀ¬à7P)l€„ ¢'†E˜ÁaP¥@h‡ˆ2˜eæ@Äa&ÿXá@ fÙZÐ 9„©K7Ã$‘„5†ÁeHðݨPÕq ¡p°ðsDBŠ(ƒ(±I„aØ áP<Œ „. Õe{Øà‚B ³ÌXè°J –ÐÅ#È0+Ð@`ÐE%Ö`„1ô d˜‚!òÚ¸>P„" †J„!ÅTOaB¡RèÁá:PðAÞç‚ KH§"cˆëªÆ1âæ˜ÀAfSFD‚J„•N%ñ]*8" >H]f1’Á 9PÄŽ …c¡df2<± a’! k@‚)0‰_*¢? X8Âp›ˈA}LÜ *‚½jAH¾L#°ÐS ¡fpÐÀ ·¯ 8XE!‡Ü@ºeÎ!ÿ MACGCon 1Written by GIFConverter 2.3.7 of Jan 29, 1994;milter-0.8.18/doc/policy.ht0000644000160600001450000003045711631757720014411 0ustar stuartbmsTitle: Python Milter Mail Policy

    Python Milter Mail Policy

    The milter package is a flexible milter built using pymilter that emphasizes authentication. It helps prevent forgery of legitimate mail, and most spam is rejected as forged, or because of poor reputation when not forged.

    These are the policies implemented by the bms.py application in the milter package. The milter and Milter modules in the pymilter package do not implement any policies themselves.

    Classify connection

    When the SMTP client connects, the connection IP address is saved for later verification, and the connection is classified as INTERNAL or EXTERNAL by matching the ip address against the internal_connect configuration. IP addresses with no PTR, and PTR names that look like the kind assigned to dynamic IPs (as determined by a heuristic algorithm) are flagged as DYNAMIC. IPs that match the trusted_relay configuration are flagged as TRUSTED.

    Examples from the log file (not the SMTP error message returned):

    2005Jul29 13:56:53 [71207] connect from p50863492.dip0.t-ipconnect.de at ('80.134.52.146', 1858) EXTERNAL DYN
    2005Jul29 18:10:15 [74511] connect from foopub at ('1.2.3.4', 46513) EXTERNAL TRUSTED
    2005Jul29 14:41:00 [71805] connect from foobar at ('192.168.0.1', 41205) INTERNAL
    2005Jul29 14:41:15 [71806] connect from cncln.online.ln.cn at ('218.25.240.137', 35992) EXTERNAL
    

    Certain obviously evil PTR names are blocked at this point: "localhost" (when IP is not 127.*) and ".".

    2005Jul29 14:49:50 [71918] connect from localhost at ('221.132.0.6', 50507) EXTERNAL
    2005Jul29 14:49:50 [71918] REJECT: PTR is localhost
    

    HELO Check

    The HELO name provided by the client is saved for later verification (for example by SPF). We could validate the HELO at this point by verifying that an A record for the HELO name matches the connect ip. However, currently we only block certain obvious problems. HELO names that look like an IP4 address and ones that match the hello_blacklist configuration are immediately rejected. The hello_blacklist typically contains the current MTAs own HELO name or email domains. Clients that attempt to skip HELO are immediately rejected.
    2005Jul29 18:10:15 [74512] hello from example.com
    2005Jul29 18:10:15 [74512] REJECT: spam from self: example.com
    2005Jul29 18:17:09 [74581] hello from 80.191.244.69
    2005Jul29 18:17:09 [74581] REJECT: numeric hello name: 80.191.244.69
    

    MAIL FROM Check

    Before calling our milter, sendmail checks a DNS blacklist to block banned sender domains. We never see a blocked domain.

    The MAIL FROM address is saved for possible use by the smart-alias feature. First, the internal_domains is used for a simple screening if defined. If the MAIL FROM for an INTERNAL connection is NOT in internal_domains, then it is rejected (the PC is most likely infected and attempting to send out spam). If the MAIL FROM for an EXTERNAL connection IS in internal_domains, then the message is immediately rejected. This is quick and effective for most small company MTAs. For more complex mail networks, it is too simplistic, and should not be defined. SPF will handle the complex cases.

    wiretap

    The wiretap feature can screen and/or monitor mail to/from certain users. If the MAIL FROM is being wiretapped, the recipients are altered accordingly.

    SPF check

    The MAIL FROM, connect IP, and HELO name are checked against any SPF records published via DNS for the alleged sender (MAIL FROM) to determine the official SPF policy result. The offical SPF result is then logged in the Received-SPF header field, but certain results are subjected to further processing to create an effective result for policy purposes.

    If the official result is 'none', we try to turn it into an effective result of 'pass' or 'fail'. First, we check for a local substitute SPF record under the domain defined in the [spf]delegate configuration. It is often useful to add local SPF records for correspondents that are too clueless to add their own. If there is no local substitute, we use a "best guess" SPF record of "v=spf1 a/24 mx/24 ptr" for MAIL FROM or "v=spf1 a/24 mx/24" for HELO. In addition, a HELO that is a subdomain of MAIL FROM and resolves to the connect IP results in an effective result of 'pass'.

    If there is no local SPF record, and the effective result is still not 'pass', we check for either a valid HELO name or a valid PTR record for the connect IP. A valid HELO or PTR cannot look like a dynamic name as determined by the heuristic in Milter.dynip.

    If HELO has an SPF record, and the result is anything but pass, we reject the connection:

    2005Jul30 19:45:16 [93991] connect from [221.200.41.54] at ('221.200.41.54', 3581) EXTERNAL DYN
    2005Jul30 19:45:18 [93991] hello from adelphia.net
    2005Jul30 19:45:19 [93991] mail from  ()
    2005Jul30 19:45:19 [93991] REJECT: hello SPF: fail 550 access denied
    
    Note that HELO does not have any forwarding issues like MAIL FROM, and so any result other than 'pass' or 'none' should be treated like 'fail'.

    Only if nothing about the SMTP envelope can be validated does the effective result remain 'none. I call this the "3 strikes" rule.

    If the official result is 'permerror' (a syntax error in the sender's policy), we use the 'lax' option in pyspf to try various heuristics to guess what they really meant. For instance, the invalid mechanism "ip:1.2.3.4" is treated as "ip4:1.2.3.4". The result of lax processing is then used as the effective result for policy purposes.

    With an effective SPF result in hand, we consult the sendmail access database to find our receiver policy for the sender.
    REJECT Reject the sender with a 550 5.7.1 SMTP code. The SMTP rejection includes a detailed description of the problem.
    CBV Do a Call Back Validation by connecting to an MX of the sender and checking that using the sender as the RCPT TO is not rejected. We quit the CBV connection before actualling sending a message. If the CBV is rejected, our SMTP connection is rejected with the same error code and message. CBV results are cached.
    DSN Do a Call Back Validation by connecting to an MX of the sender and checking that using the sender as the RCPT TO is not rejected. Unlike a CBV, we continue on to data and send a detailed message explaining the problem. This can be useful for reporting PermError or SoftFail to the sender. Keep in mind that for any result other than 'pass', the sender could be forged, and your DSN could annoy the wrong person. However, a SoftFail result is requesting such feedback for debugging and a PermError result needs to be fixed by the sender ASAP whether forged or not. DSN results are cached so that senders are annoyed only weekly.
    OK Accept the sender. The message may still be rejected via reputation or content filtering.

    SPF policy syntax

    First, the full sender is checked:
    SPF-Fail:abeb@adelphia.net     DSN
    
    This says to accept mail from that adelphia.net user despite the SPF fail, but only after annoying them with a DSN about their ISP's broken policy.

    If there is no match on the full sender, the domain is checked:

    SPF-Neutral:aol.com     REJECT
    
    This says to reject mail from AOL with an SPF result of neutral. This means AOL users can't use their AOL address with another mail service to send us mail. This is good because the other mail service is likely a badly configured greeting card site or a virus.

    Finally, a default policy for the result is checked. While there are program defaults, you should have defaults in the access database for SPF results:

    SPF-Neutral:            CBV
    SPF-Softfail:           DSN
    SPF-PermError:          DSN
    SPF-TempError:          REJECT
    SPF-None:               REJECT
    SPF-Fail:               REJECT
    SPF-Pass:               OK
    

    Reputation

    If the sender has not been rejected by this point, and if a GOSSiP server is configured, we consult GOSSiP for the reputation score of the sender and SPF result. The score is a number from -100 to 100 with a confidence percentage from 0 to 100. A really bad reputation (less than -50 with confidence greater than 3) is rejected. Note that the reputation is tracked independently for each SPF result and sender combination. So aol.com:neutral might have a really bad reputation, while aol.com:pass would be ok. Furthermore, when a sender finally publishes an SPF policy and starts getting SPF pass, their reputation is effectively reset.

    Whitelists and Blacklists

    The administrator can whitelist or blacklist senders and sending domains by appending them to ${datadir}/auto_whitelist.log or ${datadir}/blacklist.log respectively. In addition, recipients of internal senders (except for automatic replies like vacation messages and return receipts) are automatically whitelisted for 60 days, and senders that fail CBV or DSN checks are automatically blacklisted for 30 days. Whitelisted and blacklisted senders are used to automatically train the bayesian content filter before being delivered or rejected, respectively.

    Real Soon Now users will be able to maintain their own whitelist and blacklist that applies only when they are the recipient.

    Recipient Check

    When the pysrs package is installed and configured, outgoing mail is "signed" by adding a cryto-cookie to MAIL FROM. All DSNs (null MAIL FROM) must be sent to a MAIL FROM address only, so a DSN without a validated cookie in RCPT is immediately rejected. Forwarded domains can have a list of valid recipients configured, and invalid recipients are rejected. The MTA rejects invalid local RCPTs. Four or more invalid RCPTs cause the IP to be blacklisted.

    Content Filter

    Most messages have been rejected or delivered by now, but spammers are always finding new places to send their junk from. For instance, we get around 10000 emails a day, of which around 500 are first time spam senders. A bayesian filter is trained by the whitelists and blacklists, and scores the message. What is likely spam is either rejected or quarantined. If the sender is an effective SPF pass, then they get a DSN notifying them that their message has been quarantined. (A DSN failure gets the sender auto blacklisted.) Else, if the reject_spam option is set, the message is rejected. Otherwise, a CBV is done (failure gets the sender auto blacklisted) and the message is silently quarantined.

    Normally, you don't want email messages to silently disappear into a black hole, so you should set the reject_spam option. However, if you don't want your correspondent's email to get rejected, you can check your quarantine frequently instead.

    Honeypot

    You can also blacklist recipients by listing them as aliases of the 'honeypot' dspam user. These are collectively called the honeypot. Any email to these recipients is used to train the spam filter as spam and chalk up a reputation demerit for the sender, then discarded. It might be a good idea to blacklist the sender if it has SPF pass as well, but I'm afraid of accidents.

    Reputation

    Reputation is tracked by sending domain and effective SPF result. The GOSSiP server tracks the spam/ham status of the last 1024 messages for each domain:result combination. When the server is queried during the SMTP envelope phase (MAIL FROM), it also queries any configured peers, and the scores are combined. Domains with a history of spam for a given SPF result are rejected at MAIL FROM. The GOSSiP system has a command line utility to reset (delete) a reputation for cases where a sender that was infected with malware is repaired. In addition, the confidence score of a reputation decays with time, so a bad sender will eventually be able to try again without manual intervention. milter-0.8.18/doc/changes.html0000644000160600001450000004335612120724406015042 0ustar stuartbms Recent Changes
      

    Recent Changes for the pymilter project

    0.9.7

    Raise RuntimeError when result != CONTINUE for @noreply and @nocallback decorators. Remove redundant table in miltermodule (low level change).

    0.9.6

    Raise ValueError on unescaped '%' passed to setreply (setreply arg is ultimately passed to printf by libmilter, usually resulting in a coredump if it contains % escapes).

    0.9.5

    Print milter.error for invalid callback return type. (Since stacktrace is empty, the TypeError exception is confusing.) Fix milter-template.py. It is not in the test suite and stopped working at some point - not good for example code.

    0.9.4

    Handle IP6 in Milter.utils.iniplist(). Support (and require for RPM packages) python-2.6.

    0.9.3

    Handle source routes in Milter.util.parse_addr(). Fix unitialized optional arg in chgfrom(). Disable negotiate callback when libmilter < 8.14.3 (runtime API version 1.0.1)

    0.9.2

    Change result of @noreply callbacks to NOREPLY when so negotiated. Cache callback negotiation. Add new callback support: data,negotiate,unknown. Auto-negotiate protocol steps. Fix missing address of optional param to addrcpt().

    0.9.0

    Spec file change for Fedora: stop using INSTALLED_FILES to make Fedora happy, remove config flag from start.sh glue, own /var/log/milter, use _localstatedir.

    pymilter-0.9.0 is the first version after separating milter and pymilter. This will allow easier reuse by other projects using pymilter to wrap libmilter. In addition, we now support chgfrom and addrcpt_par in the milter API. NS records are now supported by Milter.dns. I suspect that it might be useful to track reputation by nameserver to fight throwaway domains.

    Recent Changes for the milter project

    0.8.15

    Support (and require for RPM packages) Python2.6.

    0.8.14

    Ignore zero length keywords (from_words, porn_words) - a disastrous typo. Ban generic domains for common subdomains. Allow illegal HELO from internal network for braindead copiers. Don't ban for multiple anonymous MFROM. Trust localhost not to be a zombie - sendmail sends from queue on localhost. Ban domains on best_guess pass.

    0.8.13

    Default internal_policy to off. Experimental banned domain list. Block DSN from internal connections, except for listed internal MTAs. BAN policy in access file bans connect IP. Use DATA callback to improve SRS check.

    0.8.12

    Use the pid file in the initscript. Fix bugs with greylisting config and adjust demerits for HELO fail. Add an SPF Pass policy. Can be used to ban a domain.

    0.8.11

    Greylisting is now supported. Messages from the 'vacation' program are now recognized as autoreplies. IPs of trusted relays (secondary MXes, for instance) are never banned. Added ban2zone.py to convert banned IP lists to BIND zonefile data.

    0.8.10

    SRS rejections now log the recipient. I have finally implemented plain CBV (no DSN). The CBV policy will do a plain CBV from now on, and the DSN policy is required if you want to send a DSN. I started checking the MAIL FROM fullname (human readable part of an email) for porn keywords. There is now a banned IP database. IPs are banned for too many bad MAIL FROMs or RCPT TOs, and remain banned for 7 days.

    0.8.9

    I use the %ifarch hack to build milter and milter-spf packages as noarch, while pymilter is built as native. I removed the spf dependency from dsn.py, so pymilter can be used without installing pyspf, and added a Milter.dns module to let python milters do general DNS lookups without loading pyspf.

    0.8.8

    Programs do not belong in the /var/log directory. I moved the milter apps to /usr/lib/pymilter. Since having the programs and data in the same directory is convenient for debugging, it will still use an executable present in the datadir. Several general utility classes and functions are now in the Milter package for possible use by other python milters. In addition to the trivial example milter, a simple SPF only milter is included as a realistic example. The spec file now build 3 RPMs:
    • pymilter is the milter module and Milter package for use by all python milters.
    • milter is the all-singing, all-dancing python milter application, with supporting /etc/init.d, logrotate and other scripts.
    • milter-spf is the simple SPF only milter application.

    0.8.7

    The spf module has been moved to the pyspf package. Download here.

    0.8.6

    Python milter has been moved to pymilter Sourceforge project for development and release downloads.

    0.8.5

    Release 0.8.5 fixes some build bugs reported by Stephen Figgins. It fixes many small things, like not auto-whitelisting recipients of outgoing mail when the subject contains "autoreply:". There is a simple trusted forwarder implementation. If you have more than 2 or so forwarders, we will need a way to "compile" SPF records into an IP set and TTL for it to be efficient (like libspf2 does).

    GOSSiP

    An alpha release of pygossip has been commited to CVS, module pygossip. A version of the bms.py milter has been commited to CVS which supports calling GOSSiP to track domain reputation in a local database.

    New website design

    Hey, I'm no artist, so I just used the ht2html package by Barry Warsaw. The mascot is by Christian Hafner, or maybe his wife. I chose Maxwell's daemon because it tirelessly and invisibly sorts molecules, just as milters sort mail. Christian has also provided a fun simulation that lets you try your hand at sorting molecules.

    0.8.4

    Release 0.8.4 makes configuring SPF policy via access.db actually work. The honeypot idea is enhanced by auto-whitelisting recipients of email sent from selected domains. Whitelisted messages are then used to train the honeypot. This makes the honeypot screener entirely self training. The smfi_progress() API is now automatically supported when present. An optional idx parameter to milter.addheader() invokes smfi_insheader().

    0.8.3

    Release 0.8.3 uses the standard logging module, and supports configuring more detailed SPF policy via the sendmail access map. SMTP AUTH connections are considered INTERNAL. Preventing forgery between internal domains is just a matter of specifying the user-domain map - I'll define something for the next version. We now send DSNs when mail is quarantined (rejecting if DSN fails) and for SPF syntax errors (PermError). There is an experimental option to add a Sender header when it is missing and the From domain doesn't match the MAIL FROM domain. Next release, we may start renaming and replacing an existing Sender header when neither it nor the From domain matches MAIL FROM. Since bogus MAIL FROMs are rejected (to varying degrees depending on the configured SPF policy), and both Sender and From and displayed by default in many email clients, this provides some phishing protection without rejecting mail based on headers.

    0.8.2

    Release 0.8.2 has changes to SPF to bring it in line with the newly official RFC. It adds SES support (the original SES without body hash) for pysrs-0.30.10, and honeypot support for pydspam-1.1.9. There is a new method in the base milter module. milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.

    0.8.0

    Release 0.8.0 is the first Sourceforge release. It supports Python-2.4, and provides an option to accept mail that gets an SPF softfail or fails the 3 strikes rule, provided the alleged sender accepts a DSN explaining the problem. Python-2.3 is no longer supported by the reworked mime.py module, although API changes could be backported. There are too many incompatible changes to the python email package.

    Older Releases

    Release 0.7.2 tightens the authentication screws with a "3 strikes and you're out" policy. A sender must have a valid PTR, HELO, or SPF record to send email. Specific senders can be whitelisted using the "delegate" option in the spf configuration section by adding a default SPF record for them. The PTR and HELO are required by RFC anyway, so this is not an unreasonable requirement. There is now a coherent policy for an SPF softfail result. A softfail is accepted if there is a valid PTR or HELO, or if the domain is listed in the "accept_softfail" option of the spf configuration section. A neutral result is accepted by default if there is a valid PTR or HELO, (and the SPF record was not guessed), unless the domain is listed in the "reject_neutral" option. Common forms of PTR records for dynamic IPs are recognized, and do not count as a valid PTR. This does not prevent anyone from sending mail from a dynamic IP - they just need to configure a valid HELO name or publish an SPF record.

    As SPF adoption continues to rise, forged spam is not getting through. So spammers are publishing their SPF records as predicted. The 0.7.2 RPM now provides the rhsbl sendmail hack so that spammer domains can be blacklisted. With the RPM installed, add a line like the following to your sendmail.mc.

    HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
    

    Of course, spammers are now starting to register throwaway domains. The next thing we need is a custom DNS server, in Python, that can recognize patterns. For instance, one spammer registers ded304.com, ded305.com, ded306.com, etc. We also need the custom DNS server to let SPF classic clients check SES (which will be part of pysrs). The Twisted Python framework provides a custom DNS server - but I would like a smaller implementation for our use.

    The RPM for release 0.7.0 moves the config file and socket locations to /etc/mail and /var/run/milter respectively. We now parse Microsoft CID records - but only hotmail.com uses them. They seem to have applied for a patent on the brilliant idea of examining the mail headers to see who the message is from. We aren't doing that here, so not to worry - but I am not a lawyer, so if you are worried, change spf.py around line 626 to return None instead of calling CIDParser(). There is a new option to reject mail with no PTR and no SPF.

    Microsoft is pushing an anti-opensource license for their pending patent along with their sender-ID proposal before the IETF. It is royalty free - but requires anyone distributing a binary they've compiled from source to sign a license agreement. The Apache Software Foundation explains the problem with sender-ID, and Debian concurs. Since the Microsoft license is incompatible with free software in general and the GPL in particular, Python milter will not be able to implement sender-ID in its current form. This was, no doubt, Microsoft's intent all along.

    Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers. Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I recommend ignoring it and continuing to implement and improve SPF until a working and unencumbered proposal for RFC2822 headers surfaces.

    SPF logo Release 0.6.6 adds support for SPF, a protocol to prevent forging of the envelope from address. SPF support requires pydns. The included spf.py module is an updated version of the original 1.6 version at wayforward.net. The updated version tracks the draft RFC and test suite.

    The FAQ addresses how to get started with SPF.

    Release 0.6.1 adds a full milter based dspam application.

    I have selected the dspam bayes filter project and packaged it for python. Release 0.6.0 offers a simple application of dspam I call "header triage", which rejects messages with spammy headers. To use header triage, you must have DSPAM installed, and select a dictionary that is well moderated by someone who gets lots of spam. That dictionary can be used to block spam that is obvious from the headers (e.g. X-Mailer and Subject) before it ties up any more resources. I have yet to see any false positives from this approach (check the milter log), but if there are, the sender will get a REJECT with the message "Your message looks spammy."

    milter-0.8.18/doc/Maxwells.gif0000644000160600001450000007760210327523043015031 0ustar stuartbmsGIF89a­÷À  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿ!ùÀ,­@ÿ H° Áƒ*\Èa¯†#JœH±¢Å‹‹Ø„1"##ÿÈ¥Sçm;!:ZÜÁ²%K•0!*‘s" yÒÆ…ófîš@"Øö-Ü8oÓΡ”â% P8ÕtàŽiðÖ½S÷.Žª`w„Óömœ6rÝÎí* $³R©é2Æ ”"6V R„I4z6 ŠòaÀ @ ùPh @&ƒµÒˆãåŒ¡ŠŒ¤ôXñ%TA1§è¥ë&N½`«³Â^:q¤Ô”yÅï“R÷î‰öil/_²€±ãæM›5hÐp©sg’2tàÐyËVÖ\ž0Ivmk¡ rÛ¬¡ÿ£¦ÍZµtäÀe“Vmœ8jàÜ•{í­\¸rÚ1ŠÕÇØœB»ÄÉA™”Ï:瀳Î=11ˆ©5eFÛÀÃL¬£ Ȥ£ÍÀ,BC1ù¤³Í!R AÓ@1Ç5çØsO<ã4³ /Ñ Ã7êp£;DÐs8â\c9õ3Î7âì4Ž:àà :"¥õ‚æ˜ã7FCZ6â ¸E¸ÈF=ùd€3ÿ„AÓ/*a¡PJ!ñ|ãÍ7ßLñm”áÏ}ØrB µ˜³Í4Ùx£M7“†cQàEW8átƒ?YRÊÖÜ“Î8¢µ£ª;èˆÏ6ä˜ÿ3N:ðȳŽ:?Îcë:ìÈ#Î;é°£N:í°ÃÎ<ê ±=ïðSŠ]"RÔ B(±Ð—´ÀB õ°³>à°Óå|蔳ΓçˆôM7æÔ MKÔÓÏ­˜áŽÚ°šž7éœ#Î(òO7üCÏ9æ°ÚÎ:à€sŽ;ç”SŽ:ß\S8æ¤3:"‰ó$ÿ Q 0f<àN:[äáŠA` QmDW¨¡Ð™ñC>ݤÆ7ê †;ØlÊ$xÚx#N7åÔ§“¼CN5`L±Æ8QÐC-™¼M6ä0y±¬“ž£N4ãS÷€“=QDÏ9ëxãiã¤uM“ø€ÿ!9ë„“N8è„Cä<~ŒCŒä¸Ì8lq '9²ÇË3¤È ý `Üs:vÄ“N>!ð£NÇàxSxÓÛl£Ž,— AŽ5L–¡‚:åìÁ†9ÜŒãN&Ë<9â€ùM6Ùl#ò9†§MN8í¸sÊ%[ESGö ‘:ë¸CÎ9ô¨Ã0;âè£É ò´Cl<ó˜³:ôTñOÙ01sä™[ˆ‚ƒ˜Ãê0Ä/ÄQhüÁå#R8ÄÁ$qxcoø…"êQn´ÃC°Á<Æ9 Bç˜<à°$¡ÅOK‡;v µ€£ä`G#Äð….¸áΈÿÌ0‡Y´ßø Òð­ÐCì <èQÅCÆŸ:´¶3¸Bãƒ@†° % c1 Dò`Ô\¨GÏÒÁ†v”C[àÆQ1ÅJIëC¸ñà¹Ã[è7ÌŽsÜò°8Ôq|Pc-°Â/j 9ªCøEÄH© @bþø>þ¨âå€Ç?tdTXÉ#Ñ…|˜ãÛF7˜A‡Y*G2ÓáeࡊA˜à‹­!ŽHè¬QÔÂXx C;æðŽÖGÜh‡¨ñ‡vXƒâ±Â²0pêÖÀôáÿQÀ£(Ýð’>ÃqYå@åZ?ÒŠ{¬™éˆÎ=¸ÁzУº(G3j wlƒ*0ƒV˜€là ‡í@>î`/øƒJà›X161†\ ±Â,˜:¥=Ð:,Ì~`Çì1Aqp*gP‡àŽB¦ƒ`àÀ2šæžp ö¢`Y´aÁpL/K‡9Øá{üÀœÐ–°DÈ"xfÁŽ‘Ô#+ÓF#áÁ ¬b÷À‡"@y€ÏOäЇÀ±!¥Åóȃ-*5ðô Y¨Â52‡¨ArØÆïì™KÍÁí´ç:HA@ÿÌ!ø—´m\RÑG”ÆŽmΞڠ"<êqŸ]BÇ“Š»jtÊÙˆ<ìÁ 0àUG>X€Œ6ˆ/_F°B=þ%Žy´À\È;<8#ÿ˜€Ñ2ä Ç Ä>À  D¤cß0=ÇBvàÆ,0ŒýX6® ŒaÛð‘1>¸NRÖÐ 6°1 iƒ„N`Ã÷¨7&ìÃÜ(‡8îÁŠ\ Ø@*`‚cHó8!>w8É؃)NQtHö`ƒ;±†|Á¿ €€LŒF¥`Gìi\e€ÿçH‚:` `àà€$` Ô¡x,ž°ƒ*\˜YbWpP#ëh4:  €Q .À¨PÅ ` `à6 é€á…@ ô0‡:ª  <¸AàÀ0]g P ×À€4@\`E ñÅhUxÀÐ@6° Tàk˜‡,Á@у €&j€€ÀÀ!  3;@Å/hQ…uHc lèòÖnh Vi¨Á–ðE4à 48À1 clãúxE!Ø €ë_*¦„ä¸Â:„ X»Îÿðu.•ÿÚ?vùêŒ_ ›œó4ç xæà@4@td \Ð6ªÁm ãè õAh€"P:Ž`q8@mÿ`„ Xà[Ä£_/˜Ç6´ñŽ-äà_`EdLâ(@ 3  ÄȇÀ/h¨H ±L`å¾RŽaÛ¹€€Ð%´a*ð € d €ÀÌýCÙñiLà X ØÀ° io CÞ@:ÞaTX݇&q*hy(Âþ@‰&èálAXLè" @Àÿ ÜÑAg°b£8À!Æ À€ gD-¶ÁŽvÐcé(Ç>„)ìÀ!0 á 0wP%¦`°ðc&y0yÐ`R£ Û°F *ð† $±P{ÔÆ  ôðvÏ  Î`аÿð  Òq K÷M ° ÎP¾àz¹ s€J”  ]f%ép*ß;ÐC,£S1#AB5 ‹€< 0 ° . §bà`‚° 7`5°Û 0 ðîÐ:3Ý7×ÿ ã0J Pyphptp `g`MQ@mB&lŒ°€]ÐAÆ€mO·sø`]æ@PÔ@ áÃ*æ€ 8ñrÝàPõ è€ >0 D0 šÐeP ²@ž—À p px@ •À ¦@† £ ™½p¾` „€ ˆp v@>@MäÂ5µi†ð ¡  Ÿ …x3€  ئ ƒp šú°‚€Ö–Pg¿ÐÀ° 5Á€{À&p¹Ç‡¦@8¦ß`ê Þp ÿð™ Œ ¡”`¢ð^•À •P ˜p ¨ {ЦàŒÐ´° yð³ 0 ®àn £à=  ­°Y ˜@¢  X Ê`˜ € ®PP ³¯`§tPZ*p ó|@öò  °¦ðm` ú“ Õð\ êà ´Àt_Ó Šwsp²Â`²7†Ð@m.)d³ÉtÀxÓ¡ ´¼€² ÚÀ¥ ¤p‡ vŒÀZ ’ ¬€°* -P} €­Ð – pÿp ŠðÇàZp ˆP ¶ ¬0õ51`k€ _@–°¸p i†°’`²@ Ÿ€vP  ‡€_5$@ÿ ܧ v0 «@Þð Å Ç *` PpP XÀ¶ÀCÕVjG A! À g tdBF(@ É7°à ƒ„i  0 ¡0 ·ðZ ¼à ¡ ’P µ@§0Œ `  p@ —Ð ©P‚Дcp ÿóà ó€Ð Æðd€ Ù R€À •€ •º€ ¡@½Ç` Œÿp XpyP ¨ ¯ ± ¢ š°Uð½ð·€ ßðØð-  ÿ`  á9:l`¤†xƒà’ › @× ¸0ñpçÀ4#h œp¨p¸ u ¾ }ð ‹p ’àqPÍ ‚€ °ð¦à“@—À ½ó£ð î ÿÀ Ãpðž ì ø ý Þ@ç #·€ °t ¦ð™0 x€:aÐ ±Ðv@ w°79°w@.éA§€{ 0 Û0Ø ᣠĠ `{ÿ€–ÿpš °£<Ї`ÒÐrÐ ªp .0 € z@ž dpO kÐ.ÐT€†@ ®ª…`S0ÀÐÖ0zG ÿ)¥*ìprðÀ…`V’°× öPú°Öpr`ç€&ú\QÜ@ Î Òð å` ~°Îð 8$ €9×M„,Æòò ù€Ö`PÝð÷àû€Ûà˜ÀðÚTy‹? ñPí°þpÍ05àJ°²` ›)J ¿k0#ñðÆp¯õG\õÿ ï0?T¢+†è øð#òÀ,À¢î?ïàÂòÆâ-„¿ù"`‘J ú õ@¼ò3,îCèÀVé0]è0À@ ú@ûð^€S׋¹ÅVëî¾ðpëp Ž!0Kp @"ÀNðYpX€#@H°Ÿà_s0ï@Uè *=!¾ðàï`úpŽ¥ ¦0Jp,=€ˆR óà ÷à 8üÅ`Æb<ÆdL¿pÆhœÆ«ù»jÒe` ÊÀ İ v 3PÆx<ýuMX€ ¸)ì  O` ÑÆ–À³ …ÿt×Ä9 c¼Ç (_  q o”V¢‘Bñ0y\]p ë ÔÀ ÑÐ/fªZPÐ×Óð'Û@$ÊУ|6 æ x§€ b Á ÛSQ0zp˜ ‚ kð_ày ›¤àŠ0h MÀ¦P “À^€å€"ƒ c É Ä0 ± t>PÓÀ¯6#@±ðçBó ´ÐË‚P* éð W€D ë€w ‘K`°Pèc1Þ€ î ì°]ü@ j`‚0ò@ Ù Ep ÝÿÀ ÚÀõ¡ŒßLæP$ÆUUÚ  ÔQÝÀ#áP ¥aÕÀ1ï Càw䬰PÿPÔð ­âá ôpm ›À£¬ý3à Ö æ ¹¶€íÐ Nà2PW‡i >C`Õ #GBHývðÑÕàp Ø :`€‚ TUL ]æ  #+Yâ #Lä@1é(@“nûa` c° GPÀ›ŒÇÖ„_0j4p¾à>¾[  ÿà‘p YÀ5MÔÄp*ȃ u*À4óïàcÀ Ç Ö pp ÐK’ÿ`ŽÐ¶9d¦>ó í¿EŒ«¿è;Ä31 È` ÿÿ¢ÐóÅ ?¤ì»e «{ŒUë —à çjà ñ+å@7×J¢%;–Âü@‹ 'Ðõb z $Ö@W²%a£„òðÄñ°  C"Ó ¼£ÀÞ0>;‹ó$ÕÍО°·Ü`¹ûÐݱd@£y›ˆ‘_ Bpø`8w 铌 K7áQâ ÅÕ:Ý {0ù çP bpvªÌ Z²Tè”×PÌÝp%ÝÐ[ªó@¤ÿ`ë×àI “%œB%|ܰ ¬° ïð':’ Mbfðb–0-€2jpÀaÌËAõðçиÐmØ@æ «CbÜ€yàïÐæÀ0@íP Tð0è$}À%’ô ëÐoßPwc7U…é€ß fà ÎÀ#Y`Õ}P ç Ö u$ÂË+ÓpwPA,$é0?ýð ¿0Y€É1ZP wP,°Á}€ÂBÐHÂßiðÁ :–`ÎÃ)ã`e0ÔPi  C z *° š {€K€u° üÀH … dÿe = ÜÐ0Ió0s—c6Ö` ûøÐ?@ïápÿà‰à îÄ#¡§ÿpïëY¡þÝ¥ÀêÀà¡úP|Û›| TE¼ îð72ðiàó8ÝHQ´y@ÖÀ B=gÐtÓó  0ç0*0 \Ý:E!Ví` +`{` Vz0̳#fEÒEñ Iðî1Öp ဠíðPó *Ðé"1¿¤oà‰?±8‰¿€° ‚ðea ¢<kÜ” h@·%ü ïpv çëPÈD¼”° jð äp;éÿð 8çºÀP@ñ ªà!‘ØUEje€¢GLâp¯÷@Ï×Nù ëp+‘NŸ¤9ÿÄpÏ CðÞùÀ ^:tæÊ}Û§Ð|ëº3g®Þ9æâ˜jLåJ•i¤°„SæLš5í´«ç;5Ö -êÆÎܸpçÊ­Á–G]9l㲕PºmÓ¢}×MÝ8zÄäùW/€‚h÷Yh9È5Ð©Ç ;ÿlHÀ‡’pÇ ÀÀ Õà]h×Mw] îöÙ²“>û ø_`º%`šÚs 6È×íRSå›i@eo¼Ùu|h‡œqþÂ&†ûh §ƒ¦AdÕVyG›&GrŠyã+¸™fꨙxÄÑž[à§ Cmd³€ Cn̈E`^xÓåaŒ9à`ã –ãasÖ±1âg`ùG *È Þ|PÅnÕˆ(US7P@Þ®äŸ)4A@ùTIÍ€ LPF8˜± )¼!Kp€„ $ˆÀó¸„ް OÌw`Â>Ú±ŽÿŠ|»…#‘†/¤ NˆBtà ! @PÂæ O¤BÎÆÆÐ†ü€#PÁ8 òC ¡ÿ˜A:À  G8Ô›s¨ãÀXpЋ@q Ø@–þ!y] PßLºàa`ÀÐ-hcØÀ À$ì¡å`‡ ¼Q Ì#â`Ñ!á¸L€Ž€C¦`†6! |¨}¾ÀUGÁp D‚ºxƒ$üpŠK|‚kp†B±‹A`ªÐDLà†OD‚EǔŠˆ„"(ÁDp‡ PA :$‚ 0Å(ñ†K`â ˆˆ„â0X!Šx„+怇Tpb†Pæð‹<þ@Ç8Ò¡w(€kÀCÚ7lɈ%Ò ZøÃÿ(À=h!†_c*pD¨àPØ…;Uƒ „M x#F€ëXÇÈÁa܃ÿ*Æ4 -ì¢nP„'ø ‡:Ô‹ "¶7@ Ì E=º1 z\ƒ= ÀnA KÌ! 3Ñ<¨A‚PE ÁŠ2$BpØmt ` ­ØÃätƒÐ` @`*2qìÁ¦`ÿC7Š[˜a—D"(ч=´aË ÄráSø¡¿(Å'’±>0bq°Åf‘„üÁ¼Ç$¦…v  ߨÇ=^Áuh¡ÌaI! <+¸Ä  ¾Š{‹«`9ðbÃ0F4þ Žt¤B %`EJ±ˆ38‚ÃY¸D$æ° @ÐЗÈ.a \ˆa–À±‡w ƒ’(Ã?œ@ 1D#¡ðFÉ a‡|\†¸„1Ñ N¨Ã ü6¡‰Gx¢¨Ã0&1ŠW`ᕨÂ6Q†@¸âc¨ ÚìŒ}¨h€~â°8 €JÿP0€à`‚õ.X€u …9°àŽ8ðB ¾¸B<˜ñ€TTBœhD”À…=lA20€ x„>°¡ô†þAhàÃÿ@Ä%â s¨Ã?ñÆ=t±Ž4¡úp‡1Hðsh£YˆÅ*ñvP! xø@ƒ-ð  Žx(¡Wô €6p„O\`|‰:jA½Ö=5‹ø‚'à1 j¼Ã´X‡.àA "éÈÆs~P xl#0ÕØF=ÒíRØ]QŠ îøã’G<Ô1x ƒåhÇÍ‘ãV€A{:ÐDÂò·Ÿ‰%@ÿ|øÃë K:ÖAuôœƒòpG;Ø¡v”CÍ·N?$€{Ö\âØ8€ ÂâŸ#ìÈ1¨¡u$ÿïpG7îA tƒÈOG;¾ßޱ ³€d ;ŽóS€ ÔW `†|Èx[ …Pà] …"xa¨‡q øGh‡;Ù†ÀE±€ tˆ‡¸‡øX…}ø‡~°qÀ†sˆ‡Þ‹}`yh‡ósz˜‡zx‡z@º{hw@bP„3(€˜E8†l(€Û£€ÔC‡uØ 73勇ЇvX&„‡ëQv˜‡ã>tð.sÐZx‰ TÃÿ5´;…~è½{¸‡~{ØôlxDƒð186L _è#ÄDTÄEdÄFtÄGDPPð~Àý³"s˜†\8¨—GEõ €-HÄ1H¦ûAÚÅWTŸ€[ÄG„Ú(‘r „AhƒQÜ0"~8‡iȆo0ÆxX”‰°›H ƒ.€‚.0ƒ[cC0„tè†uÀ†p@‡FXeŒ =¨zà†n c´'øÆW,VP . Ë ‚ÿà´q0‡}0p¤ -†¬ð†oo‡ésÄ;™è6ôd0t(‡sHÿ$àÇšuørð†l¸†oHH V(„Cðƒ;èƒ?0„G¨„€Q‚C`‰ Õ ; ö±€[Žå(€€)ÈJL%(†p soH‹L nàuˆ&˜‚,PƒGØ?˜‚8p…D1ƒ$ÀƒkpN¨À‚˜ØF†<à– 61¸B€†;¼ž(êdà„B؃AØQ… È6`VŠs sà‡!`ÊšH Q`ƒOX‡s؇>H •X((ƒ[@ì(ð0qȆ¡ð>mÚ†£üÄ"c€ƒ%†E0ƒ~x(@hІ8 G r(ÿk ‡ÃrȆêÀ ¡‡¼Hwsx„Øf@pÕX„=°C:¨4y¨8X…Å„‰R 8kÀv€‡@(x …* …C(±-¸‚6`?7xvlª4Ðð…tð ^ q(Çn(2 Îlà‹,ü“p‡o˜rPnØÈ{È4ƒOðnøƒ<˜NX h[@v`ŠkðŒ°Ï™˜Îaƒsàlè€2†ˆ‰'pƒqhsH‡zð T¨ƒ/ø‚+è!x€ø€° /‚X‚h@r‡jɉ‡ŠÈ©ièÿÿІD*N¢8ШúÒnHdÂ,àXY`€.Øx…F¨‡3 „\Ø–`‚ð`€Gˆ‡o(Ç(qpEÀ‚t…$( ˜à*h| v(P뮇u‡k‡Wå ,X‡‘PÛ§ø†pÐB£½X/uÿvp´ââÈ…{¨Cð֘Ж½ÈLÔ03P  †zP†;˜SsèxðÍt8È]q W¥:ð@؇~@È€^Œäìr‡sø‡Jà‚,@ÝW‡1%‡s‰„MX|<‡„ÅŠo¸lðPoÐ<°‡D„kð„ˆƒ™ãCˆ‰8°eœS¤ G¸ÛPœ{PƒpH‡eHƒ}8‡.-[o˜* ý†E‡\À(hœy8,èuà‚Uõw0Jõ„=ÐtÀm Šsˆ†3@"}¸ƒøvÀêÌs膊˜Ôt‡ÿõßqX‡tx‡uÈ:ÐZÇ Iu‡gøY(sP•pP™@X¼L(˜†v¨†eRƒt ‡w טÓÝc4eì@Bˆá‹"6 €XØ >ƒw@Ökx‡@0% Ü(ïÚ(ò†|ƆÆØí‡3 íf°ÿ'Pƒ~èZ ‡ë¾.Ɖ᳆;¨„~@‡ç;)´‡4¸ê•ðPV ÀuÄ9(Qk¸Õëyjp‡7ÐBh…5Æä¬†p …Q8ƒˆ&n8@W˜‡´^°‡zà.}؃dåg£,n¸‚ŸÁŠtloˆ õ†1°Oø<`‡s€|X…,à‡~€iHÛlpXvq\(`‚y‡wˆØxx‡vÀðZ8ƒ4°•p„%†'¸‡$XiQ„ƒ•*¨r@}°oˆwh…M€„epbEM·.k‰ {€$(œwˆ‚V˜ƒ)/@QPÿ¦ N‡Wõ•mðu H°8.ø‡È_X†r¨ L‡\Pñ,]„c‡™k}ð.À† bჇy(1¸Ä[õŽÎP‡]à;Ø‚ƒX " †4E=X D?Èc4<]=‚¸–lšß ‡zÀƒix¢å×vèƒ7‚ýŠwÈXrˆl@Rø‡åtX_ÿjøØ²E=h†u8P¥ýs8èØ„9¥b¼‡°ƒzhOSHkHàq‡<€ƒ,ȇYÙÝŽ…Uh8HƒjE–pµLDt§?Y@„{@‡vø„‰y‡I˜‚~°ªBLÿxË8çn8ÍW€j/‚< ø&¸ƒxkå[Õ›á|ЇRX€ „xõW¬øsÈ&hpxv ‡u(‡[å@ÖÀ0À‡Ö†ëwÈXƒ&Á·¦Ýuȇ}K„1—… ±¦» á÷4*•ЂT'SPqH8Ѓt„ÚHV½i„HÀ@Ðr5¸w؆mˆbpÇ–`¡øä¼ÐH£¤‘²‡‰ü˜…°µ~@‡@HqÒs¨ÁÛ}‡hX}˜‡pƒI¨³=xƒàÞîº  kH‡%‡îI‡5ð‚|p™ÿ™ˆ>xDÀ„|8e°u‡=(‰vŽåpøxHƒu(tˆqØt`d†þE†/à édž\amÒ8`* ƒ\ ‡v¸SÀ‚¸y(jhò{yÃЇÈy؇w8à„åˆ0ÿÄé#@0uò@X‡®:sçâ‘êÇZ9tæÊ}cgG“+Z“Î+i²¤¡<—N²léò%Ì—³:½‹×º:Ÿ–ÉiwŽœ¹tâÔ™9ÇÇ6lßÚÍ“ÁØ9lãÆu+WoV“ˆK÷­^¹pâ´ñâÄ “4¥˜eûgÏÛ¼ixšwî9pà¾}óÆmܵ®çÚÿ4M:vÅp VŽ[¹téîa™UžºrçÀÕìÐ?6\bQó4êÔ%wì°ôKŸ~l¤U’× Çoa²ñiwM\·^2 yé`oåô¦ó;‘œp¡ƒLÀsØ€ð*p‚6TÅWÈAxÀ;ašˆ‚?r °„ ÌGVà|À*“]üâ$5\^L¸à€m ZxûÀ?aŒvp¡ÛÀ8î¡\¡Ð@5Ѐ! ûèБ‚0ôâ}RpŒ dáã O7¤²vœA…ø€#¤s¼c!0@`€ Hà¸À ) ItƒÓHNpw´ Ý`äÖB T@¸@2II> h!.³?~rº8‰/|CÕ„¯nÿ€´z8¾@ˆãbpÇ8luŽ •hcØÁB(S@H0 9„!…ÐF!:ЈÔ¡fX‡6²A¤”ã˜À¬ü°‚\ˆb°@2@ @` 0 Œãî0D†°‚†ÜÂ×Ð&ðÉPz@0ϑР&Ì$PøÉ1¼D•«< )@·»Ár~Ä›.À€Y5BH嚀LÂ^(0@>0Œ#L`Â<|à‚FlbÈW3ÐQ„t`#áp‡-”P‰5¨ãT€74€ˆ{ÐC€q(”!ðÂ6¼1†1ÐJÿ$¦ €?z’’ä#(Vp„ ˆ P€(àG?Fà’›Ü«@Q‹š7¼h ØŸE‹!e,:?âÜ8b7Úñ‡J¼cäˆG4¡„c4£ÿ˜°±€}ôCW(Å=Ö‘Žiô€xBgqáœzö8߯!€r8+ €ñ¼ãf¸Aæá/”`‚Pƒ®s…Ÿô@W@€=¸—ë% 5€‹Â„ð€c Aá]€<Üñ;CcIÆ6œÑuÄCNÈÁ%Æ€ ‚5Hƒ*졼 àCNÿ€EÄC€ÇH‘qX#7°Aøp´â8ޱŒ$äõÀÚp 9°!/Àÿ< @À¡{¥@˜À £°E" q¤cãØ’ð@ a$ܾ@ß–(À±p¬°„y0…^0Œ  nŽ}ÀŽpwœƒhƒ>öà ˜ˆ²Q%¸"f8„ȃG4ã'êàÇrñð{0—¸Ñ PìdŽv\+ö|<††…9ðF1æ†s8#ªÃ?âÑ#$£ ©pD707ôáˆ) @P€<È£;|€@ €ÿ0¥àèá°f“СÍ€â+)à¬÷xÃàÑŽ#HÀ‡X B ŒÂˆÛpÇ h=H`u€þa >Lá PE :¢@Ùˆ9Ü¡£sØ)àX‡ÌºQéèë;N zØáëø@?ÌCŽÐ`ÛH:¸åˆ}Ô!Ç(Aäñ€™ÜÐ5–ÓŽm xÃÝ8°(tZ@Ø*°㔫\8Z}¨a@,–à„À^¸+,eHc¸…8†n Þªå|$Ôat@ €9@¸Ù0¼ É€LiÀÝ,$ÀT@øN-V@C/lƒyÄC0ät‚>l€´ A/˜Ú8”CDüCð€?ÀC<ø„@ð#ÜB&,œÂt!€7$ÂÈAÈøA+ŒAˆB"üÁ,ÜøÂ'B,&˜3¸‚ pÁœ0`C„‚$ ‚!ôÁ̬Aè@9”C*àÂ? Á: 5ÄÁƒ(Á™OX@°A@ƒ,¬’d¡O<€Hÿ@APe€d€ Øb6|œ-hC¤'dÂ!ô‡­Â)ÈÁ#(B&Ü&ÈÂDÃ(œB(XB0€B(Â$ÔÂEr‚$HB(ÂÄ­Á’ðüB)ØA'ÈŒB$äÀD0¼‚T‚! øÂ¡}Â'È%´$È=xÇ9h„p@WúÀ:„Æ&C¬ÃôÃ,¤ƒ&4 àXÂ8?ðà L@µ!BÁ?lÞòüЀÐÓõPìL€<`2C3`B=ìƒ`À d‚%ˆäÐB!„"ÐÂÜÁ+dB),*È Ü€¨8AlÂ"lÁd¼&Ì*ÐA*œàA(„°Âl"P‚!Ä-Ì(|BD((‚"ü¤¨ˆ$)(‚ | èÁ€ Ø©"XB'h €A2dC2¨4@ÃüC-dÿƒ)Â=ÌÃ,à 4C¤ÃC;\ Aü‘™±“=(O@ºÛ ÂÙÄÂdlÃ?äCXÃ? € B+ é0$‚(ì-èÂ%dB-`B""ÄÂ(¨h '„Á%€ *ˆ,è L€%ü6„Á6˜=`I2ÀÜ@*hÁ2ˆB)€‚'l‚$ ‰%¸Â'D+ð!„B%,B.t‚ð(ÄÁ&è)ä‚€-7`Ã?Á)ô9B°ƒ9(‚ñ<LÈÀ@‹(AØÁ|qÍ*ÀÐ ÂØ €*Àè‚9Ìœƒ,$‚DÁ+˜8Üÿ+ü*BpB)`B0 Â&Â#4‚&‚'lÂ.Ð*¼Â,¤hB6¤Â; P¥0…Â&T6<,—ÀÎzƒP@|Á;d‚Ô‚&XC4¤hBEª-d‚+øÁ"øHB-¸‚‚1@»^Â?4C6¸`ÀJXÈ+úôNÈdB¼<DËÝÈ’d@×y€8ƒ,`C! Cà=´è€B$‚ˆ,0B)ì ÐÁ" ‚3¼Â¨Â$0‚!ð!è/p3€Á? 8¤ø‚8C.̃)Hƒ1‚50A?XC>¤ÿB#äƒ äC+€C2d‚1€¼Â$4‚˜B0$Â- 'Ô<èA$pB €Â ˜‚#H‚¨@I ˆmÞh”D/Ä‹!0ÀôP~q~ pæáièÁ6TÇMCüƒ<¨?x¬B* èÆÂ-¸€BX#|Á)`€ >ŒÃ< B7ŒB" Â'üƒ-t2ÜÃ;¨À ŽƒÄZ:¸;$<ÈÃ:¬_ïÃ=,Á¤õša) C3dÃ;pÂ=œvBøC&ðÊ€Üÿ#ÁÃ>H†:ÜÃŒÃ?ôƒ=˜2HÂta¿øC=œ;ȃ;¸ÃØá’¹C<ØC=Ä0°À.àƒ<̃=ØÃrÅ2X@j+OA t@x@ LÀ ă>HO£;ÈÖk£ƒ±C;¬C9ŒÃF$Ÿ8t>TÁ:7K˜B˜øƒrÚÃ4B À¤ƒ?àC9ÈÃ?¤Ã´€lƒ>\w7 =ˆÂ8øö>ä‚$@ìÀ$H#8Üz_ˆôÀ ð@>üÃ<ÈÃUwG;Ã;àÅ; Ã>̃0lÂhó…«v>lCüÃÚI½Ã;Ø29xƒ8°š-§ÿC;(H:˜ø:¨w«9|XËV_§œ9ôõä`ÃàÆ¸I”ÌÁ?ìƒ;ÔC>øC°Ùv;Ú=ôC>°üƒ„›ŸÆ)<‹ÀÀ$‚$¸B(ð¿Z?܃9¸B$Ðô@ Ü€ x€´À¬ÌÀ &èB:àClC$t @¨tZK€9Ѓ9´ï‘³š:xõ‘˶v÷ÿõ:ÄZ¢³ƒ;ÐC‚EsñÀ««F¨€ À@ ¬-Üû*1‚ûä5:|5$²Cʶ:ùW#¡?Äwøõîéƒ4üÁ tÀ ŠÀw¼…àÂE¡ä?PÃ*øA<ÁäÀ ðÀ¤Á ‚)(šã?Ôƒ>ÜC9ÜÂÐ!x<Ðý˜ô Ä@H"°BÐ/=Ó7½Ó?=ÔG½ÔO=Õ§¶ ¤¤ Uý½óBÊ+l aïø:܃=¤ø?B7üƒTË×O=¸zC%øƒ8 C<€Ã9ü-@èíÜK=€@Œ€:dF‚”ƒ>p‚ ,3~PÿÖÄ8ÔC7„C10Â<(Xy2Pþ…TyŒo€?H8˜C?p éG½éÇx lÇ}6C3HÐïÀ7€ƒ;ØÇ‘C5°kPý*¨À½Ã‚¼C•ˆC_4},xÀŒÃ;$Å6„ÃU@'<½Ä¡$BŒ pA®—, €Âiœ(¬ƒeeÃQA+D=xÁÈÿÅ6T=P@`6`Aƒ&Th°‘…ŸéáAY {T{'mÜ:qú„T9’dAžày£F-8uJÆ$9 æÂ;&¬’)’Æ9v嬱ƒ÷NÆN£G×ÔÛÆíÚ´uû¼::•ÿ`‘7 ¿DµbiëÐ¥sÇîœC®gŽ€×mZ·m×Ð):ƒVæ/eE÷áŽlܾ‰3·.¾‡ F™G.[·nÚÆ1‚ƒ¸"M€ ®hAy¡’jæ°±»ÆM\Q·Kí®Ü¸qÑÒµ9¸‰%Z¨y3G •&ZxŒ¨  Á ,HŠÉB0‹W I½qI'r,±B¨ŠQ$" !#*¡bèΛv͇ӈwìp+®±ØSbØræ‘#7P€’Lz¤‹+¦P€À=B#ŒD@ Sªh#ˆ)(§™V,Ù„@™B±Â ! Tæ)goÆ¡ÿ§†÷Ë kƹf ¢Àd9¦`d¤PZÑ¡;žð$žP~èBŠ‚ª ¨Ž6°@r¸é&XR™„[rá†tªÙ… 7(*)ˆ4gžlÌ!'zf¤‘¯BÄÐ$ FZÙâ‘v€@-Î0äj¾AožwΑGrÚq§q„¹¤ *ð æ•$ˆ§ 7–€e K:˜<òà‚uÌéÆœsΧv\2'uè1§ibùbŽVÞéæ8H"`°çpâñâ=éÂb7RY‡žv¼™„‰(€Aä‰?ÆÑ'°Ä)G›m²YmΙµuÈ ìl¶Gr8©ÿB'€9ÆŠ- d(ŽyŒgº«špÀQ‰rºùFn ‡r¼Ñ„’eQƒMŽyC6…:H§Q{€I à(„Z®P8˜f³‰¦’pÞ¡FŠIö©ÆŽ* ˜£™©ŒaÐù†œrÀqÇ&:ðÆøpg5pl…:qdÇoœÑØœt/ÍFœkÈgmê˜deÀA¤ŽD˜8ˆmÌɆœ¯¿Ñg ž›Ù(: RqÒÉÆ \BDzà %ú ä^~a™fDY'$‚Ä9'KJ¸çú0&Gpćœqбƚm°áZpÀø›p¨y|Ï)ÿ§.öÀÇbäP°<±#™vÀùFeqÂù&…Q 0<&ò ’áŸRnèa’uj…Ã…˜È"zÆyýœgÏùFh¨Q¦–J,Á \¬ŽÐ@'zQ…)œÂ Y°,òw\c_HB޹QˆÃÛ°6Ì$ÀcæpZqÈ šC…쇗4VjtcK®ÑF8¼Ž f¯ÞÞ5È"%àŸ‡9L[ü£–PÈ„|X¨j A€Ç7²§ k0Ƙø+‡ƒ¸¶°‡=¬Aüáñ ÕjÂsÐÎô¡5@nl£mÕÀĺ±1ì¥T‡4ÒÁ Œ2-G iÈv¨ƒqëH;œ ÿ\xáTø.p´…p"Ÿ ñA$ ‚8ˆÆñ ü TƒÛ ÃlŒ¤‚ÃèÂÔÀ„\¬£ѨÇÆQÓrÄôÞXGJ½á|Ô«)à¤U40V§q´ãÜh‡<âÁŽvļõZ‡bGz¶Žccî˜þ¡Žw¨C Q(… @L¦©$D)"‡;üã ªÀD¾ñŒÅâêà-„ðS @,àQjd#$ÛǼn€#Þ(ã:*`VУ¦î€ÚÁv¬Cí@XÔ‘ãshV½cAG<~úxÐxñ˜—:Z4y¸c– Á?Öñ24B*€)ü›2,ä càÂ@ ày´¡…G*æð†=ü#•Ð… Òñ.qp£%Ç߸±]ójô¦òø†Ô€ C( xxÁ6쑃”#ñjGkÖ㟌Éò<ØñŽx˜7‘£<Ž,Nwãþrþ_$‚rq„²l­$Äÿ.`ªA zd#P¸…?P ˆü EÙÈ‘ß4HÍ-9÷¶{G5¨P edÁ .è-ú@ ~ðãÐÅ=L0nÌȺR‡Œ­éér%,1†bÙwÞ€`G šhâðˆ(&q$,!ÖÀ „B<`‡…€à«øŽÀWäÁ‡,¡9´ñÁp4uÚx<1‡sáÿX;²à(øãÎàˆñ9€P‚oDkø¹ÝQÓy˜c•LŽÙ!r8“ãÌ-±íÊŠaÿÀ‚´1ãI¡ƒ²\¸ƒœÀ&€áЬciP†8ÐŽqfµÿŠ×Ï‘1gÎCŽÅôñÁ•°ê{XóÖéÿx„ V;¤Ãèh»HLŒ·ÑƒÝð0þÎÚ™Cü¦À?Öü#¸Ø¤AjA…9¼ ZH JfP 6 êˆ)˜‰zÍjÆëðFÓbÚSìAó0 ‰bôƒ ÿÛ!3.+sŒÃˆ¨DüÑ 0C¤3ÇÆC«o˜ÁàèFÜñU>6­Ä×0„É¥ü ,C¤˜B¶ðÖ£Â:Q âHàà¡´æa:ÁȯÈ ÞÉÜÁÎ&î`t€â :œ‚žjÿ²'GìH¾w\BFêè¡`èÅTø¾ï¦ÈëEâÁü¸¡¥4ªV¸eÍ’ Ø`®@ v¡”!Ò` ³ †à!N` Pê!œ!èá¨`ÎJLn _ Z#Ö Iö @Aèà/²! äáR°¿{ÌøÀÁòÁöa b,Pð›P°vúhãÐTáÖŽ&({JLüàb`–^ À¡ê b ªÀ¿ú` â €Xúöæba5œ«®¾ANª® ARaÀà¶áÌ` ð€ *á0ͨwÿ  ²GºçvâŠöÀŠ ²á ¶ ê*¹¸§%Ðam¾¡¶Á žœÔ¡ ¦ŒÔà"œ Àîï ¾qò©®`!\aõ.á²a°áð àá Þ€Œü¨VÌ(¤A˜av¡á¼¸ Z¡x.&Î ‘´a ³!c¾i‚Äa5‰›¶!¢ è ® ô 6ÆŠ){¦¡¦ÐÁœn Zä%Óh'uŒ!þô\€á ŒàÔ!´ ¬ ‡°N!8Á€AT¿á´@äèØb(h%ƒ‡bÀ!ÄÀZacà! ª¾aÿ¼ ¨ä`§lÅâpd%Çœrñ‚ÈA‚@‚¢ þÀ†`rLWˆ)£úè‡ «¸A²Þȼ†A^"¾ ºº!.Á, G –ê€A`ÅÀ È´ Ú!óÇm*†¸J ”Ãà¡ Â`ôaö`ÐÁ%lA~#×Ê(Ì©(ËÖ{†Д€.ŠÈÞÁØa0†Œ¸KÜ æa§"mÇ¢ìŠA>àa €bï °Yf¦BÆ` Àöb+taÖ!Œà¼áüÌ$=j¥1¼ hÿa^ÆA î¿a !àÁ™È+¥€gcÂn'{®!†+cä¡ è!ì¡ hA€øa§ØA21’Òâa®á rî=meèèa Ô! è@fb$RÀpf-!ÂXàÖ«oâ ¬á0`0m®°®„N) >A,´! €àa” ¸ENö`Œ¾¡~ǃڲŒ”á’+£påëA â¡ÊáäÀÒ! ìaüµ£žÆâÒ3k´Æ$ ø! þab!5‚¤ P â. Ö€ÒŒ¨ºà2¦žÀ§l žFÿ¸Á)üàô  ã8ŸÀ ÈŸ`-̉æ3N ¶gÄaÖ€º)©R3Ó!†ÁÞáêT€ȳ’§êAí¬éÜüAРÜagL¶Ѐ8 òaˆà2£àH ”€ „aâaÞþ Š…qvìš2ò0Á Zj y¡ `VæÁæ`ÓÊkú²Œº¡¦PðRMÃÚî¡Ö³n¡N€ ìÀ ‚@€Ñ\ Ü!ß!ºK³ÜùÈ`ªVšTÂAZâ` Vº̠ ®":é4ª,ô€ÿ¬áèÁZás~¡nÉAŽ\ÑNà€zç%é:`”B¼`zSeÔ!vL'ŸSØÁÎ`lÇvÔP`À‡Öa† â!æá†`þa àve6í§ÖkÄa ”r(Æ3ÍØ v@Ö` ÔÀ €¡¬€ FÄ¿t `ø†¨a Ú`$a­á¢áŠ cøþ 6†áB€€A<(¡¢Ðè,cÞɤ )f!Nn*àà€òáÌZá‰ÔHìØ€àÁjLà!RØa! ÿ"¡¼K½kjù@r`ÀZÁRѶ ø©´Rd †ÈHYÿÀ†GÁÖŠ§ºáŒ¾¡ÐAaÀ„ºÁ¬`t ¼€ Ê`AÚáüá4áì ÆÁ*¶¸¡æ¡æ¡˜àuW òáÞÁ6Æî`8¡þá ´A¾.`€Œ ÒkÈkRà€¬ØÜáÜÎ!èà €þ6 ž7Ë´T Ê`–k΀Å~á†îk¸FS¹æ b¡Ê¡ AâÁvàzsôÁ ààXÁ’À ¯ v!†¬¡`¡ÿþáoàNáè`J@àjàô6˶`tÖ¡.þ¡øÀze0ÊÁêÜ` Zd„’3Á á raBôo á‚ ˦e þó póoá ‘ö ÜaMÙgVÈ/£^Ò@)Ô¡Âà,%™âá8Á¦gÊ!´ baÞÀâ@ÖR àîaÀàaáóía–Á´ l ¨aÞâ§ÁÀë‚GÙáÎ&ÐNGpù²aÒ ÖìTÄh`'â–*ªl`a¸¤!ð Ù:­iÿÈOÏà8WB(ʶ WÆÁæ• îÐ"Àæ7®à! ä¡ aüàþ¡¦á%ùâ%žœÁ» ¸ÁªÀô¡È`Óá–!†€è+çêÁ¡2ÁBË ´àN¯$Æz*j˜ ®lŒlAIÐzàØÀu26^èF¡, V6ª x€p¥¦Þ¡¦àêjÀHì-®¡ü (€>àj8¡í.ô‚˜Z\UB¸¬a´Á®âá>`øa&€àá¦Ü¡Qêàà ö!™”üá Fä` ÿb ˆ+‰›/ö` ,ÏáZ>ýà ÞÁ ä2•²Ô¡Ü`Ð`¥fèÀàÃH wÒ¡ÈTAÉZ¢]$è!bÁÞ°lÀBãêÐáw4*Þ ÒØ ˆ`úFu< ÞÚàðW±´kW¼` PAª*®ØÁ<òÀp â ®b!Š1Âj Ü!ÜapÁïà¾a~§Œ&‘ôNaÌoÜáü@›ò…Hãtz!ß2îÎ(Æd¿<¸–=Û! ` ËÉï´ÊAÎAcÌZê* xÇ¡,àÿð¡3môÇÁ ® ¥|Ëõׯàä`ybŽÀ¿zè ¸åL¡,êô áØ&xÐ#L ‰ž!¦!ÌÀÊ!â¡\`†`à!Ày¡òT)¹Îa yzVÆáH<€î  „îÁÈ`¥èáØ@_·` ö Â!Àj_ívå !⡼$¸µ â¡ ì !ìî ´ÚD”ÁEhà+àBÖ;§o½ 2_àᢠj€Á 5fÁfà!€ ˆA”AáÜ < òaÆÒ! ~@•Rnú\ÿ0"íV@êa.öÀÇ€ô!u ôÁ Ø` ð!j¥jÜaP¡ä !¬ŸòIKap·Î÷ Ö^~퀧È€ÌÀFg'–  `àln ¡´¦m 2ŒìµÕ+^ÌþA€à¦ß˜Á! º Ê Šà€`fƒ®ÆÑÎ|2ÁäáˆÀÈ㪽«ºaPä›Âaè¡ 6ÁËéÀ5" Ô‡j ìÀ°ÐAèbM!piØ —Î\:vcæ±n\µv7ìlX¶Î[¸nàâ)bñŒß»sáÿÈiëÆ[8sãèéÛ.Ø#6?šâæîŸ¼îШÀ毹uểë¶-¸kàØ¥É!ÉÍ;y¤L,hWNܹréúÁ€Æ:uéÊåc“«½5j€¹}ûθtëÚ½‹/ yñÊÌ;§'W,=ä¶vë–ÎÝkyÔI÷­›Œ1;0©ƒ8®›7qëòy@Ã*H¦ ©¢FQ'cáèÙÓwïž»z%”¥ˆß¼rà¸}û†®›¸pá¼Iã69Õô° 7Ï ‚ày3‡Ýž"Mx¬µ ÷mœ¼3ÆÕ£³%/>yÛ»l‡°|q¼ÝCcON*^ñÌe—Í9p@Ã:âÿxÃ2&Ä1…òtCM6߀£Q8ÃeƒI`aH+¼Ñ€!wdp|ÀÄ»´cO7ä„÷9à(·NIÜlµÎ7Û Ï¿”ã) ´#3¤ Î6ä ÇN8`Tc<ç$DÂÜaÏmäåFð} f]ô£?dŒÓ˜QÙXÃØ3|Ôh8­ÜpG*ô’Ü8›yà 6¼¥#Žžé¤ÃÍ;õ¬“Î;Ib·Ù;ö¼SŽ6Þ|“Î:3ŠCŽ7KvsŽ9åœÓNˆ€ØÂ†¡”¨A"íƒ6âäÓƒ?¶3å8XTƒÇ?‡dÑa fþ"Í%Á¸QÎ6ÜxsN7gÿ€#H:㈃Ž*Gà1†9f$qOVÞpDÎ8×PN:Þ8é‹àüRÄa@€ò¬SÎfåpäÍ6ÚŒ³N=¬ÈÀ‚0ìÄ,Â4à‹$ óÅ5ÐcΤ@u°Ïõ¨ÃN=¢x²Ì#ÿÜE{ BlËí•òÊ7‚È“†,­¨£Q9êÈqŽëÄJ+:ØñHA0°ÄLâ ‚ÜLs7à@Î7Ù ãN îðëN ßàÓ .ØaŽ5ÚdSN4»Ibú|Ã<ç€Ï4L@@"t„³Î:h<€3눣œ;)$S§”šaÎ+‘ØjF{Vìáråw•‘úØQ÷ÿÈÊM9l¬³Æ;1²ã‹qìÉ4e,#Æ<½Ð#ø®ðDìÀÝÀÓ1wŽ6׬Ôϵ(O).Ä` *@p3¹lcÊ>ýˆÁ€4p#¨zÀ‡;áH8Ž—42Œ; gñÏÐü³Â&íb‰åþÓåˆIã[pÄ2¾Q m¨ƒ ÷CÆ(ôŒÐÁr€AÂÐ!uŒÂé` }´.@¢à‚O ÃÜ ‡3¦± i”ƒÚø†0š2üãà˜!vä@èÇ!Àt@¨0:yô`cªj Á¢xE;Þ‹ÿAà#ç˜Ü£‡ÿ©Ñ-| GÄq QœáÚ0‡ßð1lhÄé8ªa0ÑpHF"”Ñb¸#p7¨á§uÁ¼ØÂàfô oTãÎØÆ6ÊÁ-ÔãñxTÁ†lTB¯ÐA.` T`°À²áx˜ˆ€°q oøâtE'Ü‘,Ád[rOÖ¸F=€Bò`Ú u˜áƒpF6¶!u¤Â‚<À <ÀùÈÂ'ä1 #´U¨G7¬qvPÐàÓ¸²Qlt#Π3ð-Ü o˜…?p@)tB°€4` Tÿ€ €)Î w8‚C(€$Ö±¤=$¡=:˶eàí14¡‰‰T݃ €xG7Þ1oü¢ìè†6à‘†aJÛ@ 0#ãø>ñ |Ñó |!s¤dÓȆ8¨vgàï 6ెn(ð@$ ´Í_ˆ€$ñ…W‚2:Æ/€£–@Å:à '° ¯ %0 ôê  5t 2´p° `÷°rÀqÀÀÐrðyµtp év >ààyP ±à @  øl@q0w/·yÐhØ…lœK!&,v± qÆnµ´Õm¯Ç ü0 ~@Ôp Ú€ <ðªÐ6`>0 €lÔ0E0‰>@Àà€ [pÿÕÀ Ç 4ðzpü ìðâpp °£wg ]°._`?3À‰ð ðJ“Å_0b@…à€W-Wf–õy-7Y×PÀgpQ ø?Qpg" Ý6t¬WK­Pß  ÛÐ íP ÷@òP @ÿP7@¬àËà& 0pòpXš Üë¥`€P¢ð2€þ ÿ %€/2pŒàp'ðð N`¢k Ó €Š`“U…CyaP § C  °¯sa6ÿY"† pÀÐ tñ êh9•P‚ô‡´SPïÈxâ0 Ó J‚ Àé°øà[À9ÀË0M¦ Yð°ÛÎÀí@§P5Pw÷óà+ ìÀ¯PÃCP ë jÐèp)€tà‚Bp Ðy`KSIÌ–hš7›²6nЩðð  S@W°$›By^Y9à@qEwQKtQïØmp"t$c×RãBé` @ð7 : N ~ µÐy°4@ñà'ÜPóà ñ`Æ@yp *° ! 9ðÿ*ê  p[À{°dðGpî0[ !K#&f£§h°€¯E[o€0Pÿ@àÀ bð‹€d°Ä §çp±ˆ5J^‚ä=@àð æ€EâPFðÞðUP ‚ð ÐÀ Ipõqð À'€8` ÷€Þ@=ðQÀUt F€C(ò@ š  hó[P ¥p<TXs1‡¡C)  o€ ½@ np K  ÿÙpÂeËKV*p”ós4P# ÿŽff…±ä=Ð P‹°öð 0ûà î d2@@ß0BÀ@1àÓ`ÿðÃ&Ѓ°]Àá¢#áD øé1@‡@,°T0} à pt€,àÐD€l` uà€c6zÂKÔHY0“àüP Ÿ€;àK¼À €]ø6ðsZÀ¯wQ¹ ;0z Ð|°7 ¬WÀü°«0±ÀoÐlÀ?f€>Ð DðÁ0`À`ðo"Ю3°5°XÀ^ˆ Ì(úèP¼±ÿ ¿±$êæë`(•× WÐ{@˜À c €0 »ÐÐ\ ;`6À_p÷Úh4ôµïÀÎàÁP »ªð×F aÙz°d0f@ { 4@À°D–ðì4d\@ Ý  Ë` @ ËðÔn ʰ€°{%€à)ðRðw@ž •P °p À È Ë ¾ ¦`` J°%‚‘Àc@ ñ0&P° è@l@óÀ ë`Û°s0ý°Cú°°0ƒ@ ö@BÀÿm€ë ÅSpmŠûz³µÈ°ÿ ù0  mÄý…Ð p×pO)€ X 0 î ÅTïÐq0†0ð° žBìpðpò÷ÿÐó AlëpúÀô0òðó°è•€šÀ:4þ€-Àç`ÔS€® ÿ27@ä°ëЛ¾ù !ð´dù…Ð PÀúÓY! ÀÒùzÀU  å ˜Àê@ ¶2 Äó Ù 5Ȱg"dNP„) `)ÿ â›{é^Sú!ÌÅ ÷$ .)ÞPùPìð @}œ°[‘óðD`(ø€ þÃð¡ À ²$RÄLg Pft0Çê° xWBÐYT é4ðTPp ‹ÉPGà ` @Ï  ügÓ  JÁöÐa D`} û bÀ i @ðøÀöôð.1êPíÐÜ Ø ¤° bP+p°pSp ª`G0i¯NK‚? 0¼€ qp8Њ` qÐ`èP5ÝPßpèÿÿ‚ŸŒ ÚìTÿU° P^À  Фa¬×{ð;0ÿІÛK` p,ÐNÕà– ôÜàvÍõMúPhœÐ›° iÀ‰  pt`}°° ³@ Ï0 Ø€ ¯0ñWpÉ”° ž° ¦` Ë@~ ÒÐ ð wÀ@ |‹û0 Ð ÿ€“p+¡ä@µ` a](ÀAPÍ×"dÀÌà ¶Pì¶y¹€ôÀ{» ¸MX´Ôz pðà °å`_ `µ`Xà äÀ Ù€ +ÿLါ$°§[N0.phâѰNÀ]ž ŸÐ° œPŒp –p ˆ ¤ð¡À •p ˆ ­  ­Ðw ÊBp  QPàïµ2ãP0 ¸p Ÿ ™à 8@?KRÃ#°õ ½ð¼à 7 ³5f ປE k`±‡À€0äEDÀƒÀWÉp ‰ÀäÈôHä e5!ã0ûÀ á é€ô0 (ìð à™0 `¸@ËP‹p°’ ³£ð ¦`”°”0  ŒˆÀ sàÿ#ç5%`½0à îp-ôð Àð ˜àœ0 zÐfÿÀãt’à Ñ  Õ@¸p-@^#fP‘à àR ,£F3QÞ-O`ªAÊïÆ„ äpöÀcô]0ú ÚÐ’P´÷ ' Y1ÿ€ 3°éðv°†»ð•à‰` lj' `’P q Œð¡` ž   @ ú& @ ™ð¡ ˆ0Nðx· [ðÊ™ Ú™@ ð~P|🰽HÀ‡Õܰ³€™hf¸½pÿÀ0mjÄõèTzÐÝ8–¯'Y ì°ÝðŸ@  ž€ ¦B5€˜°)à°ô°ÿ€sðæ#°Z ˜  ‹°†Ð‚ k“P ·  ªð` “ š@€0 Ð ¾j “ nP ÉP ° ` t°8Ѐ røP àjÐvPy°fàN@· þ0úðäÐÚ ¶€ÑV*f6zÀ P«p¼j”® S0::K³ä=‰Ë½€á`¬Ö« šÀ‘` à «`ÿ …€ º0 N@pI†)X¦@ƒ|ñià ¢L™üÄøaìO”R·6Mš“ G~C¬Ðª>ˆvB„+T™EZ`¡é’ãÅŒæÐ3ÇŽ— 6úÌC‡Î#ÿŒm{Åe*}öÎ kÖ®Å.0XàzáÁAzøVÖìY´iÓJÐÐAB.pðÀÕB‡ .ˆY‡Ž 75à¸8DiÑ¡=|´8ÕêÖEŒZ¹ „¨¥?¡(¹¹¥G‘®Päd:¤£4}P±á²Ž&Et,™)]k &bÞ8 Ò£KHuTÊŽ§7˜x©aô&C }áÖÕQ_ ÿ vÎL¨’矲}œäñÐÖ-Û¬N¾˜½À%EÐ .dЊ`QÂÒþRÛ­ 6è‚? ÈÀ‚ (àà‚ 6ÈmøéƇuÚQg½Œx"Ü…ŽYFéÃA¥j>å Uð0Gq¤Jbé#“9i£Y å<e+ Y% RD%C˜øä•LB¡¥aàx€´ Ã="±ä$>Pgžv h‡þù§d ‚ºÀç™n¦ñç \9æ ‚ùggšs ,àÀ‚ .Ѐef?ÿ"ÆÁ™8!ƒ ´@NéƒÀd$©f̰ęÿx â‘\ˆ Å—I0ICHä•V˜e [Áƒ‘/è„”N¡cO9ƒŽE: ÂKü(e8$ÄIÌ`Ã(~Y¥‘`²Á„; ™c9*¹ÃX^1V¡£†9ú1—p"’;Jé‚r )çšq¹eŽÜ8¦;ð)$l|øG 08$Hš<Ò"KÒþt  ®(TCìÀƒ  †kÞ!‡›o,Ñ€—|~¸b’LQ¥‘Dtù¥‹CY„ H2‰…=d4h™‚•;1äK\ÑEPa® (lYa±å+`Ð\ & øÆÿ?1,‘>K%YfiâÞˆ™rÀùFšyåeZÈGoÄ©’fz ¦`ziG—cÖ)¦~h0&€¼6à€ ¸’ 8‹?ÕBƒ ž¨¸-9Ø@0 ˜FfG²a%„|‘:úE±ùøŒ’G\á¢O¬è¡º0„R2ÙäM>DRp¤@N9c 5>qãEðPF¨OJÑ‚ ö€„‡°“EÁ D|Bv˜‘}¨à¾HÂ?àaˆ&üãÛ Þa n¤£¢H‡ ú?™ƒîÈ<Ôáod€àTlˆøcÿˆDì$% p À^4·dÌ)Þa wäb¨8Ä Ô°‹0´O‰„ ‘„Y8âR@ƒ x6ÌA€à„êÀ\k°Z ±T,B¤¸‚ &щV¬AXÃäH@"ŸðÄÄ ‹VXB™Ð!°‹P㟰ðQ„‹Cvð5ÄÂ:ø9Pñ;˜BÄÈ1èÑŽ”£í †ŒÔà p šÑ¬@[à3|‚CÀÀ~0>àaCA ``~øã,üàŠRxs8'"ñ QøÁ¬ F+81‰@ŒbsØã@aòÙ¡ Ã@îÜàîøÅgŒñä‚ã=ìÑ_ð?ˆÄ ¨0Ȩ†>ÀLÿ¸ºkPñÒ»l (F< u4„&(ñ‡IˆÁ‘ÙÂNá pžPÅ'Ä`ŠEhâ’à+æpað"¯8Ä-±Š, 8Àj x ~††Qˆ@0‚p°ƒ+,щNÜ!_Cÿ) q’[h‚œ8E^QGÂvX-øÐŠ1¨À)øÇÀqUÔ£ß耚€P8AR±¨.‚8(‚»ÝE :ÀC±ÌtpŽÿð‰{,áºèF0èᜠ•€#AB|øC+‹7ˆ•8(¡ˆBìA›¢´ˆà05ÂqtÀ í0†üñ\ˆà¥(:ÔÐS0B¥ÈE.T‘ >¬A }@Ã2d‘„\hbš…X‘…A¤X‹àƒ˜ð üƒ à„'þŽ´NÌ,C8eœA`—Ï2;èˆ]Ý ° h<#â F3ô°Ä¼‚’¸„#†‹B¤–pÖ`‰; b®j„#‰QHB & 4ˆi@"Èà=€ÁfhèD%æAÿü#îH‡2®! s¼á\pÆ)1Š6£9xƒl‘Ši$¢¯ÀÄ!Q®OtÂ!H þñjT#èàíXl°0Ààí³@á%ó—50Óe à`E1láo‚¼ÈA;B†=¤¢¼ð&AˆD¨‚›­ &Q #„Â~˜…(æ°„|4ø‡5ªAŒp”ãUP‡"®ae ÃûXA:¶ vXÂ}°†:R±Šlb{¸%ü` Uˆb‹zpÃ+òpˆP ¢­äPèÂèhºLu³ Á€!Ê¢ äC HbƒÆL—Cµÿ Íp†=Öá[ ËÀøÁ¡ ODI‡ C%$q BÂlÐÄ$j[ F@‚pÄü‘sø_(5sp„ˆ„xp†3ø‡Kh†w(`‚wˆm˜EH‡+¨‡fX‡rp„ 8…„HØ„B@„dX…9 Oˆƒ9@„ HñùƒF(S€ ˜®èK‹‚³ø¬;ˆ&p 0+B€Røm°†~øzàH„-@jø‡(€+@XÀ‚O`OP„M¸„¶:„_ˆ?Oˆ¥L „ØHÈ,èV(s˜mXq@ƒøn ¸‡(ø‡uwÀ†z(ÿà€À\y`X ‚x‚0Ø…JPG°…: -ȃað„Fè—0X(‹<@`‚TÈÁþðA.»ñºØ:YÄÐE1‡+˜o`é@GˆfȆh€Jè(€8ЂH#`Pø;XMð"€xȇjx†8{À9wP^øø‡C¨iH ø‡hÀ‡^ƒ@´9-ˆmtðuÀ„~‚wø†hºÀKˆ„c°N]H„;  €8„o¸†/H€zø5èVL‹^@¾/08É"Ì€ƒ|p‚öò‚fÿ‡pЇ](‚{ø‡?¸ ƒAðACI]@X°ƒC6¸B  iÀ†m¨o@‡}°yr†~`È8È#ˆ(Hz8x Ї¡Iè‡_…Fh€¦S€€‚U4 ˆ[x%‚ Ђ^(r0*ð“uP‡lH‡~8l̲h·"t‹!Ô „RÀ„_‡;˜…KÀ{„K˜‡W‡0àü…WPL`…LÐÐ@„GX‚'h¼aø‡O¨x ‡i¨zPy@„h`8ƒPÈ-ÈP€ ¨20)Ñ|Z`¦I€ ÿ0…~˜u m°yp¡ux‡Ãd‡sø‡pLÇ4P€¸ €ŠÁØ„p†Uˆ„^XWXË)P†w`†? ‡P‡<€sÀ…80°@,Àr˜îB…3ðØ€à`:˜TpÏ´ø‚ (ƒõFÀ’EX‡€œv`‡Uz€‡ ¹¥­4Q- pOȆUh†I°dFȇî‚øÈx€(ˆkàoð†v0‡8€…:eèØ€2~ø|˜‡y8Ïx8euX‡up|øU€-ûR…TxXØ@Ȇu@zÿ@uØxÀ{‡v ‡r°røk0‡Ã)…:@„ (€0=õN8 «vHvx‡w‡ 9œÇy‡vˆxø‡j€Ø“kk°x0‡< Uä[pІk0ƒ|¸v؇hx(|ˆ4€Àp X€˜d8‡t ‡pV´xBà‡u tP.xrP‡zΨ‡Ê_ˆQ{€‡}ȇn˜5øDØu¾T@bXsà†'Ða@…>€-øu(‡q˜tP8…Y¥vPÏrP‡n°{˜Ôp‡@Ø„ÿE‹"x…z0‡óD{Àp‡H°„³ÄÑ|0‡Ex pø‡V‚èqhº6@9‹GP2Ø;…—YøCux‡z@yˆ‡xHrѰ×tHbq8‡q@‡tø“¬E $x!Ѐ+8n¸‡%×y°<€‚[x‡y8‡pðpèzà…¨ƒz°‡{ˆ‡rH|¨‡Àí¸‡|ˆzÀzà0‚¹íAP`THÖÎ}‡sˆ‡£yˆÕ}‡r‡oXÙ쇪+]´(`k€o ‡q0]J‡ypv(9D‡M~¨‡u8r‡Tÿ=‡u0r W9íXt@v¨›tð]´hCø‡|P‡Uè‚€T@°‡y QxÈ|Pe舀 ‚7@ß³ÈQP\`€|px8‡l ‡vxs€‡vØ%q`‡qPá †wXíÚXU-ÏŒ]T‡|x‡ ``øV°s8w(‡òì‡~p‡f&‚/Єnh†_…a³@|hÞxø‡X ¢ÙvX‡yxuàs YåõÚÃŒ‡®…‡w ‡M›u±•‡rýâS_ö(ÝY>(À!ˆ‚+‚(àt!TÀƒ®}vqÿðR#N‹0…zHVu‡È„UuˆX}q¨\qpû ÚÙwp¡U­^v0wpè}}]là@P€€ßyY@Zˆ¢*µÃl2v‡}Іƒy¸y˜8€jˆ[(ä´hZøv¨†\`…ÕƒX$p¸(À0x„Œx†\íwŒuÐn8†]´à•ø‚M¸r€‡q1°*LÕV0!x†~ßtpî%‡ñýÞtXÕ‚þiÀ–ý‡m˜…{€‡@æ´Ø6P~à{ˆÑN†v Px€È/°„]HF>K”þ‡›T“tÿ`†T°ƒ$H¼À5¸„¬M_Ðs ÛÇsPñ d@†[v‡v@–‡yÐ\˜‡r Aà‰N‹$hq؆b†Sèƒ* ¨€¸ÎH1©Mð„ã›jCp~xØnÙtðØ9ß³EÕ®‡ÞÞt8Ìwàv¨0nH˜jÁUøÛNô”S9Ó~ëqÐ^9[vp‡qH‡-m‡zð‡yhélÏëÆ\£~P[]/Vdu×5^T-hy°‡mh†_(…7`!Hoø‡gÏþmÇT„:Ø\Ž{8e¥>tN…J˜( x‚:0fP^5_Q“b „  `àþîÆÈ‚:„Q s¸É~Ýø´û‡e¨"X`WðÎoÍ'€t˜‡•ÅI`-ШŠV[Ðoopð©~…X€ðü;milter-0.8.18/doc/sflogo.png0000644000160600001450000000072511246122255014536 0ustar stuartbms‰PNG  IHDRXTúÒtEXtSoftwareAdobe ImageReadyqÉe<ZPLTEÿÿÿîîî_jr´¹½Š’˜Ÿ¦ªt~…ßáãêë숎¿ÃÇjt{Ô×Ù”œ¡ª°´ôõöÊÍв¸¼’šŸìªœ˜Ÿ¤ÖI*ÍÐÓÅÉ̹¾Á ¦«ª“‘ñ¸–£éžmV§RIDATHÇíÓÉnÜ0DÑÛ÷q)µ'±3ýÿof!·‡dÝCµ ÀÍA¡r¹S¸p—œð Ÿðgƒ¯]{%ÔÞrR.jjd_’gW7T{«]Eud0MêqX.\v+Û˜´^np†AÍ9C3A³T²YYü1J%¶Å i&ÀbSäµÁ úåÎ#­Y}R ®%üzˆ‹Z€d)ûQ¶½Á´Üã¯@ÚŠö¹ð­ñ\ÜÀäÃkãZÊ¿g¯„¿ÇBí}:¢Ät_ jO¦H†¶‘^.<~{L¶êSlêFV×këjº–`ï/³ë”Ýþ½¨ë³êÏ?¿’8¦(ï¦8?È Ÿð'‚Sö »‹ÉzòIEND®B`‚milter-0.8.18/doc/SPF.gif0000644000160600001450000000316110327523043013652 0ustar stuartbmsGIF89aW2ÄâSZ\÷œ•ÎÏÐþûÝ væÙм¹´ÍýÙéëÿþò;:ªólhˆéãÙ¾ÂÑÎÛá|‚Àôìã{}yâàãde¼˜”Ȭ¥¡›œœ«·¹ÕæèÞìïÝëîüøìÛêí!ù,W2ÿ ×diš\ªŽéé¾p,—âgßxn§ÛA d‰Ø:º¤rÉl‘Îg Éä‘j"›¨x܉‘ÖÁdA xÞ„Å$‘ù†Éx¼ÙÉQhknoƒprwy‹ePL~l„”q ‰ŒJ{K’•”q’™›Šž­ Ÿlm´µ— – «Ž­Œ¯:–\ ÉÊÊ º 3ØÙ&OL†Ôâã q±Eñòóôõó==õ Â8Xšphœ8 @pÃA(`XF±¢Ee ¸’L:hB`hÍŸ¤ÿ<ØÈr#4g2"8 (@¥ræ$p Oø* ÁT($ª¡Dvo0IêAa*Lƒp€àίƒLh `BϲÒª%ú €† L‡ýX0èÈ»B¨ ê@ëÀ‚]€«€á ¨ÅE—€¶oãæ –€®Ø‚ä0ЫáàC8Ô¼ ÖIƒ… ,4&ºød¸¿p“535„ 0<èwAèѤ †ÐàèâŸd_K–]Œî›-9n`…åB~[¤ ;-8nÄ’=pvìÐÆi]»…ý©wîoÆž DËù1À^gÑÅÔFj-þqÄ1c@yðXG …ÅÿzËÝÐÁ]ÕÔ&L·Àa†EP“;ëP€ \°ÀTà|hÁІ­(Ab¥KŒ—À`l~Ë~Îè%I€âh¸KM0€(\°d”@iA 0`Ø7šV7†bÅ.n¶™BçXq¸“à–7ì PPñ“…Tv:@©`;Þþ–—„ÿ¦€ÑòÖ+»MOOºsÃ…\¦¥~AðX7š`ÿ–`ABMbƒ¤ZX— 'ÃIØnp&Cad8@CþYƒ -4A\àˆwÄ"Iň‡IQw v.ªn…@ì˜Å¤’ G™©1FÙÅTrÁî%jhÌ¢£B\˜éL^|ãO¤ÁŸS™qeÃb‚(DWùÄ("ÏÈ“Ÿ|±(køŠ×Z’FÀY@n*ãŸð°iZd£È2Eï}Lä$¿4ŸîéD„T9@©xºC±p– Gs E~/£üÒ,ai¼ÓáÁ|c ¦0‡IÌbS*Êq¥¾1Hò8ó™ÐŒ¦4§IÍj 2¾$Ã0´nzó›à §8ÇINp®âgNØÀ¼ÀÎvºóðŒ§<á ’(Ó À‡>÷ÉÏ~úóŸý'?: mœ@M¨BÊІ:´¡,;milter-0.8.18/doc/logmsgs.html0000644000160600001450000001707312120724406015102 0ustar stuartbms Python Milter Log Documentation
      

    Milter Log Documentation

    The milter log from the bms.py application has a variety of "tags" in it that indicate what it did.
    DSPAM: honeypot SCREENED
    message was quarantined to the honeypot quarantine
    REJECT: hello SPF: fail 550 access denied
    REJECT: hello SPF: softfail 550 domain in transition
    REJECT: hello SPF: neutral 550 access neither permitted nor denied
    message was rejected because there was an SPF policy for the HELO name, and it did not pass.
    CBV: sender-17-44662668-643@bluepenmagic.com
    we performed a call back verification
    dspam
    dspam identifier was added to the message
    REJECT: spam from self: jsconnor.com
    message was reject because HELO was us (jsconnor.com)
    INNOC: richh
    message was used to update richh's dspam dictionary
    HONEYPOT: pooh@bwicorp.com
    message was sent to a honeypot address (pooh@bwicorp.com), the message was added to the honeypot dspam dictionary as spam
    REJECT: numeric hello name: 63.217.19.146
    message was rejected because helo name was invalid (numeric)
    eom
    message was successfully received
    TEMPFAIL: CBV: 450 No MX servers available
    we tried to do a call back verification but could not look up MX record, we told the sender to try again later
    CBV: info@emailpizzahut.com (cached)
    call back verification was needed, we had already done it recently
    abort after 0 body chars
    sender hung up on us
    REJECT: SPF fail 550 SPF fail: see http://openspf.com/why.html?sender=m.hendersonxk@163.net&ip=213.47.161.100
    message was reject because its sender's spf policy said to
    REJECT: Subject: Cialis - No prescription needed!
    message was rejected because its subject contained a bad expression
    REJECT: zombie PC at 192.168.3.37 sending MAIL FROM seajdr@amritind.com
    message was rejected because the connect ip was internal, but the sender was not. This is usually because a Windows PC is infected with malware.
    X-Guessed-SPF: pass
    When the SPF result is NONE, we guess a result based on the generic SPF policy "v=spf1 a/24 mx/24 ptr".
    DSPAM: tonyc tonyc@example.com
    message was sent to tonyc@example.com and it was identified as spam and placed in the tonyc dspam quarantine
    REJECT: CBV: 550 calvinalstonis@ix.netcom.com...User unknown
    REJECT: CBV: 553 sorry, that domain isn't in my list
    REJECT: CBV: 554 delivery error: dd This user doesn't have an account
    message was rejected because call back verification gave us a fatal error
    Auto-Whitelist: user@example.com
    recipient has been added to auto_whitelist.log because the message was sent from an internal IP and the recipient is not internal.
    WHITELIST user@example.com
    message is whitelisted because sender appears in auto_whitelist.log
    BLACKLIST user@example.com
    message is blacklisted because sender appears in blacklist.log or failed a CBV test.
    TRAINSPAM: honeypot X-Dspam-Score: 0.002278
    message was used to train screener dictionary as spam
    TRAIN: honeypot X-Dspam-Score: 0.980203
    message was used to train screener dictionary as ham

    milter-0.8.18/doc/license.ht0000644000160600001450000006000011631757745014526 0ustar stuartbmsTitle: GNU Documentation License

    GNU Free Documentation License

    Version 1.3, 3 November 2008

    Copyright © 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. <http://fsf.org/>

    Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.

    0. PREAMBLE

    The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.

    This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.

    We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

    1. APPLICABILITY AND DEFINITIONS

    This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.

    A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.

    A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.

    The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.

    The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.

    A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".

    Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.

    The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.

    The "publisher" means any person or entity that distributes copies of the Document to the public.

    A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.

    The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.

    2. VERBATIM COPYING

    You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.

    You may also lend copies, under the same conditions stated above, and you may publicly display copies.

    3. COPYING IN QUANTITY

    If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.

    If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.

    If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

    It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

    4. MODIFICATIONS

    You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:

    • A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.
    • B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement.
    • C. State on the Title page the name of the publisher of the Modified Version, as the publisher.
    • D. Preserve all the copyright notices of the Document.
    • E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.
    • F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.
    • G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice.
    • H. Include an unaltered copy of this License.
    • I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.
    • J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.
    • K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.
    • L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.
    • M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version.
    • N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section.
    • O. Preserve any Warranty Disclaimers.

    If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.

    You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties—for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.

    You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.

    The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

    5. COMBINING DOCUMENTS

    You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.

    The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.

    In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements".

    6. COLLECTIONS OF DOCUMENTS

    You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.

    You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

    7. AGGREGATION WITH INDEPENDENT WORKS

    A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.

    If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.

    8. TRANSLATION

    Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.

    If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.

    9. TERMINATION

    You may not copy, modify, sublicense, or distribute the Document except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, or distribute it is void, and will automatically terminate your rights under this License.

    However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.

    Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.

    Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, receipt of a copy of some or all of the same material does not give you any rights to use it.

    10. FUTURE REVISIONS OF THIS LICENSE

    The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.

    Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. If the Document specifies that a proxy can decide which future versions of this License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Document.

    11. RELICENSING

    "Massive Multiauthor Collaboration Site" (or "MMC Site") means any World Wide Web server that publishes copyrightable works and also provides prominent facilities for anybody to edit those works. A public wiki that anybody can edit is an example of such a server. A "Massive Multiauthor Collaboration" (or "MMC") contained in the site means any set of copyrightable works thus published on the MMC site.

    "CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0 license published by Creative Commons Corporation, a not-for-profit corporation with a principal place of business in San Francisco, California, as well as future copyleft versions of that license published by that same organization.

    "Incorporate" means to publish or republish a Document, in whole or in part, as part of another Document.

    An MMC is "eligible for relicensing" if it is licensed under this License, and if all works that were first published under this License somewhere other than this MMC, and subsequently incorporated in whole or in part into the MMC, (1) had no cover texts or invariant sections, and (2) were thus incorporated prior to November 1, 2008.

    The operator of an MMC Site may republish an MMC contained in the site under CC-BY-SA on the same site at any time before August 1, 2009, provided the MMC is eligible for relicensing.

    ADDENDUM: How to use this License for your documents

    To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:

        Copyright (C)  YEAR  YOUR NAME.
        Permission is granted to copy, distribute and/or modify this document
        under the terms of the GNU Free Documentation License, Version 1.3
        or any later version published by the Free Software Foundation;
        with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
        A copy of the license is included in the section entitled "GNU
        Free Documentation License".
    

    If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with … Texts." line with this:

        with the Invariant Sections being LIST THEIR TITLES, with the
        Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
    

    If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation.

    If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.

    milter-0.8.18/doc/credits.html0000644000160600001450000001500012120724406015050 0ustar stuartbms Credits
      

    CREDITS

    Jim Niemira wrote the original C module and some quick and dirty python to use it. Stuart D. Gathman took that kludge and added threading and context objects to it, wrote a proper OO wrapper (Milter.py) that handles attachments, did lots of testing, packaged it with distutils, and generally transformed it from a quick hack to a real, usable Python extension.

    Other contributors (in random order):

    Christian Hafner
    for the pymilter mascot image of Maxwell's daemon
    Stephen Figgins
    for reporting problems building with sendmail-8.12, and when building milter.so for the first time.
    Dave MacQuigg
    for noticing that smfi_insheader wasn't supported, and creating a template to help first time pymilter users create their own milter.
    Terence Way
    for providing a Python port of SPF
    Scott Kitterman
    for doing lots of testing and debugging of SPF against draft standard, and for putting up a web page that validates SPF records using spf.py
    Alexander Kourakos
    for plugging several memory leaks
    George Graf at Vienna University of Economics and Business Administration
    for handling None passed to setreply and chgheader.
    Deron Meranda
    for IPv6 patches
    Jason Erikson
    for handling NULL hostaddr in connect callback.
    John Draper
    for porting Python milter to OpenBSD, and starting to work on tutorials then pointing out that it would be easier to just write the MTA in Python.
    Eric S. Johansson
    for helpful design discussions while working on camram
    Alex Savguira
    for finding bugs with international headers and suggesting the scan_zip option.
    Business Management Systems
    for hosting the website, and providing paying clients who need milter service so I can work on it as part of my day job.
    If I have left anybody out, send me a reminder: stuart@bmsi.com
    milter-0.8.18/doc/milter.ht0000644000160600001450000004051512017231717014372 0ustar stuartbmsTitle: Python Milters

    Viewable With Any Browser Your vote? I Disagree I Agree

    Sendmail/Postfix Milters in Python

    by Jim Niemira and Stuart D. Gathman
    This web page is written by Stuart D. Gathman
    and
    sponsored by Business Management Systems, Inc.
    (see LICENSE for copying permissions for this documentation)
    Last updated Aug 08, 2012

    Maxwell's Daemon: pymilter mascot Mascot by students of Christian Hafner See the FAQ | Download now | Support | Overview | pydspam | libdspam

    Sendmail introduced a new API beginning with version 8.10 - libmilter. Sendmail 8.12 officially released libmilter. Version 8.12 seems to be more robust, and includes new privilege separation features to enhance security. Even better, sendmail 8.13 supports socket maps, which makes pysrs much more efficient and secure. Sendmail 8.14 finally supports modifying MAIL FROM via the milter API, and a data callback allowing spam to be rejected before beginning the DATA phase (even after accepting some recipients).
    A Python Pymilter provides a milter module for Python that implements a python interface to libmilter exploiting all its features.
    A Postmark Now Postfix also implements the milter protocol, and you can program SMTP time filters for Postfix in Python.

    What's New

    • milter 0.8.16 has dkim signing, and Authentication-Results header. pymilter-0.9.7 has several improved diagnostics for milter programming errors.
    • milter has dkim checking and logging in CVS. Will use DKIM Pass for reputation tracking, and as an additional acceptable identity along with HELO, PTR, or SPF.
    • pymilter-0.9.4 supports python-2.6
    • pymilter-0.9.2 supports the negotiate, data, and unknown callbacks. Protocol steps are automatically negotiated by the high-level Milter package by annotating callback methods with @nocallback or @noreply.
    • pymilter-0.9.1 supports CHGFROM, introduced with sendmail-8.14, and also supported by postfix-2.3.

    Support

    • pymilter mailing list
    • SPF forums and chat for SPF questions
    • IRC channel: #dkim on irc.perl.org for DKIM questions
    • IRC channel: #pymilter on irc.freenode.net for pymilter questions

      You may be required to register your user nickname (nick) and identify with that nick. Otherwise, you may not be able to join or be heard on the IRC channel. There is a page describing how to register your nick at freenode.net.

    Overview

    To accomodate other open source projects using pymilter, this package has been shedding modules which can be used by other packages.
    • The pymilter package provides a robust toolkit for Python milters that wraps the C libmilter library. There is also a pure Python milter library that implements the milter protocol in Python.
    • The milter package provides the beginnings of a general purpose mail filtering system written in Python. It also includes a simple spfmilter that supports policy by domain and spf result via the sendmail access file.
    • The pysrs package provides an SRS library, SES library, a sendmail socketmap daemon implementing SRS, and (Real Soon Now) an srsmilter daemon implementing SRS, now that sendmail-8.14 supports CHGFROM and this is supported in pymilter-0.9.
    • The pyspf package provides the spf module, a well tested implementation of the of the SPF protocol, which is useful for detecting email forgery.
    • The pygossip package provides the gossip library and server daemon for the GOSSiP protocol, which exchanges reputation of qualified domains. (Qualified in the milter package means that example.com:PASS tracks a different reputation than example.com:NEUTRAL.)
    • The pydns package provides the low level DNS library for python DNS lookups. It is much smaller and lighter than the more capable (and bigger) dnspython library. Low level lookups are needed to find SPF and MX records for instance.
    • The pydspam package wraps an old version of libdspam for python. The C API changed dramatically for new versions, and I haven't gotten things updated yet. Another content filter might be in order.

    At the lowest level, the milter module provides a thin wrapper around the sendmail libmilter API. This API lets you register callbacks for a number of events in the process of sendmail receiving a message via SMTP. These events include the initial connection from a MTA, the envelope sender and recipients, the top level mail headers, and the message body. There are options to mangle all of these components of the message as it passes through the milter.

    At the next level, the Milter module (note the case difference) provides a Python friendly object oriented wrapper for the low level API. To use the Milter module, an application registers a 'factory' to create an object for each connection from a MTA to sendmail. These connection objects must provide methods corresponding to the libmilter callback events.

    Each event method returns a code to tell sendmail whether to proceed with processing the message. This is a big advantage of milters over other mail filtering systems. Unwanted mail can be stopped in its tracks at the earliest possible point.

    The Milter.Milter class provides default implementations for event methods that do nothing, and also provides wrappers for the libmilter methods to mutate the message.

    The mime module provides a wrapper for the Python email package that fixes some bugs, and simplifies modifying selected parts of a MIME message.

    Finally, the bms.py application is both a sample of how to use the Milter and spf modules, and the beginnings of a general purpose SPAM filtering, wiretapping, SPF checking, and Win32 virus protecting milter. It can make use of the pysrs package when available for SRS/SES checking and the pydspam package for Bayesian content filtering. SPF checking requires pydns. Configuration documentation is currently included as comments in the sample config file for the bms.py milter. See also the HOWTO and Milter Log Message Tags.

    Python milter is under GPL. The authors can probably be convinced to change this to LGPL if needed.

    What is a milter?

    Milters can run on the same machine as sendmail, or another machine. The milter can even run with a different operating system or processor than sendmail. Sendmail talks to the milter via a local or internet socket. Sendmail keeps the milter informed of events as it processes a mail connection. At any point, the milter can cut the conversation short by telling sendmail to ACCEPT, REJECT, or DISCARD the message. After receiving a complete message from sendmail, the milter can again REJECT or DISCARD it, but it can also ACCEPT it with changes to the headers or body.

    What can you do with a milter?

  • A milter can DISCARD or REJECT spam based based on algorithms scripted in python rather than sendmail's cryptic "cf" language.
  • A milter can alter or remove attachments from mail that are poisonous to Windows.
  • A milter can scan for viruses and clean them when detected.
  • A milter scans outgoing as well as incoming mail.
  • A milter can add and delete recipients to forward or secretly copy mail.
  • For more ideas, check the Milter Web Page.
  • Documentation for the C API is provided with sendmail. Documentation for pymilter is provided via Doxygen. Miltermodule provides a thin python wrapper for the C API. Milter.py provides a simple OO wrapper on top of that.

    The Python milter package includes a sample milter that replaces dangerous attachments with a warning message, discards mail addressed to MAILER-DAEMON, and demonstrates several SPAM abatement strategies. The MimeMessage class to do this used to be based on the mimetools and multifile standard python packages. As of milter version 0.6.0, it is based on the email standard python packages, which were derived from the mimelib project. The MimeMessage class patches several bugs in the email package, and provides some backward compatibility.

    The "defang" function of the sample milter was inspired by MIMEDefang, a Perl milter with flexible attachment processing options. The latest version of MIMEDefang uses an apache style process pool to avoid reloading the Perl interpreter for each message. This makes it fast enough for production without using Perl threading.

    mailchecker is a Python project to provide flexible attachment processing for mail. I will be looking at plugging mailchecker into a milter.

    TMDA is a Python project to require confirmation the first time someone tries to send to your mailbox. This would be a nice feature to have in a milter.

    There is also a Milter community website where milter software and gory details of the API are discussed.

    Is a milter written in python efficient?

    The python milter process is multi-threaded and startup cost is incurred only once. This is much more efficient than some implementations that start a new interpreter for each connection. Testing in a production environment did not use a significant percentage of the CPU. Furthermore, python is easily extended in C for any step requiring expensive CPU processing.

    For example, the HTML parsing feature to remove scripts from HTML attachments is rather CPU intensive in pure python. Using the C replacement for sgmllib greatly speeds things up.

    Goals

  • Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS forwarder accounts (perhaps in ~/.forwarders), and a util provides a special local alias for the user to give to the forwarder. Alias only works for mail from that forwarder. Milter gets forwarder domain from alias and uses it to SPF check forwarder. Requires milter to have read access to ~/.forwarders or else a way for user to submit entries to milter database.
  • The bms.py milter has too many features. Create a framework where numerous small feature modules can be plugged together in the configuration.
  • Create a pure python substitute for miltermodule and libmilter that implements the libmilter protocol in python. Someone has already started this at Google code.
  • Find or write a faster implementation of sgmllib. The sgmlop package is not very compatible with Python-2.1 sgmllib, but it is a start, and is supported in milter-0.4.5 or later.
  • Implement all or most of the features of MIMEDefang.
  • Follow the official Python coding standards more closely.
  • Make unit test code more like other python modules.
  • Confirmed Installations

    Please email me if you do not successfully install milter. The confirmed installations are too numerous to list at this point.

    Enough Already!

    Nearly a dozen people have emailed me begging for a feature to copy outgoing and/or incoming mail to a backup directory by user. Ok, it looks like this is a most requested feature. In the meantime, here are some things to consider:
    • The milter package (bms.py) supports the mail_archive option in the [wiretap] section. This is not by user, however.
    • If you want to equivalent of a Bcc added to each message, this is very easy to do in the python code for bms.py. See below.
    • If you want to copy to a file in a directory (thus avoiding having to set up aliases), this is slightly more involved. The bms.py milter already copies the message to a temporary file for use in replacing the message body when banned attachments are found. You have to open a file, and copy the Mesage object to it in eom().
    • Finally, you are probably aware that most email clients already keep a copy of outgoing mail? Presumably there is a good reason for keeping another copy on the server.

    To Bcc a message, call self.add_recipient(rcpt) in envfrom after determining whether you want to copy (e.g. whether the sender is local). For example,

      def envfrom(...
        ...
        if len(t) == 2:
          self.rejectvirus = t[1] in reject_virus_from
          if t[0] in wiretap_users.get(t[1],()):
    	self.add_recipient(wiretap_dest)
          if t[1] == 'mydomain.com':
            self.add_recipient('<copy-%s>' % t[0])
          ...
    

    To make this a generic feature requires thinking about how the configuration would look. Feel free to make specific suggestions about config file entries. Be sure to handle both Bcc and file copies, and designating what mail should be copied. How should "outgoing" be defined? Implementing it is easy once the configuration is designed.


     [ Valid HTML 3.2! ]  [ Powered By Red Hat Linux ]

    milter-0.8.18/doc/faq.ht0000644000160600001450000003100112006600003013615 0ustar stuartbmsTitle: Python Milter FAQ

    Python Milter FAQ

  • Compiling Python Milter
  • Running Python Milter
  • Using SPF
  • Using SRS
    1. Compiling Python Milter

    2. Q. I have tried to download the current milter code and my virus scan traps several viruses in the download.

      A. The milter source includes a number of deactivated viruses in the test directory. All but the first and last lines of the base64 encoded virus data has been removed. I suppose I should randomize the first and last lines as well, since pymilter just deletes executables, and doesn't look for signatures.

    3. Q. I have installed sendmail from source, but Python milter won't compile.

      A. Even though libmilter is officially supported in sendmail-8.12, you need to build and install it in separate steps. Take a look at the RPM spec file for sendmail-8.12. The %prep section shows you how to create a site.config.m4 that enables MILTER. The %build section shows you how to build libmilter in a separate invocation of make. The %install section shows you how to install libmilter with a separate invocation of make.

    4. Q. Why is mfapi.h not found when I try to compile Python milter on RedHat 7.2?

      A. RedHat forgot to include the header in the RPM. See the RedHat 7.2 requirements.

    5. Q. Python milter compiles ok, but I get an error like this when I try to import the milter module:
      ImportError: /usr/lib/python2.4/site-packages/milter.so: undefined symbol: smfi_setmlreply
      

      A. Your libmilter.a is from sendmail-8.12 or earlier. You need sendmail-8.13 or later to support setmlreply. You can disable setmlreply by changing setup.py. Change:

                  define_macros = [ ('MAX_ML_REPLY',32) ]
      
      in setup.py to
                  define_macros = [ ('MAX_ML_REPLY',1) ]
      

      Running Python Milter

    6. Q. The sample.py milter prints a message, then just sits there.
      To use this with sendmail, add the following to sendmail.cf:
      
      O InputMailFilters=pythonfilter
      Xpythonfilter,        S=local:inet:1030@localhost
      
      See the sendmail README for libmilter.
      sample  milter startup
      

      A. You need to tell sendmail to connect to your milter. The sample milter tells you what to add to your sendmail.cf to tell sendmail to use the milter. You can also add an INPUT_MAIL_FILTER macro to your sendmail.mc file and rebuild sendmail.cf - see the sendmail README for milters.

    7. Q. I've configured sendmail properly, but still nothing happens when I send myself mail!

      A. Sendmail only milters SMTP mail. Local mail is not miltered. You can pipe a raw message through sendmail to test your milter:

      $ cat rawtextmsg | sendmail myname@my.full.domain
      
      Now check your milter log.

    8. Q. Why do I get this ImportError exception?
      File "mime.py", line 370, in ?
          from sgmllib import declstringlit, declname
          ImportError: cannot import name declstringlit
      

      A. declstringlit is not provided by sgmllib in all versions of python. For instance, python-2.2 does not have it. Upgrade to milter-0.4.5 or later to remove this dependency.

    9. Q. Why do I get milter.error: cannot add recipient?
      
      

      A. You must tell libmilter how you might mutate the message with set_flags() before calling runmilter(). For instance, Milter.set_flags(Milter.ADDRCPT). You must add together all of ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS that apply.

      NOTE - recent versions default flags to enabling all features. You must now call set_flags() if you wish to disable features for efficiency.

    10. Q. Why does sendmail sometimes print something like: "...write(D) returned -1, expected 5: Broken pipe" in the sendmail log?

      A. Libmilter expects "rcpt to" shortly after getting "mail from". "Shortly" is defined by the timeout parameter you passed to Milter.runmilter() or milter.settimeout(). If the timeout is 10 seconds, and looking up the first recipient in DNS takes more than 10 seconds, libmilter will give up and break the connection. Milter.runmilter() defaulted to 10 seconds in 0.3.4. In 0.3.5 it will keep the libmilter default of 2 hours.

    11. Q. Why does milter block messages with big5 encoding? What if I want to receive them?

      A. sample.py is a sample. It is supposed to be easily modified for your specific needs. We will of course continue to move generic code out of the sample as the project evolves. Think of sample.py as an active config file.

      If you are running bms.py, then the block_chinese option in /etc/mail/pymilter.cfg controls this feature.

    12. Q. Why does sendmail coredump with milters on OpenBSD?

      A. Sendmail has a problem with unix sockets on old versions of OpenBSD. OpenBSD users report that this problem has been fixed, so upgrading OpenBSD will fix this. Otherwise, you can use an internet domain socket instead. For example, in sendmail.cf use

      Xpythonfilter, S=inet:1234@localhost
      
      and change sample.py accordingly.

    13. Q. How can I change the bounce message for an invalid recipient? I can only change the recipient in the eom callback, but the eom callback is never called when the recipient is invalid!

      A. For sendmail-8.13 and later, use pymilter-0.9.3 and clear Milter.P_RCPT_REJ in the _protocol_mask class var:

      class myMilter(Milter.Base):
        def envrcpt(self,to,*params):
            return Milter.CONTINUE
      myMilter._protocol_mask = myMilter.protocol_mask() & ~Milter.P_RCPT_REJ
      
      For sendmail-8.12 and earlier, configure sendmail to use virtusertable, and send all unknown addresses to /dev/null. For example,

      /etc/mail/virtusertable

      @mycorp.com	dev-null
      dan@mycorp.com	dan
      sally@mycorp.com	sally
      

      /etc/aliases

      dev-null:	/dev/null
      
      Now your milter will get to the eom callback, and can change the envelope recipient at will. Thanks to Dredd at milter.org for this solution.

    14. Q. I am having trouble with the setreply method. It always outputs "milter.error: cannot set reply".

      A. Check the sendmail log for errors. If sendmail is getting milter timeouts, then your milter is taking too long and sendmail gave up waiting. You can adjust the timeouts in your sendmail config. Here is a milter declaration for sendmail.cf with all timeouts specified:

      Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
      
    15. Q. Once I feed my milter a valid address (which returns Milter.ACCEPT from the envrcpt() method) the envrcpt() method is no longer called. Is there something I can do to change this behavior?

      A. Return Milter.CONTINUE instead of Milter.ACCEPT from envrcpt().

    16. Q. There is a Python traceback in the log file! What happened to my email?

      A. By default, when the milter fails with an untrapped exception, a TEMPFAIL result (451) is returned to the sender. The sender will then retry every hour or so for several days. Hopefully, someone will notice the traceback, and workaround or fix the problem. Beginning with milter-0.8.2, you can call milter.set_exception_policy(milter.CONTINUE) to cause an untrapped exception to continue processing with the next callback or milter instead. For completeness, you can also set the exception policy to milter.REJECT.

    17. Q. I read some notes such as "Check valid domains allowed by internal senders to detect PCs infected with spam trojans." but could not understand the idea. Could you clarify the content ?

      A. The internal_domains configuration specifies which MAIL FROM domains are used by internal connections. If an internal PC tries to use some other domain, it is assumed to be a "Zombie".

      Here is a sample log line:

      2005Jun22 12:01:04 [12430] REJECT: zombie PC at  192.168.100.171  sending MAIL FROM  debby@fedex.com
      
      No, fedex.com does not use pymilter, and there is no one named debby at my client. But the idiot using the PC at 192.168.100.171 has downloaded and installed some stupid weatherbar/hotbar/aquariumscreensaver that is actually a spam bot.

      The internal_domains option is simplistic, it assumes all valid senders of the domains are internal. SPF provides a much more general check of IP and MAIL FROM for external email. Pymilter should soon have a local policy feature for more general checking of internal mail.

    18. Q. mail_archive isn't working. Or I don't understand how it's suppose to work. I have mail_archive = /var/mail/mail_archive in pymilter.cfg but nothing ever gets dumped into /var/mail/mail_archive.

      A. The 'mail' user needs to have write access. Permission failures should be logged as a traceback in milter.log if it doesn't.

      Using SPF

    19. Q. So how do I use the SPF support? The sample.py milter doesn't seem to use it.

      A. The milter package contains several more useful milters. The spfmilter.py milter checks SPF. The bms.py milter supports spf and too many other things. The RedHat RPMs will set almost everything up for you. For other systems:

      1. Arrange to run spfmilter.py or bms.py in the background (as a service perhaps) and redirect output and errors to a logfile. For instance, on AIX you'll want to use SRC (System Resource Controller).
      2. Copy spfmilter.cfg or pymilter.cfg to /etc/mail or the directory you run bms.py in, and edit it. The comments should explain the options.
      3. Start spfmilter.py or bms.py in the background as arranged.
      4. Add Xpythonfilter (or whatever you configured as miltername) to sendmail.cf or add an INPUT_MAIL_FILTER to sendmail.mc. Regen sendmail.cf if you use sendmail.mc and restart sendmail.
      5. Arrange to rotate log files and remove old defang files in tempdir. The RedHat RPM uses logrotate for logfiles and a simple cron script using find to clean tempdir.

      spfmilter.py runs as a service, and does just SPF. It uses the sendmail access file to configure SPF responses just like bms.py, but supports only REJECT and OK.

    20. Q. bms.py sends the SPF DSN at least once for domains that don't publish a SPF. How do I stop this behavior?

      A. The SPF response is controlled by /etc/mail/access (actually the file you specify with access_file in the [spf] section of pymilter.cfg). Responses are OK, CBV, DSN, and REJECT. DSN sends the DSN.

      You can change the defaults. For instance, I have:

      SPF-None:	REJECT
      SPF-Neutral:	CBV
      SPF-Softfail:	DSN
      SPF-Permerror:	DSN
      
      I have best_guess = 1, so SPF none is converted to PASS/NEUTRAL for policy lookup, and 3 strikes (no PTR, no HELO, no SPF) becomes "SPF NONE" for local policy purposes (the Received-SPF header always shows the official SPF result.)

      You can change the default for specific domains:

      # these guys aren't going to pay attention to CBVs anyway...
      SPF-None:cia.gov	REJECT
      SPF-None:fbi.gov	REJECT
      SPF-Neutral:aol.com	REJECT
      SPF-Softfail:ebay.com	REJECT
      

      Using SRS

    21. Q. The SRS part doesn't seem to work as whenever I try to start /etc/init.d/pysrs, I get this in /var/log/milter/pysrs.log:
      ConfigParser.NoOptionError: No option 'fwdomain' in section: 'srs'
      

      A. You need to specify the forward domain - i.e. the domain you want SRS to rewrite stuff to.

      For instance, I have:

      # sample SRS configuration
      [srs]
      secret = don't you wish
      maxage = 8
      hashlength = 5
      ;database=/var/log/milter/srs.db
      fwdomain = bmsi.com
      sign=bmsi.com,mail.bmsi.com,gathman.org
      srs=bmsaix.bmsi.com,bmsred.bmsi.com,stl.gathman.org,bampa.gathman.org
      
      The sign is for local domains which are signed. The srs list is for other domains which you are relaying, and which need to have SRS checked/undone for bounces.
    milter-0.8.18/doc/milter.html0000644000160600001450000005062612120724406014724 0ustar stuartbms Python Milters
      

    Viewable With Any Browser Your vote? I Disagree I Agree

    Sendmail/Postfix Milters in Python

    by Jim Niemira and Stuart D. Gathman
    This web page is written by Stuart D. Gathman
    and
    sponsored by Business Management Systems, Inc.
    (see LICENSE for copying permissions for this documentation)
    Last updated Aug 08, 2012

    Maxwell's Daemon: pymilter mascot Mascot by students of Christian Hafner See the FAQ | Download now | Support | Overview | pydspam | libdspam

    Sendmail introduced a new API beginning with version 8.10 - libmilter. Sendmail 8.12 officially released libmilter. Version 8.12 seems to be more robust, and includes new privilege separation features to enhance security. Even better, sendmail 8.13 supports socket maps, which makes pysrs much more efficient and secure. Sendmail 8.14 finally supports modifying MAIL FROM via the milter API, and a data callback allowing spam to be rejected before beginning the DATA phase (even after accepting some recipients).
    A Python Pymilter provides a milter module for Python that implements a python interface to libmilter exploiting all its features.
    A Postmark Now Postfix also implements the milter protocol, and you can program SMTP time filters for Postfix in Python.

    What's New

    • milter 0.8.16 has dkim signing, and Authentication-Results header. pymilter-0.9.7 has several improved diagnostics for milter programming errors.
    • milter has dkim checking and logging in CVS. Will use DKIM Pass for reputation tracking, and as an additional acceptable identity along with HELO, PTR, or SPF.
    • pymilter-0.9.4 supports python-2.6
    • pymilter-0.9.2 supports the negotiate, data, and unknown callbacks. Protocol steps are automatically negotiated by the high-level Milter package by annotating callback methods with @nocallback or @noreply.
    • pymilter-0.9.1 supports CHGFROM, introduced with sendmail-8.14, and also supported by postfix-2.3.

    Support

    • pymilter mailing list
    • SPF forums and chat for SPF questions
    • IRC channel: #dkim on irc.perl.org for DKIM questions
    • IRC channel: #pymilter on irc.freenode.net for pymilter questions

      You may be required to register your user nickname (nick) and identify with that nick. Otherwise, you may not be able to join or be heard on the IRC channel. There is a page describing how to register your nick at freenode.net.

    Overview

    To accomodate other open source projects using pymilter, this package has been shedding modules which can be used by other packages.
    • The pymilter package provides a robust toolkit for Python milters that wraps the C libmilter library. There is also a pure Python milter library that implements the milter protocol in Python.
    • The milter package provides the beginnings of a general purpose mail filtering system written in Python. It also includes a simple spfmilter that supports policy by domain and spf result via the sendmail access file.
    • The pysrs package provides an SRS library, SES library, a sendmail socketmap daemon implementing SRS, and (Real Soon Now) an srsmilter daemon implementing SRS, now that sendmail-8.14 supports CHGFROM and this is supported in pymilter-0.9.
    • The pyspf package provides the spf module, a well tested implementation of the of the SPF protocol, which is useful for detecting email forgery.
    • The pygossip package provides the gossip library and server daemon for the GOSSiP protocol, which exchanges reputation of qualified domains. (Qualified in the milter package means that example.com:PASS tracks a different reputation than example.com:NEUTRAL.)
    • The pydns package provides the low level DNS library for python DNS lookups. It is much smaller and lighter than the more capable (and bigger) dnspython library. Low level lookups are needed to find SPF and MX records for instance.
    • The pydspam package wraps an old version of libdspam for python. The C API changed dramatically for new versions, and I haven't gotten things updated yet. Another content filter might be in order.

    At the lowest level, the milter module provides a thin wrapper around the sendmail libmilter API. This API lets you register callbacks for a number of events in the process of sendmail receiving a message via SMTP. These events include the initial connection from a MTA, the envelope sender and recipients, the top level mail headers, and the message body. There are options to mangle all of these components of the message as it passes through the milter.

    At the next level, the Milter module (note the case difference) provides a Python friendly object oriented wrapper for the low level API. To use the Milter module, an application registers a 'factory' to create an object for each connection from a MTA to sendmail. These connection objects must provide methods corresponding to the libmilter callback events.

    Each event method returns a code to tell sendmail whether to proceed with processing the message. This is a big advantage of milters over other mail filtering systems. Unwanted mail can be stopped in its tracks at the earliest possible point.

    The Milter.Milter class provides default implementations for event methods that do nothing, and also provides wrappers for the libmilter methods to mutate the message.

    The mime module provides a wrapper for the Python email package that fixes some bugs, and simplifies modifying selected parts of a MIME message.

    Finally, the bms.py application is both a sample of how to use the Milter and spf modules, and the beginnings of a general purpose SPAM filtering, wiretapping, SPF checking, and Win32 virus protecting milter. It can make use of the pysrs package when available for SRS/SES checking and the pydspam package for Bayesian content filtering. SPF checking requires pydns. Configuration documentation is currently included as comments in the sample config file for the bms.py milter. See also the HOWTO and Milter Log Message Tags.

    Python milter is under GPL. The authors can probably be convinced to change this to LGPL if needed.

    What is a milter?

    Milters can run on the same machine as sendmail, or another machine. The milter can even run with a different operating system or processor than sendmail. Sendmail talks to the milter via a local or internet socket. Sendmail keeps the milter informed of events as it processes a mail connection. At any point, the milter can cut the conversation short by telling sendmail to ACCEPT, REJECT, or DISCARD the message. After receiving a complete message from sendmail, the milter can again REJECT or DISCARD it, but it can also ACCEPT it with changes to the headers or body.

    What can you do with a milter?

  • A milter can DISCARD or REJECT spam based based on algorithms scripted in python rather than sendmail's cryptic "cf" language.
  • A milter can alter or remove attachments from mail that are poisonous to Windows.
  • A milter can scan for viruses and clean them when detected.
  • A milter scans outgoing as well as incoming mail.
  • A milter can add and delete recipients to forward or secretly copy mail.
  • For more ideas, check the Milter Web Page.
  • Documentation for the C API is provided with sendmail. Documentation for pymilter is provided via Doxygen. Miltermodule provides a thin python wrapper for the C API. Milter.py provides a simple OO wrapper on top of that.

    The Python milter package includes a sample milter that replaces dangerous attachments with a warning message, discards mail addressed to MAILER-DAEMON, and demonstrates several SPAM abatement strategies. The MimeMessage class to do this used to be based on the mimetools and multifile standard python packages. As of milter version 0.6.0, it is based on the email standard python packages, which were derived from the mimelib project. The MimeMessage class patches several bugs in the email package, and provides some backward compatibility.

    The "defang" function of the sample milter was inspired by MIMEDefang, a Perl milter with flexible attachment processing options. The latest version of MIMEDefang uses an apache style process pool to avoid reloading the Perl interpreter for each message. This makes it fast enough for production without using Perl threading.

    mailchecker is a Python project to provide flexible attachment processing for mail. I will be looking at plugging mailchecker into a milter.

    TMDA is a Python project to require confirmation the first time someone tries to send to your mailbox. This would be a nice feature to have in a milter.

    There is also a Milter community website where milter software and gory details of the API are discussed.

    Is a milter written in python efficient?

    The python milter process is multi-threaded and startup cost is incurred only once. This is much more efficient than some implementations that start a new interpreter for each connection. Testing in a production environment did not use a significant percentage of the CPU. Furthermore, python is easily extended in C for any step requiring expensive CPU processing.

    For example, the HTML parsing feature to remove scripts from HTML attachments is rather CPU intensive in pure python. Using the C replacement for sgmllib greatly speeds things up.

    Goals

  • Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS forwarder accounts (perhaps in ~/.forwarders), and a util provides a special local alias for the user to give to the forwarder. Alias only works for mail from that forwarder. Milter gets forwarder domain from alias and uses it to SPF check forwarder. Requires milter to have read access to ~/.forwarders or else a way for user to submit entries to milter database.
  • The bms.py milter has too many features. Create a framework where numerous small feature modules can be plugged together in the configuration.
  • Create a pure python substitute for miltermodule and libmilter that implements the libmilter protocol in python. Someone has already started this at Google code.
  • Find or write a faster implementation of sgmllib. The sgmlop package is not very compatible with Python-2.1 sgmllib, but it is a start, and is supported in milter-0.4.5 or later.
  • Implement all or most of the features of MIMEDefang.
  • Follow the official Python coding standards more closely.
  • Make unit test code more like other python modules.
  • Confirmed Installations

    Please email me if you do not successfully install milter. The confirmed installations are too numerous to list at this point.

    Enough Already!

    Nearly a dozen people have emailed me begging for a feature to copy outgoing and/or incoming mail to a backup directory by user. Ok, it looks like this is a most requested feature. In the meantime, here are some things to consider:
    • The milter package (bms.py) supports the mail_archive option in the [wiretap] section. This is not by user, however.
    • If you want to equivalent of a Bcc added to each message, this is very easy to do in the python code for bms.py. See below.
    • If you want to copy to a file in a directory (thus avoiding having to set up aliases), this is slightly more involved. The bms.py milter already copies the message to a temporary file for use in replacing the message body when banned attachments are found. You have to open a file, and copy the Mesage object to it in eom().
    • Finally, you are probably aware that most email clients already keep a copy of outgoing mail? Presumably there is a good reason for keeping another copy on the server.

    To Bcc a message, call self.add_recipient(rcpt) in envfrom after determining whether you want to copy (e.g. whether the sender is local). For example,

      def envfrom(...
        ...
        if len(t) == 2:
          self.rejectvirus = t[1] in reject_virus_from
          if t[0] in wiretap_users.get(t[1],()):
    	self.add_recipient(wiretap_dest)
          if t[1] == 'mydomain.com':
            self.add_recipient('<copy-%s>' % t[0])
          ...
    

    To make this a generic feature requires thinking about how the configuration would look. Feel free to make specific suggestions about config file entries. Be sure to handle both Bcc and file copies, and designating what mail should be copied. How should "outgoing" be defined? Implementing it is easy once the configuration is designed.


     [ Valid HTML 3.2! ]  [ Powered By Red Hat Linux ]

    milter-0.8.18/doc/faq.html0000644000160600001450000004112012120724406014164 0ustar stuartbms Python Milter FAQ
      

    Python Milter FAQ

  • Compiling Python Milter
  • Running Python Milter
  • Using SPF
  • Using SRS
    1. Compiling Python Milter

    2. Q. I have tried to download the current milter code and my virus scan traps several viruses in the download.

      A. The milter source includes a number of deactivated viruses in the test directory. All but the first and last lines of the base64 encoded virus data has been removed. I suppose I should randomize the first and last lines as well, since pymilter just deletes executables, and doesn't look for signatures.

    3. Q. I have installed sendmail from source, but Python milter won't compile.

      A. Even though libmilter is officially supported in sendmail-8.12, you need to build and install it in separate steps. Take a look at the RPM spec file for sendmail-8.12. The %prep section shows you how to create a site.config.m4 that enables MILTER. The %build section shows you how to build libmilter in a separate invocation of make. The %install section shows you how to install libmilter with a separate invocation of make.

    4. Q. Why is mfapi.h not found when I try to compile Python milter on RedHat 7.2?

      A. RedHat forgot to include the header in the RPM. See the RedHat 7.2 requirements.

    5. Q. Python milter compiles ok, but I get an error like this when I try to import the milter module:
      ImportError: /usr/lib/python2.4/site-packages/milter.so: undefined symbol: smfi_setmlreply
      

      A. Your libmilter.a is from sendmail-8.12 or earlier. You need sendmail-8.13 or later to support setmlreply. You can disable setmlreply by changing setup.py. Change:

                  define_macros = [ ('MAX_ML_REPLY',32) ]
      
      in setup.py to
                  define_macros = [ ('MAX_ML_REPLY',1) ]
      

      Running Python Milter

    6. Q. The sample.py milter prints a message, then just sits there.
      To use this with sendmail, add the following to sendmail.cf:
      
      O InputMailFilters=pythonfilter
      Xpythonfilter,        S=local:inet:1030@localhost
      
      See the sendmail README for libmilter.
      sample  milter startup
      

      A. You need to tell sendmail to connect to your milter. The sample milter tells you what to add to your sendmail.cf to tell sendmail to use the milter. You can also add an INPUT_MAIL_FILTER macro to your sendmail.mc file and rebuild sendmail.cf - see the sendmail README for milters.

    7. Q. I've configured sendmail properly, but still nothing happens when I send myself mail!

      A. Sendmail only milters SMTP mail. Local mail is not miltered. You can pipe a raw message through sendmail to test your milter:

      $ cat rawtextmsg | sendmail myname@my.full.domain
      
      Now check your milter log.

    8. Q. Why do I get this ImportError exception?
      File "mime.py", line 370, in ?
          from sgmllib import declstringlit, declname
          ImportError: cannot import name declstringlit
      

      A. declstringlit is not provided by sgmllib in all versions of python. For instance, python-2.2 does not have it. Upgrade to milter-0.4.5 or later to remove this dependency.

    9. Q. Why do I get milter.error: cannot add recipient?
      
      

      A. You must tell libmilter how you might mutate the message with set_flags() before calling runmilter(). For instance, Milter.set_flags(Milter.ADDRCPT). You must add together all of ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS that apply.

      NOTE - recent versions default flags to enabling all features. You must now call set_flags() if you wish to disable features for efficiency.

    10. Q. Why does sendmail sometimes print something like: "...write(D) returned -1, expected 5: Broken pipe" in the sendmail log?

      A. Libmilter expects "rcpt to" shortly after getting "mail from". "Shortly" is defined by the timeout parameter you passed to Milter.runmilter() or milter.settimeout(). If the timeout is 10 seconds, and looking up the first recipient in DNS takes more than 10 seconds, libmilter will give up and break the connection. Milter.runmilter() defaulted to 10 seconds in 0.3.4. In 0.3.5 it will keep the libmilter default of 2 hours.

    11. Q. Why does milter block messages with big5 encoding? What if I want to receive them?

      A. sample.py is a sample. It is supposed to be easily modified for your specific needs. We will of course continue to move generic code out of the sample as the project evolves. Think of sample.py as an active config file.

      If you are running bms.py, then the block_chinese option in /etc/mail/pymilter.cfg controls this feature.

    12. Q. Why does sendmail coredump with milters on OpenBSD?

      A. Sendmail has a problem with unix sockets on old versions of OpenBSD. OpenBSD users report that this problem has been fixed, so upgrading OpenBSD will fix this. Otherwise, you can use an internet domain socket instead. For example, in sendmail.cf use

      Xpythonfilter, S=inet:1234@localhost
      
      and change sample.py accordingly.

    13. Q. How can I change the bounce message for an invalid recipient? I can only change the recipient in the eom callback, but the eom callback is never called when the recipient is invalid!

      A. For sendmail-8.13 and later, use pymilter-0.9.3 and clear Milter.P_RCPT_REJ in the _protocol_mask class var:

      class myMilter(Milter.Base):
        def envrcpt(self,to,*params):
            return Milter.CONTINUE
      myMilter._protocol_mask = myMilter.protocol_mask() & ~Milter.P_RCPT_REJ
      
      For sendmail-8.12 and earlier, configure sendmail to use virtusertable, and send all unknown addresses to /dev/null. For example,

      /etc/mail/virtusertable

      @mycorp.com	dev-null
      dan@mycorp.com	dan
      sally@mycorp.com	sally
      

      /etc/aliases

      dev-null:	/dev/null
      
      Now your milter will get to the eom callback, and can change the envelope recipient at will. Thanks to Dredd at milter.org for this solution.

    14. Q. I am having trouble with the setreply method. It always outputs "milter.error: cannot set reply".

      A. Check the sendmail log for errors. If sendmail is getting milter timeouts, then your milter is taking too long and sendmail gave up waiting. You can adjust the timeouts in your sendmail config. Here is a milter declaration for sendmail.cf with all timeouts specified:

      Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
      
    15. Q. Once I feed my milter a valid address (which returns Milter.ACCEPT from the envrcpt() method) the envrcpt() method is no longer called. Is there something I can do to change this behavior?

      A. Return Milter.CONTINUE instead of Milter.ACCEPT from envrcpt().

    16. Q. There is a Python traceback in the log file! What happened to my email?

      A. By default, when the milter fails with an untrapped exception, a TEMPFAIL result (451) is returned to the sender. The sender will then retry every hour or so for several days. Hopefully, someone will notice the traceback, and workaround or fix the problem. Beginning with milter-0.8.2, you can call milter.set_exception_policy(milter.CONTINUE) to cause an untrapped exception to continue processing with the next callback or milter instead. For completeness, you can also set the exception policy to milter.REJECT.

    17. Q. I read some notes such as "Check valid domains allowed by internal senders to detect PCs infected with spam trojans." but could not understand the idea. Could you clarify the content ?

      A. The internal_domains configuration specifies which MAIL FROM domains are used by internal connections. If an internal PC tries to use some other domain, it is assumed to be a "Zombie".

      Here is a sample log line:

      2005Jun22 12:01:04 [12430] REJECT: zombie PC at  192.168.100.171  sending MAIL FROM  debby@fedex.com
      
      No, fedex.com does not use pymilter, and there is no one named debby at my client. But the idiot using the PC at 192.168.100.171 has downloaded and installed some stupid weatherbar/hotbar/aquariumscreensaver that is actually a spam bot.

      The internal_domains option is simplistic, it assumes all valid senders of the domains are internal. SPF provides a much more general check of IP and MAIL FROM for external email. Pymilter should soon have a local policy feature for more general checking of internal mail.

    18. Q. mail_archive isn't working. Or I don't understand how it's suppose to work. I have mail_archive = /var/mail/mail_archive in pymilter.cfg but nothing ever gets dumped into /var/mail/mail_archive.

      A. The 'mail' user needs to have write access. Permission failures should be logged as a traceback in milter.log if it doesn't.

      Using SPF

    19. Q. So how do I use the SPF support? The sample.py milter doesn't seem to use it.

      A. The milter package contains several more useful milters. The spfmilter.py milter checks SPF. The bms.py milter supports spf and too many other things. The RedHat RPMs will set almost everything up for you. For other systems:

      1. Arrange to run spfmilter.py or bms.py in the background (as a service perhaps) and redirect output and errors to a logfile. For instance, on AIX you'll want to use SRC (System Resource Controller).
      2. Copy spfmilter.cfg or pymilter.cfg to /etc/mail or the directory you run bms.py in, and edit it. The comments should explain the options.
      3. Start spfmilter.py or bms.py in the background as arranged.
      4. Add Xpythonfilter (or whatever you configured as miltername) to sendmail.cf or add an INPUT_MAIL_FILTER to sendmail.mc. Regen sendmail.cf if you use sendmail.mc and restart sendmail.
      5. Arrange to rotate log files and remove old defang files in tempdir. The RedHat RPM uses logrotate for logfiles and a simple cron script using find to clean tempdir.

      spfmilter.py runs as a service, and does just SPF. It uses the sendmail access file to configure SPF responses just like bms.py, but supports only REJECT and OK.

    20. Q. bms.py sends the SPF DSN at least once for domains that don't publish a SPF. How do I stop this behavior?

      A. The SPF response is controlled by /etc/mail/access (actually the file you specify with access_file in the [spf] section of pymilter.cfg). Responses are OK, CBV, DSN, and REJECT. DSN sends the DSN.

      You can change the defaults. For instance, I have:

      SPF-None:	REJECT
      SPF-Neutral:	CBV
      SPF-Softfail:	DSN
      SPF-Permerror:	DSN
      
      I have best_guess = 1, so SPF none is converted to PASS/NEUTRAL for policy lookup, and 3 strikes (no PTR, no HELO, no SPF) becomes "SPF NONE" for local policy purposes (the Received-SPF header always shows the official SPF result.)

      You can change the default for specific domains:

      # these guys aren't going to pay attention to CBVs anyway...
      SPF-None:cia.gov	REJECT
      SPF-None:fbi.gov	REJECT
      SPF-Neutral:aol.com	REJECT
      SPF-Softfail:ebay.com	REJECT
      

      Using SRS

    21. Q. The SRS part doesn't seem to work as whenever I try to start /etc/init.d/pysrs, I get this in /var/log/milter/pysrs.log:
      ConfigParser.NoOptionError: No option 'fwdomain' in section: 'srs'
      

      A. You need to specify the forward domain - i.e. the domain you want SRS to rewrite stuff to.

      For instance, I have:

      # sample SRS configuration
      [srs]
      secret = don't you wish
      maxage = 8
      hashlength = 5
      ;database=/var/log/milter/srs.db
      fwdomain = bmsi.com
      sign=bmsi.com,mail.bmsi.com,gathman.org
      srs=bmsaix.bmsi.com,bmsred.bmsi.com,stl.gathman.org,bampa.gathman.org
      
      The sign is for local domains which are signed. The srs list is for other domains which you are relaying, and which need to have SRS checked/undone for bounces.
    milter-0.8.18/permerror.txt0000644000160600001450000000161410575570465014564 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: Critical SPF configuration error Auto-Submitted: auto-generated (configuration error) This is an automatically generated Delivery Status Notification. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Received-SPF: %(spf_result)s Your spf record has a permanent error. The error was: %(perm_error)s We will reinterpret your record using "lax" processing heuristics which may result in your mail being accepted anyway. But you or your mail administrator need to fix your SPF record as soon as possible. We are sending you this message to alert you to the fact that you have problems with your email configuration. If you need further assistance, please do not hesitate to contact me again. Kind regards, postmaster@%(receiver)s milter-0.8.18/milter.rc0000755000160600001450000000414111102755317013615 0ustar stuartbms#!/bin/bash # # milter This shell script takes care of starting and stopping milter. # # chkconfig: 2345 80 30 # description: Milter is a process that filters messages sent through sendmail. # processname: milter # config: /etc/mail/pymilter.cfg # pidfile: /var/run/milter/milter.pid python="python2.4" pidof() { set - "" if set - `ps -e -o pid,cmd | grep "${python} bms.py"` && [ "$2" != "grep" ]; then echo $1 return 0 fi return 1 } # Source function library. . /etc/rc.d/init.d/functions [ -x /usr/lib/pymilter/start.sh ] || exit 0 RETVAL=0 prog="milter" start() { # Start daemons. echo -n "Starting $prog: " if ! test -d /var/run/milter; then mkdir -p /var/run/milter chown mail:mail /var/run/milter fi daemon --check milter --user mail /usr/lib/pymilter/start.sh milter bms RETVAL=$? echo [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter return $RETVAL } stop() { # Stop daemons. echo -n "Shutting down $prog: " # Find pid. pid= base="milter" if [ -f /var/run/milter/milter.pid ]; then local line p read line < /var/run/milter/milter.pid for p in $line ; do [ -z "${p//[0-9]/}" -a -d "/proc/$p" ] && pid="$pid $p" done fi if test -n "$pid"; then checkpid $pid && kill "$pid" for i in 1 2 3 4 5 6 7 8 9 0; do checkpid $pid && sleep 2 || break done if checkpid $pid; then failure $"$base shutdown" RETVAL=1 else success $"$base shutdown" RETVAL=0 fi else killproc -d 9 milter RETVAL=$? fi echo [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter return $RETVAL } # See how we were called. case "$1" in start) start ;; stop) stop ;; restart|reload) stop start RETVAL=$? ;; condrestart) if [ -f /var/lock/subsys/milter ]; then stop start RETVAL=$? fi ;; status) status milter RETVAL=$? ;; *) echo "Usage: $0 {start|stop|restart|condrestart|status}" exit 1 esac exit $RETVAL milter-0.8.18/dkim-milter.cfg0000644000160600001450000000211711750377027014677 0ustar stuartbms[milter] # The socket used to communicate with sendmail socketname = /var/run/milter/dkimmiltersock # Name of the milter given to sendmail name = pydkimfilter # Mail from internal networks is DKIM signed. # NOTE: SMTP AUTH connections are also considered internal. internal_connect = 127.0.0.1,192.168.0.0/16,10.0.0.0/8 datadir = /var/log/milter [dkim] privkey = /etc/mail/dkim_rsa selector = default # Domain for DKIM signature domain = example.com [loggers] keys=root,dkim-milter [handlers] keys=syslogHandler,fileHandler [formatters] keys=milterFormatter [logger_root] level=DEBUG handlers=syslogHandler [logger_dkim-milter] level=INFO ;handlers=syslogHandler handlers=fileHandler qualname=dkim-milter propagate=0 [handler_syslogHandler] class=handlers.SysLogHandler level=INFO formatter=milterFormatter args=(('localhost', handlers.SYSLOG_UDP_PORT), handlers.SysLogHandler.LOG_MAIL) [handler_fileHandler] class=FileHandler level=DEBUG formatter=milterFormatter args=('/var/log/milter/dkim-milter.log', 'a') [formatter_milterFormatter] format=%(asctime)s %(message)s datefmt=%Y%b%d %H:%M:%S milter-0.8.18/TODO0000644000160600001450000002363412121123111012447 0ustar stuartbmsThe recent feature to let a REJECT policy for SPF None be overridden by whitelisting is working for CSI and CMS. However, there could be a sender that we want to REJECT even when whitelisted - because they normally get a guessed PASS. Need another policy name - or else just add them to local SPF so they won't ever get 'None'. When policy is OK, do not use cbv_cache for blacklist. Add postmaster option or general rcpt list to dsn. Can send dsn to user and postmaster on the same connection. Support CBV to local domains and cache results so that invalid users can be rejected without maintaining valid user lists. Now that we blacklist IPs for too many bad rcpts, delay SPF until RCPT TO. When content filtering is not installed, reject BLACKLISTed MFROM immediately. There is no use waiting until EOM. Configuration is problematic when handling incoming, but not outgoing mail. The problem comes when alice@example.com sends mail to bill@example.com, and we are the MX for example.com, but alice is sending from some other MTA. The mail is flagged external, so we don't list example.com in internal_domains (or we would get "spam from self"). But, if we try to do a CBV, we get "fraudulent MX", because the MX is ourself! So we need to avoid doing CBV on such domains. Currently, we try to make sure the SPF policies don't do CBV. The real solution is for users to use SMTP AUTH, but some of them are stubborn. We now don't check internal domains for incoming mail if there is an SPF record. On the other hand, if alice is sending internally, or with SMTP AUTH, she *does* need the domain to be in internal_domains. The solution to that is to use the new SMTP AUTH access configuration to specify which domains can be used by smtp AUTH (by user if desired). It would be cleaner if CBV would know which domains we have agreed to be MX for. Some ideas for external connections: a) check access file for To:example.com RELAY b) check mailertable c) check mx_domains config list d) if there is an SPF record, don't check internal_domains (let SPF block unauthorized machines) But that still doesn't handle the roaming user, who won't use SMTP AUTH, but sends through some hotel MTA. Maybe we don't want to support him? When setting up pydspam, both sender and rcpt must resolve to dspam users for falsepositive recognition. Usually, this means adding honeypot@mail.example.com to alias list for honeypot in pymilter.cfg. This needs to be documented. I was caught by it setting up a new site. Add signature (x-sig=AB7485f=TS) to Received-SPF, so it can be used to blacklist sources of delayed DSNs. rcpt-addr may let us know when a recipient is unknown. That should count against reputation. Need to use wildcards in blacklist.log: *.madcowsrecord.net Need to exclude emails like !*-admin@example.com in whitelist_sender. Need to exclude robot users from autowhitelist. Don't want to have to list all users, so implement something like !*-admin@bmsi.com,@bmsi.com. GOSSiP feedback from user training is ignored because UMIS has already been removed from queue. Maybe keep UMIS in queue, and add method to alter last feedback for ID. Generate DSNs according to RFC 3464 Get temperror policy from access file. Reporting explanation for failure should show source if sender provided explanation. Bug in Auto-whitelist. Recent Auto-whitelist doesn't override expired entry. SPF permerror diagnostics should include corrected mechanism. Delay SPF check until RCPT TO. Cache result to avoid repeating for multiple RCPT. This avoids overhead for invalid RCPT, and allows for per RCPT local policy. Check SPF for outgoing mail (including local policy for internal addresses). This could also solve the second part of the mail from relay problem below. Whitelisted senders from trusted relay get PROBATION. Need to extracted SPF result from headers - and in the case of mail internal to relay (e.g. bmsi.com), supply 'pass' result. Add auto-blacklisted senders to blacklist.log with timestamp. Add emails blacklisted via CBV so that they are remembered across milter restarts. Make all dictionaries work like honeypot. Do not train as ham unless whitelisted. Train on blacklisted messages, or spam feedback. This can be called Train On Error. Should be possible to startup with training on everything to get dictionary built fast, then switch to train on error to minimize labor. Allow unsigned DSNs from selected domains (that don't accept signed MFROM, e.g. verizon.net). Allow verified hostnames for trusted_relay. E.g. HELO name that passes SPF. When do we get two hello calls? STARTTLS is one reason. Option: accept mail from auto-whitelisted senders even with spf-fail, but do not update dspam. This can be done for individual senders or domains using the access file. pysrs: SRS doesn't get applied to proper recipients when there are multiple recipients. This requires debugging cf scripts - yuk. auto_whitelist false_positives from quarantine - perhaps only when user selects special button (use special header to communicate that from dspamcgi.py to milter.) Use send_dsn.log for blacklist also. AddrCache needs localpart wildcard (e.g. empty localpart). Quarantined mail is missing headers modified/added by milter after checking dspam. Send DSN for permerror before processing extended result. An additional DSN may be sent based on extended result. Send permerror DSN to postmaster@sending_domain. Rescind whitelist for banned extensions, in case sender is infected. Train honeypot on error only. Find rfc2822 policy for MFROM quoting. Support explicit errors for SPF policy in access file: SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS" Defer TEMPERROR in SPF evaluation - give precedence to security (only defer for PASS mechanisms). Create null config that does nothing - except maybe add Received-SPF headers. Many admins would like to turn features on one at a time. Can't output messages with malformed rfc822 attachments. Move milter,Milter,mime,spf modules to pymilter milter package will have bms.py application Web admin interface message log for automated stats and blacklisting Skip dspam when SPF pass? NO Report 551 with rcpt on SPF fail? check spam keywords with character classes, e.g. {a}=[a@ãä], {i}=[i1í], {e}=[eë], {o}=[o0ö] Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS forwarder accounts, and a util provides a special local alias for the user to give to the forwarder. (Or user just adds arbitrary alias unique to that forwarder to a database.) Alias only works for mail from that forwarder. Milter gets forwarder domain from alias and uses it to SPF check forwarder. Framework for modular Python milter components within a single VM. Python milters can be already be composed through sendmail by running each in a separate process. However, a significant amount of memory is wasted for each additional Python VM, and communication between milters is cumbersome (e.g., adding mail headers, writing external files). Copy incoming wiretap mail, even though sendmail alias works perfectly for the purpose, to avoid having to change two configs for a wiretap. Provide a way to reload milter.cfg without stopping/restarting milter. Allow selected Windows extensions for specific domains via milter.cfg Fix setup.py so that _FFR_QUARANTINE is automatically defined when available in libmilter. Keep separate ismodified flag for headers and body. This is important when rejecting outgoing mail with viruses removed (so as not to embarrass yourself), and also removing Received headers with hidepath. Need a test module to feed sample messages to a milter though a live sendmail and SMTP. The mockup currently used is probably not very accurate, and doesn't test the threading code. DONE Table of sendmail macros for documentation. In API docs on milter.org. DONE For selected domains, check rcpts via CBV before accepting mail. Cache results. This will kick out dictonary attacks against a mail domain behind a gateway sooner. DONE Convert DSN to REJECT unless sender gets SPF pass or best guess pass. Make configurable by SPF result with NOTSPAM policy (reject or deliver without DSN). Maybe policy should be NODSN - still verify sender with CBV. DONE Add parseaddr test case for 'foo@bar.com ' DONE Require signed MFROM for all incoming bounces when signing all outgoing mail - except from trusted relays. DONE Added Message-ID header to DSN with SRS signed sender. When seen on incoming rfc ignorant failure message, blacklist sender. DONE Option to add Received-SPF header, but never reject on SPF. I think the above will handle this. DONE Received-SPF header field should show identity that was checked. DONE When training with spam, REJECT after data so that mistakenly blacklisted senders at least get an error. DONE Milter won't start when it can't change permissions on *.lock to match *.log. Should maybe ignore that error - the effect will be to set the permissions to default. DONE Milter won't start when a whitelist/blacklist file is missing. DONE Delayed failure detection should parse From header to find email address. DONE When bms.py can't find templates, it passes None to dsn.create_msg(), which uses local variable as backup, which no longer exist. Do plain CBV in that case instead. DONE Find and use X-GOSSiP: header for SPAM: and FP: submissions. Would need to keep tags longer. DONE Parse incoming 3464 DSNs for "Action: failed" to recognize delayed failures. This works regardless of Subject. DONE Reports PROBATION even when rejecting message (works, but confusing in log). DONE Delayed_failure detection needs to handle multi-line header fields. Also, delayed_failure should be recognized when addressed to postmaster@helodomain DONE DSN for Permerror shows 'None' for error under some condition. DONE Allow blacklisted emails as well as domains in blacklist.log. Use same data structure as autowhitelist.log. DONE Backup copies for outgoing/incoming mail. DONE Don't match dynamic ptr in bestguess. milter-0.8.18/milter.spec0000644000160600001450000003765512121400466014152 0ustar stuartbms# This spec file contains 2 noarch packages in addition to the pymilter # module. To compile all three on 32-bit Intel, use: # rpmbuild -ba --target=i386,noarch pymilter.spec %define __python python2.6 %define pythonbase python %define sysvinit milter.rc %define libdir %{_libdir}/pymilter %define logdir /var/log/milter %define datadir /var/lib/milter Name: milter Group: Applications/System Summary: BMS spam and reputation milter Version: 0.8.18 Release: 2%{dist}.py26 Source: milter-%{version}.tar.gz #Patch: %{name}-%{version}.patch License: GPLv2+ Group: Development/Libraries BuildRoot: %{_tmppath}/%{name}-buildroot BuildArch: noarch Vendor: Stuart D. Gathman Url: http://www.bmsi.com/python/milter.html Requires: %{pythonbase} >= 2.6.5, %{pythonbase}-pyspf >= 2.0.6 Requires: %{pythonbase}-pymilter >= 0.9.8, %{pythonbase}-pydns >= 2.3.5 Conflicts: %{pythonbase}-pydkim < 0.5.3 %ifos Linux Requires: chkconfig %endif %description A complex but effective spam filtering, SPF checking, greylisting, and reputation tracking mail application. It uses pydspam if installed for bayesian filtering. %package spf Group: Applications/System Summary: Simple SPF milter Requires: %{pythonbase}-pyspf >= 2.0.5, %{pythonbase}-pymilter >= 0.9.6 Obsoletes: pymilter-spf < 0.8.10 %description spf A simple mail filter to add Received-SPF headers and reject forged mail. Rejection policy is configured via sendmail access file and can be tailored by domain. %package dkim Group: Applications/System Summary: Simple DKIM milter Requires: %{pythonbase}-pydkim >= 0.5.1, %{pythonbase}-pymilter >= 0.9.6 Requires: %{pythonbase}-authres >= 0.3 %description dkim A simple mail filter to add and verify DKIM-Signature headers and reject forged mail based on policy. Rejection policy is configured via sendmail access file and can be tailored by domain. %prep %setup -q -n milter-%{version} #patch -p0 -b .bms %install rm -rf $RPM_BUILD_ROOT mkdir -p $RPM_BUILD_ROOT/etc/mail mkdir -p $RPM_BUILD_ROOT%{logdir}/save mkdir -p $RPM_BUILD_ROOT%{datadir} mkdir -p $RPM_BUILD_ROOT%{libdir} cp *.txt $RPM_BUILD_ROOT%{datadir} cp -p bms.py spfmilter.py dkim-milter.py ban2zone.py $RPM_BUILD_ROOT%{libdir} cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg cp spfmilter.cfg $RPM_BUILD_ROOT/etc/mail cp dkim-milter.cfg $RPM_BUILD_ROOT/etc/mail # logfile rotation mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF' %{logdir}/milter.log { copytruncate compress } %{logdir}/banned_ips { rotate 7 daily copytruncate } %{logdir}/banned_domains { rotate 7 weekly copytruncate } EOF cat >$RPM_BUILD_ROOT/etc/logrotate.d/dkim-milter <<'EOF' %{logdir}/dkim-milter.log { copytruncate compress } EOF # purge saved defanged message copies mkdir -p $RPM_BUILD_ROOT/etc/cron.daily R='-r' cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF' #!/bin/sh find %{logdir}/save -mtime +7 | xargs $R rm # work around any memory leaks /etc/init.d/milter condrestart EOF chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter cp spfmilter.rc $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter cp dkim-milter.rc $RPM_BUILD_ROOT/etc/rc.d/init.d/dkim-milter ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF' /^python=/ c python="%{__python}" . w q EOF ed $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter <<'EOF' /^python=/ c python="%{__python}" . w q EOF ed $RPM_BUILD_ROOT/etc/rc.d/init.d/dkim-milter <<'EOF' /^python=/ c python="%{__python}" . w q EOF mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack %post #echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf" /sbin/chkconfig --add milter %preun if [ $1 = 0 ]; then /sbin/chkconfig --del milter fi %post spf #echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf" /sbin/chkconfig --add spfmilter %preun spf if [ $1 = 0 ]; then /sbin/chkconfig --del spfmilter fi %post dkim #echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf" /sbin/chkconfig --add dkim-milter %preun dkim if [ $1 = 0 ]; then /sbin/chkconfig --del dkim-milter fi %files %defattr(-,root,root) /etc/logrotate.d/milter /etc/cron.daily/milter /etc/rc.d/init.d/milter %defattr(-,mail,mail) %dir %{logdir}/save %dir %{datadir} %{libdir}/bms.py %{libdir}/ban2zone.py %config(noreplace) %{datadir}/strike3.txt %config(noreplace) %{datadir}/softfail.txt %config(noreplace) %{datadir}/fail.txt %config(noreplace) %{datadir}/neutral.txt %config(noreplace) %{datadir}/quarantine.txt %config(noreplace) %{datadir}/permerror.txt %config(noreplace) %{datadir}/temperror.txt %config(noreplace) %{datadir}/heloerror.txt %config(noreplace) /etc/mail/pymilter.cfg /usr/share/sendmail-cf/hack/rhsbl.m4 %files spf %defattr(-,root,root) %{libdir}/spfmilter.py* %config(noreplace) /etc/mail/spfmilter.cfg /etc/rc.d/init.d/spfmilter %files dkim %defattr(-,root,root) %{libdir}/dkim-milter.py* %config(noreplace) /etc/mail/dkim-milter.cfg /etc/rc.d/init.d/dkim-milter /etc/logrotate.d/dkim-milter %clean rm -rf $RPM_BUILD_ROOT %changelog * Sun Mar 17 2013 Stuart Gathman 0.8.18-2 - Default logdir to datadir for compatibility with old configs. - Add logdir to sample config. * Sat Mar 16 2013 Stuart Gathman 0.8.18-1 - Test cases and bug fixes for spfmilter - Configure untrapped_exception policy for spfmilter - Reject numeric HELO for spfmilter - from_words from file feature for bms milter - ban mailbox, not entire domain, for configured email_providers - Use pysqlite (included in python) for greylist database - banned_domains and ips moved back to logdir for bms milter - straighten out datadir vs logdir for bms milter * Mon Jan 07 2013 Stuart Gathman 0.8.17-2 - include logrotate for dkim-milter * Sun Apr 22 2012 Stuart Gathman 0.8.17-1 - report keysize of DKIM signatures. - simple DKIM milter as another sample - basic DKIM signing support - Implement DKIM policy in access file. - Parse Authentication-Results header to get dkim result for feedback mail. - Let DKIM confirm domain for missing or neutral SPF result. * Mon Oct 03 2011 Stuart Gathman 0.8.16-1 - experimental DKIM support - Reference templated URLs in error messages * Thu Mar 03 2011 Stuart Gathman 0.8.15-1 - Python2.6 * Sat Apr 10 2010 Stuart Gathman 0.8.14-2 - Default ip banning off * Sat Apr 10 2010 Stuart Gathman 0.8.14-1 - ignore zero length keywords - a disastrous typo - ban generic domains for common subdomains - allow illegal HELO from internal network for braindead copiers - don't ban for multiple anonymous MFROM - trust localhost not to be a zombie - sendmail sends from queue on localhost - ban domains on best_guess pass * Fri Aug 28 2009 Stuart Gathman 0.8.13-1 - Default internal_policy off - Experimental banned domain list - block DSN from internal connections, except for listed internal MTAs - BAN policy in access file bans connect IP - use DATA callback to improve SRS check * Sun Dec 21 2008 Stuart Gathman 0.8.12-2 - internal_policy * Mon Nov 24 2008 Stuart Gathman 0.8.12-1 - 2 demerits for HELO after MAIL FROM - Make initscript use pid file. - Fix greylist config - SPF Pass policy * Sat Oct 11 2008 Stuart Gathman 0.8.11-1 - Support greylisting - Recognize vacation messages as autoreplies. - Never ban a trusted relay. - Missing global reading banned_ips - ban2zone.py * Mon Aug 25 2008 Stuart Gathman 0.8.10-1 - log rcpt for SRS rejections - improved parsing into email and fullname (still 2 self test failures) - implement no-DSN CBV, reduce full DSNs - check for porn words in MAIL FROM fullname - ban IP for too many bad MAIL FROMs or RCPT TOs - temperror policy in access - no CBV for whitelisted MAIL FROM except permerror, softfail - Allow explicitly whitelisted email from banned_users. - configure gossip TTL * Mon Sep 24 2007 Stuart Gathman 0.8.9-1 - Use ifarch hack to build milter and milter-spf packages as noarch - Remove spf dependency from dsn.py, add dns.py * Fri Jan 05 2007 Stuart Gathman 0.8.8-1 - move AddrCache, parse_addr, iniplist to Milter package - move parse_header to Milter.utils - fix plock for missing source and can't change owner/group - add sample spfmilter.py milter - private_relay config option - persist delayed DSN blacklisting - handle gossip server restart without disabling gossip - split out pymilter and pymilter-spf packages - move milter apps to /usr/lib/pymilter * Sat Nov 04 2006 Stuart Gathman 0.8.7-1 - More lame bounce heuristics - SPF moved to pyspf RPM - wiretap archive option - Do plain CBV if missing template - SMTP AUTH policy in access * Tue May 23 2006 Stuart Gathman 0.8.6-2 - Support CBV timeout - Support fail template, headers in templates - Create GOSSiP record only when connection will procede to DATA. - More SPF lax heuristics - Don't require SPF pass for white/black listing mail from trusted relay. - Support localpart wildcard for white and black lists. * Thu Feb 23 2006 Stuart Gathman 0.8.6-1 - Delay reject of unsigned RCPT for postmaster and abuse only - Fix dsn reporting of hard permerror - Resolve FIXME for wrap_close in miltermodule.c - Add Message-ID to DSNs - Use signed Message-ID in delayed reject to blacklist senders - Auto-train via blacklist and auto-whitelist - Don't check userlist for signed MFROM - Accept but skip DSPAM and training for whitelisted senders without SPF PASS - Report GC stats - Support CIDR matching for IP lists - Support pysrs sign feature - Support localpart specific SPF policy in access file * Thu Dec 29 2005 Stuart Gathman 0.8.5-1 - Simple trusted_forwarder implementation. - Fix access_file neutral policy - Move Received-SPF header to beginning of headers - Supply keyword info for all results in Received-SPF header. - Move guessed SPF result to separate header - Activate smfi_insheader only when SMFIR_INSHEADER defined - Handle NULL MX in spf.py - in-process GOSSiP server support (to be extended later) - Expire CBV cache and renew auto-whitelist entries * Fri Oct 21 2005 Stuart Gathman 0.8.4-2 - Don't supply sender when MFROM is subdomain of header from/sender. - Don't send quarantine DSN for DSNs - Skip dspam for replies/DSNs to signed MFROM * Thu Oct 20 2005 Stuart Gathman 0.8.4-1 - Fix SPF policy via sendmail access map (case insensitive keys). - Auto whitelist senders, train screener on whitelisted messages - Optional idx parameter to addheader to invoke smfi_insheader - Activate progress when SMFIR_PROGRESS defined * Wed Oct 12 2005 Stuart Gathman 0.8.3-1 - Keep screened honeypot mail, but optionally discard honeypot only mail. - spf_accept_fail option for braindead SPF senders (treats fail like softfail) - Consider SMTP AUTH connections internal. - Send DSN for SPF errors corrected by extended processing. - Send DSN before SCREENED mail is quarantined - Option to set SPF policy via sendmail access map. - Option to supply Sender header from MAIL FROM when missing. - Use logging package to keep log lines atomic. * Fri Jul 15 2005 Stuart Gathman 0.8.2-4 - Limit each CNAME chain independently like PTR and MX * Fri Jul 15 2005 Stuart Gathman 0.8.2-3 - Limit CNAME lookups (regression) * Fri Jul 15 2005 Stuart Gathman 0.8.2-2 - Handle corrupt ZIP attachments * Fri Jul 15 2005 Stuart Gathman 0.8.2-1 - Strict processing limits per SPF RFC - Fixed several parsing bugs under RFC - Support official IANA SPF record (type99) - Honeypot support (requires pydspam-1.1.9) - Extended SPF processing results beyond strict RFC limits - Support original SES for local bounce protection (requires pysrs-0.30.10) - Callback exception processing option in milter module * Thu Jun 16 2005 Stuart Gathman 0.8.1-1 - Fix zip in zip loop in mime.py - Fix HeaderParseError in bms.py header callback - Check internal_domains for outgoing mail - Fix inconsistent results from send_dsn * Mon Jun 06 2005 Stuart Gathman 0.8.0-3 - properly log pydspam exceptions * Sat Jun 04 2005 Stuart Gathman 0.8.0-2 - Include default softfail, strike3 templates * Wed May 25 2005 Stuart Gathman 0.8.0-1 - Move Milter module to subpackage. - DSN support for Three strikes rule and SPF SOFTFAIL - Move /*mime*/ and dynip to Milter subpackage - Fix SPF unknown mechanism list not cleared - Make banned extensions configurable. - Option to scan zipfiles for bad extensions. * Tue Feb 08 2005 Stuart Gathman 0.7.3-1.EL3 - Support EL3 and Python2.4 (some scanning/defang support broken) * Mon Aug 30 2004 Stuart Gathman 0.7.2-1 - Fix various SPF bugs - Recognize dynamic PTR names, and don't count them as authentication. - Three strikes and yer out rule. - Block softfail by default unless valid PTR or HELO - Return unknown for null mechanism - Return unknown for invalid ip address in mechanism - Try best guess on HELO also - Expand setreply for common errors - make rhsbl.m4 hack available for sendmail.mc * Sun Aug 22 2004 Stuart Gathman 0.7.1-1 - Handle modifying mislabeled multipart messages without an exception - Support setbacklog, setmlreply - allow multi-recipient CBV - return TEMPFAIL for SPF softfail * Fri Jul 23 2004 Stuart Gathman 0.7.0-1 - SPF check hello name - Move pythonsock to /var/run/milter - Move milter.cfg to /etc/mail/pymilter.cfg - Check M$ style XML CID records by converting to SPF - Recognize, but never match ip6 until we properly support it. - Option to reject when no PTR and no SPF * Fri Apr 09 2004 Stuart Gathman 0.6.9-1 - Validate spf.py against test suite, and add Received-SPF support to spf.py - Support best_guess for SPF - Reject numeric hello names - Preserve case of local part in sender - Make libmilter timeout a config option - Fix setup.py to work with python < 2.2.3 * Tue Apr 06 2004 Stuart Gathman 0.6.8-3 - Reject invalid SRS immediately for benefit of callback verifiers - Fix include bug in spf.py * Tue Apr 06 2004 Stuart Gathman 0.6.8-2 - Bug in check_header * Mon Apr 05 2004 Stuart Gathman 0.6.8-1 - Don't report spoofed unless rcpt looks like SRS - Check for bounce with multiple rcpts - Make dspam see Received-SPF headers - Make sysv init work with RH9 * Thu Mar 25 2004 Stuart Gathman 0.6.7-3 - Forgot to make spf_reject_neutral global in bms.py * Wed Mar 24 2004 Stuart Gathman 0.6.7-2 - Defang message/rfc822 content_type with boundary - Support SPF delegation - Reject neutral SPF result for selected domains * Tue Mar 23 2004 Stuart Gathman 0.6.7-1 - SRS forgery check. Detect thread resource starvation. - Properly remove local socket with explicit type. - Decode obfuscated subject headers. * Wed Mar 11 2004 Stuart Gathman 0.6.6-2 - init script bug with python2.3 * Wed Mar 10 2004 Stuart Gathman 0.6.6-1 - SPF checking, hello blacklist * Mon Mar 08 2004 Stuart Gathman 0.6.5-2 - memory leak in envfrom and envrcpt * Mon Mar 01 2004 Stuart Gathman 0.6.5-1 - progress notification - memory leak in connect - trusted relay * Thu Feb 19 2004 Stuart Gathman 0.6.4-2 - smart alias wildcard patch, compile for sendmail-8.12 * Thu Dec 04 2003 Stuart Gathman 0.6.4-1 - many fixes for dspam support * Wed Oct 22 2003 Stuart Gathman 0.6.3 - dspam SCREEN feature - streamline dspam false positive handling * Mon Sep 01 2003 Stuart Gathman 0.6.1 - Full dspam support added * Mon Aug 26 2003 Stuart Gathman - Use New email module * Fri Jun 27 2003 Stuart Gathman - Add dspam module milter-0.8.18/spfmilter.cfg0000644000160600001450000000164312116745503014464 0ustar stuartbms[milter] # The socket used to communicate with sendmail socketname = /var/run/milter/spfmiltersock # Name of the milter given to sendmail name = pyspffilter # Trusted relays such as secondary MXes that should not have SPF checked. ;trusted_relay = # Internal networks that should not have SPF checked. internal_connect = 127.0.0.1,192.168.0.0/16,10.0.0.0/8 # TEMPFAIL, REJECT, CONTINUE untrapped_exception = CONTINUE # See http://www.openspf.com for more info on SPF. [spf] # Use sendmail access map or similar format for detailed spf policy. # SPF entries in the access map will override defaults. access_file = /etc/mail/access.db # Connections that get an SPF pass for a pretend MAIL FROM of # postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM. # This is for non-SRS forwarders. It is a simple implementation that # is inefficient for more than a few entries. ;trusted_forwarder = careerbuilder.com milter-0.8.18/setup.py0000644000160600001450000000247012116721242013501 0ustar stuartbmsimport os import sys from distutils.core import setup, Extension # Use the spec file to install the machinery to run this as a service. # This setup is just used to register. setup(name = "milter", version = '0.8.18', description="Anti-forgery, reputation tracking, anti-spam milter", long_description="""\ This is a milter application based on pymilter. It implements per-domain policies and reputation tracking based on SPF result (example.com:PASS tracks a different reputation than example.com:NEUTRAL). It has too many features. A simple SPF checking milter with policy in sendmail access file based on domain and SPF result is also included. """, author="Stuart Gathman", author_email="stuart@bmsi.com", maintainer="Stuart D. Gathman", maintainer_email="stuart@bmsi.com", license="GPL", url="http://www.bmsi.com/python/milter.html", keywords = ['sendmail','milter'], classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Natural Language :: English', 'Operating System :: POSIX', 'Programming Language :: Python', 'Topic :: Communications :: Email :: Mail Transport Agents', 'Topic :: Communications :: Email :: Filters' ] ) milter-0.8.18/errors/0002755000160600001450000000000012121400433013272 5ustar stuartbmsmilter-0.8.18/errors/anon.html0000644000160600001450000000131411722257335015131 0ustar stuartbmsHello. I work for Business Management Systems in Fairfax, VA. There is a problem with:
    %mfrom
    The HELO name, %helo, is invalid (does not resolve to %ip). There is no valid PTR record for %ip (does not exist or does not resolve to %ip. Dynamic names for HELO or PTR do not count since you have no Sender Policy either.

    Contact your mail administrator IMMEDIATELY! Your mail server is severely misconfigured. (If you have a dynamic PTR/HELO, it is technically correct, but most email services reject dynamic PTR because it generally indicates a "bot", a compromised home PC that sends spam.) milter-0.8.18/errors/illrepute.html0000644000160600001450000000103711722257335016205 0ustar stuartbmsHello. I am a mail screener working for Business Management Systems in Fairfax, VA. There is a problem with the domain:

    %domain
    While this domain is properly configured for email, it has acquired quite a reputation for spam on emails where the authentication result is %qual. On a scale of -100 to 100, it scores %score.

    If you are just an innocent user trying to send mail, then your mail server could be compromised. You can give us a call at 703-591-0911 and my boss can reset your domain reputation. milter-0.8.18/errors/dsnrefused.html0000644000160600001450000000465711722257335016355 0ustar stuartbmsHello. I work for Business Management Systems in Fairfax, VA. There is a problem with:

    %mfrom
    not accepting DSNs for emails sent from that domain.

    We and possibly many other ISPs require mail servers contacting us to follow the minimum requirements for the functioning of mail over the Internet by adhering to RFC 2821 (and others), which requires that a mail server accept for the purpose of error reporting the null email address ("<>") in the MAIL FROM: command. See

    RFC 2821
    section 4.1.1.2 MAIL (BNF grammar)
    section 6.1 Reliable Delivery and Replies by Email (para. 2)
    ftp://ftp.rfc-editor.org/in-notes/rfc2821.txt
    RFC 2505
    section 2. Recommendations 6a)
    section 2.6.1. "MAIL From: <>"
    ftp://ftp.rfc-editor.org/in-notes/rfc2505.txt
    RFC 1123
    section 5.2.9 Command Syntax: RFC-821 Section 4.1.2
    ftp://ftp.rfc-editor.org/in-notes/rfc1123.txt
    RFC 821 (obsoleted by RFC 2821)
    section 4.1.2 ftp://ftp.rfc-editor.org/in-notes/rfc821.txt
    Your domain "%mfrom" no longer accepts <>. The need for the null address ("<>") as an error reporting tool for mail server software is VERY important to the correct functioning of mail over the Internet. It provides legitimate users a means of find out if mail failed to arrive at its intended destination. Blocking this for the purpose of spam control is unacceptable, because legitimate users will never know that mail might have been rejected because they simply mistyped the destination address, or that a mail box is full, or no longer active, etc. You can control forged DSNs by using a BATV scheme.

    Furthermore, many ESPs, for example verizon.net, validate the return-path of incoming emails by calling back to an MX for the domain. With DSNs not functioning, you will be unable to send emails to verizon.net and other mail servers that valid the return-path.

    We ask if you would kindly contact those responsible for the "%mfrom" mail servers or if you have the means yourselves to resolve the issue so that "%mfrom" remains RFC compliant with respect to the use of the null address ("<>"). It is only a matter of time before some very important email goes undelivered and no one is notified of the problem.

    Thank you for your time and attention in this matter, milter-0.8.18/errors/bandom.html0000644000160600001450000000112011722257335015431 0ustar stuartbmsHello. I am a mail screener working for Business Management Systems in Fairfax, VA. There is a problem with the domain:

    %domain
    We have banned this domain for email abuse. The specific reasons were given on your domains previous attempts to send us email. I don't currently track this history automatically - sorry.

    If you are just an innocent user trying to send mail, then your mail server could be compromised or misconfigured. You can give us a call at 703-591-0911 and my boss can look in my logs to see what happened, and might unban your domain. milter-0.8.18/neutral.txt0000644000160600001450000000250711655257264014222 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: SPF %(result)s (POSSIBLE FORGERY) Auto-Submitted: auto-generated (sender verification) This is an automatically generated Delivery Status Notification. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Received-SPF: %(spf_result)s Your sender policy (or lack thereof) indicated that the above email was not sent via an authorized SMTP server, but may still be legitimate. Since there is no positive confirmation that the message is really from you, we have to give it extra scrutiny - including verifying that the sender really exists by sending you this DSN. We will remember this sender and not bother you again for a while. You can avoid this message entirely for legitimate mail by using an authorized SMTP server. Contact your mail administrator and ask how to configure your email client to use an authorized server. If you never sent the above message, then your domain has been forged. Your mail admin needs to publish a strict SPF record so that I can reject those forgeries instead of bugging you about them. See http://openspf.net for details. If you need further assistance, please do not hesitate to contact me. Kind regards, postmaster@%(receiver)s milter-0.8.18/HOWTO0000644000160600001450000001205611054577251012624 0ustar stuartbmsOn Sun, 11 Feb 2007, Rick Saul wrote: > Stuart I was planning to move to centos4.4 in a couple of weeks anyway... > Your advice of where to go from here. Oh - you are asking for a howto. Step one. Which DSPAM is right for you? The DSPAM project makes dspam part of the LDA (Local Delivery Agent). Pydspam puts dspam into the MTA (Mail Transfer Agent - sendmail with pymilter). The advantage of doing dspam in the LDA is that any aliasing has already been resolved. You need only configure mailboxes. The advantage of doing dspam in the MTA is it can screen an entire company as a gateway with multiple domains. Unfortunately, this means you have to tell it about all the aliases that comprise each account. (Also, pydspam is still uses dspam-2.6.5.2 - the Dspam API has changed for newer versions.) If the LDA is right for you, you'll want to use the official Dspam package. http://www.nuclearelephant.com/projects/dspam/ If the MTA approach is what you want, then pydspam is what you want. In either case, you will still want pymilter to block forgeries, Windows executables, etc. So, lets assume you want to install pymilter, and may or may not wish to install pydspam. Step two. Obtaining RPMS. For basic pymilter you'll need: python-2.4 milter-0.8.10 sendmail-8.13.x (with milter support enabled) and for SPF you'll need: pydns-2.3.3-2.4 pyspf-2.0.5-1.py24 and for SRS you'll need: pysrs-0.30.11-1.py24 I'm pretty sure you will want to have SPF and SRS available. Step three. Activate basic milter. Activate the basic milter and pysrs by editing /etc/mail/sendmail.mc and adding: define(`NO_SRS_FILE',`/etc/mail/no-srs-mailers')dnl dnl define(`NO_SRS_FROM_LOCAL')dnl HACK(`pysrs',`/var/run/milter/pysrs')dnl INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m') You can then "make sendmail.cf" and restart sendmail. Start milter and pysrs with "service milter start", "service pysrs start". Tail /var/log/milter/milter.log while SMTP clients connect to your sendmail instance. This should show you what the milter is doing. By default, milter-0.8.10 rejects on SPF fail. Step four. Tweaking the basic config. Most pymilter configuration is in /etc/mail/pymilter.cfg. To activate changes, "service milter restart". By default, milter scans attachments for executable extensions. You can turn this off by setting banned_exts to the empty list. There are options to scan ZIP attachments and rfc822 attachments. When it finds a banned file type, milter saves the original message in /var/log/milter/save, and replaces the attachment with a plain text warning message. Configure hello_blacklist with your own helo name and domains - which you know cannot legitimately be used by external MTAs. Configure trusted_relay with your secondary MX servers, if any. These should also run pymilter with similar policies. (But this isn't needed for initial testing.) Configure internal_connect with subnets of your internal SMTP clients. Internal connections skip SPF testing and other policies. You will likely need to set this to allow outgoing mail if you have an SPF policy already. Configure internal_domains with domains used by your internal SMTP clients. If they attempt to use any other domain, the attempt is blocked and the client is logged as a "zombie". Conversely, any attempt by an external MTA to use one of your internal domains is treated as a forgery and blocked (a simplified form of local SPF). Adjust porn_words and spam_words - these block emails with a Subject containing the listed strings. They can be empty to disable Subject string blocking. Advanced SPF configuration. The sendmail access file, or another readonly database with that format, can be used for detail spf policy. SPF access policy record are tagged with "SPF-{Result}:". Results are Pass, Neutral, Softfail, Fail, PermError. Currently supported policy keywords are OK, CBV, REJECT. Currently, TempError always results in TEMPFAIL. The default policies are set in pymilter.cfg. The defaults if none of the config options are set are as follows: SPF-Fail: REJECT SPF-Softfail: CBV SPF-Neutral: OK SPF-PermError: REJECT SPF-Pass: OK The tag may be followed by a specific domain. For instance, to require a Pass from aol.com: SPF-Neutral:aol.com REJECT SPF-Softfail:aol.com REJECT The CBV policy requires a valid HELO name. If the EHLO name is RFC2822 compliant, then a DSN is sent to the alleged sender. The template for the DSN is selected according to the SPF result: Fail: fail.txt SoftFail: softfail.txt Neutral: neutral.txt PermError: permerror.txt None: strike3.txt An SPF-Pass is always accepted by the milter. Domains can be blacklisted via sendmail in the access file or via a RHS DNS blacklist. To be continued. Forthcoming topics: SRS config pydspam config wiretap config -- Stuart D. Gathman Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154 "Confutatis maledictis, flammis acribus addictis" - background song for a Microsoft sponsored "Where do you want to go from here?" commercial. milter-0.8.18/heloerror.txt0000644000160600001450000000154311667434271014546 0ustar stuartbmsTo: %(sender)s From: postmaster@%(receiver)s Subject: Critical SPF configuration error Auto-Submitted: auto-generated (configuration error) This is an automatically generated Delivery Status Notification. THIS IS A WARNING MESSAGE ONLY. YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. Delivery to the following recipients has been delayed. %(rcpt)s Subject: %(subject)s Received-SPF: %(spf_result)s Your email used a HELO name, '%(heloname)', that is protected by a Sender Policy. This policy does not authorize the IP used to send your email. If you *should* be authorized to use that HELO name, please contact the email admin for %(heloname) and ask them to update their policy. Otherwise, please use a HELO name which you are authorized to use. If you need further assistance, please do not hesitate to contact me. Kind regards, postmaster@%(receiver)s milter-0.8.18/setup.cfg0000644000160600001450000000015711074146407013616 0ustar stuartbms[bdist_rpm] python=python2.4 doc_files=README NEWS TODO packager=Stuart D. Gathman release=1 milter-0.8.18/report.py0000644000160600001450000000776512117502775013701 0ustar stuartbms# Analyze milter log to find abusers import traceback import sys def parse_addr(a): beg = a.find('<') end = a.find('>') if beg >= 0: if end > beg: return a[beg+1:end] return a class Connection(object): def __init__(self,dt,tm,id,ip=None,conn=None): self.dt = dt self.tm = tm self.id = id if ip: _,self.host,self.ip = ip.split(None,2) elif conn: self.ip = conn.ip self.host = conn.host self.helo = conn.helo self.subject = None self.rcpt = [] self.mfrom = None self.helo = None self.innoc = [] self.whitelist = False def connections(fp): conndict = {} termdict = {} for line in fp: if line.startswith('{'): continue a = line.split(None,4) if len(a) < 4: continue dt,tm,id,op = a[:4] if (id,op) == ('bms','milter'): # FIXME: optionally yield all partial connections in conndict conndict = {} termdict = {} continue if id[0] == '[' and id[-1] == ']': try: key = int(id[1:-1]) except: print >>sys.stderr,'bad id:',line.rstrip() continue else: continue if op == 'connect': ip = a[4].rstrip() conn = Connection(dt,tm,id,ip=ip) conndict[key] = conn elif op in ( 'DISCARD:','TAG:','CBV:','Large','No', 'NOTE:','From:','Sender:','TRAIN:'): continue else: op = op.lower() try: conn = conndict[key] except KeyError: try: conn = termdict[key] del termdict[key] conndict[key] = conn except KeyError: print >>sys.stderr,'key error:',line.rstrip() continue try: if op == 'subject:': if len(a) > 4: conn.subject = a[4].rstrip() elif op == 'innoc:': conn.innoc.append(a[4].rstrip()) elif op == 'whitelist': conn.whitelist = True elif op == 'x-mailer:': if len(a) > 4: conn.mailer = a[4].rstrip() elif op == 'x-guessed-spf:': conn.spfguess = a[4] elif op == 'received-spf:': conn.spfres,conn.spfmsg = a[4].rstrip().split(None,1) elif op == 'received:': conn.received = a[4].rstrip() elif op == 'temp': _,conn.tempfile = a[4].rstrip().split(None,1) elif op == 'srs': _,conn.srsrcpt = a[4].rstrip().split(None,1) elif op == 'mail': _,conn.mfrom = a[4].rstrip().split(None,1) elif op == 'rcpt': _,rcpt = a[4].rstrip().split(None,1) conn.rcpt.append(rcpt) elif op == 'hello': _,conn.helo = a[4].rstrip().split(None,1) elif op in ('eom','dspam','abort'): del conndict[key] conn.enddt = dt conn.endtm = tm conn.result = op yield conn termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn) elif op in ('reject:','dspam:','tempfail:','reject','fail:','honeypot:'): del conndict[key] conn.enddt = dt conn.endtm = tm conn.result = op conn.resmsg = a[4].rstrip() yield conn termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn) elif op in ('fp:','spam:'): del conndict[key] termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn) else: print >>sys.stderr,'unknown op:',line.rstrip() except Exception: print >>sys.stderr,'error:',line.rstrip() traceback.print_exc() if __name__ == '__main__': import gzip for fn in sys.argv[1:]: if fn.endswith('.gz'): fp = gzip.open(fn) else: fp = open(fn) for conn in connections(fp): if conn.rcpt and conn.mfrom: for r in conn.rcpt: if r.lower().find('iancarter') > 0: break else: if conn.mfrom.lower().find('iancarter') < 0: continue print >>sys.stderr,conn.result,conn.dt,conn.tm,conn.id,conn.subject,parse_addr(conn.mfrom), for a in conn.rcpt: print parse_addr(a), print