Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65c73f61c2 |
@@ -5,11 +5,8 @@ wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
|||||||
it with distutils, and generally transformed it from a quick hack to a
|
it with distutils, and generally transformed it from a quick hack to a
|
||||||
real, usable Python extension.
|
real, usable Python extension.
|
||||||
|
|
||||||
Other contributors (in random order):
|
Other contributors:
|
||||||
|
|
||||||
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
|
Terence Way
|
||||||
for providing a Python port of SPF
|
for providing a Python port of SPF
|
||||||
Scott Kitterman
|
Scott Kitterman
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ include testmime.py
|
|||||||
include testbms.py
|
include testbms.py
|
||||||
include testdspam.py
|
include testdspam.py
|
||||||
include rejects.py
|
include rejects.py
|
||||||
include report.py
|
|
||||||
include bms.py
|
include bms.py
|
||||||
include spf.py
|
include spf.py
|
||||||
include cid2spf.py
|
include cid2spf.py
|
||||||
@@ -18,7 +17,6 @@ include spfquery.py
|
|||||||
include test.py
|
include test.py
|
||||||
include sample.py
|
include sample.py
|
||||||
include test/*
|
include test/*
|
||||||
include doc/*
|
|
||||||
include Milter/*.py
|
include Milter/*.py
|
||||||
include *.spec
|
include *.spec
|
||||||
include start.sh
|
include start.sh
|
||||||
|
|||||||
+4
-4
@@ -16,7 +16,7 @@ from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
|
|||||||
try: from milter import QUARANTINE
|
try: from milter import QUARANTINE
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
__version__ = '0.8.5'
|
__version__ = '0.8.3'
|
||||||
|
|
||||||
_seq_lock = thread.allocate_lock()
|
_seq_lock = thread.allocate_lock()
|
||||||
_seq = 0
|
_seq = 0
|
||||||
@@ -44,7 +44,7 @@ class Milter:
|
|||||||
for i in msg: print i,
|
for i in msg: print i,
|
||||||
print
|
print
|
||||||
|
|
||||||
def connect(self,hostname,family,hostaddr):
|
def connect(self,hostname,unused,hostaddr):
|
||||||
"Called for each connection to sendmail."
|
"Called for each connection to sendmail."
|
||||||
self.log("connect from %s at %s" % (hostname,hostaddr))
|
self.log("connect from %s at %s" % (hostname,hostaddr))
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
@@ -106,8 +106,8 @@ class Milter:
|
|||||||
return self.__ctx.setreply(rcode,xcode,msg,*ml)
|
return self.__ctx.setreply(rcode,xcode,msg,*ml)
|
||||||
|
|
||||||
# Milter methods which can only be called from eom callback.
|
# Milter methods which can only be called from eom callback.
|
||||||
def addheader(self,field,value,idx=-1):
|
def addheader(self,field,value):
|
||||||
return self.__ctx.addheader(field,value,idx)
|
return self.__ctx.addheader(field,value)
|
||||||
|
|
||||||
def chgheader(self,field,idx,value):
|
def chgheader(self,field,idx,value):
|
||||||
return self.__ctx.chgheader(field,idx,value)
|
return self.__ctx.chgheader(field,idx,value)
|
||||||
|
|||||||
@@ -1,19 +1,5 @@
|
|||||||
Here is a history of user visible changes to Python milter.
|
Here is a history of user visible changes to Python milter.
|
||||||
|
|
||||||
0.8.5 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
|
|
||||||
0.8.4 Auto-whitelist recipients of outgoing email.
|
|
||||||
Fix SPF policy via sendmail access map (case insensitive keys).
|
|
||||||
Train screener on whitelisted messages
|
|
||||||
Optional idx parameter to addheader to invoke smfi_insheader
|
|
||||||
Activate progress API when SMFIR_PROGRESS defined
|
|
||||||
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
|
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
|
||||||
spf_accept_fail option for braindead SPF senders
|
spf_accept_fail option for braindead SPF senders
|
||||||
(treats fail like softfail)
|
(treats fail like softfail)
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
Require signed MFROM for all incoming bounces when signing all outgoing mail -
|
|
||||||
except from trusted relays.
|
|
||||||
|
|
||||||
Send DSN for permerror before processing extended result. An additional
|
|
||||||
DSN may be sent based on extended result.
|
|
||||||
|
|
||||||
Rescind whitelist for banned extensions, in case sender is infected.
|
|
||||||
|
|
||||||
Train honeypot on error only.
|
|
||||||
|
|
||||||
Find rfc2822 policy for MFROM quoting.
|
Find rfc2822 policy for MFROM quoting.
|
||||||
|
|
||||||
Support explicit errors for SPF policy in access file:
|
Use /etc/mail/access for domain specific SPF policies.
|
||||||
|
|
||||||
|
SPF-Fail: REJECT
|
||||||
|
SPF-Softfail: OK
|
||||||
|
SPF-Neutral: OK
|
||||||
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
||||||
|
|
||||||
Defer TEMPERROR in SPF evaluation - give precedence to security
|
Defer TEMPERROR in SPF evaluation - give precedence to security
|
||||||
@@ -22,6 +16,9 @@ Option to add Received-SPF header, but never reject on SPF.
|
|||||||
Create null config that does nothing - except maybe add Received-SPF
|
Create null config that does nothing - except maybe add Received-SPF
|
||||||
headers. Many admins would like to turn features on one at a time.
|
headers. Many admins would like to turn features on one at a time.
|
||||||
|
|
||||||
|
Auto whitelist based on outgoing email - perhaps with magic subject
|
||||||
|
or recipient prefix.
|
||||||
|
|
||||||
Can't output messages with malformed rfc822 attachments.
|
Can't output messages with malformed rfc822 attachments.
|
||||||
|
|
||||||
Move milter,Milter,mime,spf modules to pymilter
|
Move milter,Milter,mime,spf modules to pymilter
|
||||||
@@ -36,8 +33,7 @@ check spam keywords with character classes, e.g.
|
|||||||
|
|
||||||
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
|
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
|
||||||
forwarder accounts, and a util provides a special local alias for the
|
forwarder accounts, and a util provides a special local alias for the
|
||||||
user to give to the forwarder. (Or user just adds arbitrary alias
|
user to give to the forwarder. Alias only works for mail from that
|
||||||
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
|
forwarder. Milter gets forwarder domain from alias and uses it to
|
||||||
SPF check forwarder.
|
SPF check forwarder.
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# A simple milter that has grown quite a bit.
|
# A simple milter that has grown quite a bit.
|
||||||
# $Log$
|
# $Log$
|
||||||
# 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
|
# Revision 1.29 2005/10/11 22:50:07 customdesigned
|
||||||
# Always check HELO except for SPF pass, temperror.
|
# Always check HELO except for SPF pass, temperror.
|
||||||
#
|
#
|
||||||
@@ -344,12 +290,6 @@ from Milter.dynip import is_dynip as dynip
|
|||||||
from fnmatch import fnmatchcase
|
from fnmatch import fnmatchcase
|
||||||
from email.Header import decode_header
|
from email.Header import decode_header
|
||||||
|
|
||||||
# Import gossip if available
|
|
||||||
try:
|
|
||||||
import gossip
|
|
||||||
from gossip.server import Gossip
|
|
||||||
except: gossip = None
|
|
||||||
|
|
||||||
# Import pysrs if available
|
# Import pysrs if available
|
||||||
try:
|
try:
|
||||||
import SRS
|
import SRS
|
||||||
@@ -388,7 +328,6 @@ scan_html = True
|
|||||||
scan_rfc822 = True
|
scan_rfc822 = True
|
||||||
internal_connect = ()
|
internal_connect = ()
|
||||||
trusted_relay = ()
|
trusted_relay = ()
|
||||||
trusted_forwarder = ()
|
|
||||||
internal_domains = ()
|
internal_domains = ()
|
||||||
banned_users = ()
|
banned_users = ()
|
||||||
hello_blacklist = ()
|
hello_blacklist = ()
|
||||||
@@ -398,7 +337,6 @@ dspam_users = {}
|
|||||||
dspam_userdir = None
|
dspam_userdir = None
|
||||||
dspam_exempt = {}
|
dspam_exempt = {}
|
||||||
dspam_whitelist = {}
|
dspam_whitelist = {}
|
||||||
whitelist_senders = {}
|
|
||||||
dspam_screener = ()
|
dspam_screener = ()
|
||||||
dspam_internal = True # True if internal mail should be dspammed
|
dspam_internal = True # True if internal mail should be dspammed
|
||||||
dspam_reject = ()
|
dspam_reject = ()
|
||||||
@@ -414,8 +352,9 @@ spf_best_guess = False
|
|||||||
spf_reject_noptr = False
|
spf_reject_noptr = False
|
||||||
supply_sender = False
|
supply_sender = False
|
||||||
access_file = None
|
access_file = None
|
||||||
|
time_format = '%Y%b%d %H:%M:%S %Z'
|
||||||
timeout = 600
|
timeout = 600
|
||||||
|
cbv_cache = {}
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
stream=sys.stdout,
|
stream=sys.stdout,
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -424,8 +363,18 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
milter_log = logging.getLogger('milter')
|
milter_log = logging.getLogger('milter')
|
||||||
|
|
||||||
if gossip:
|
try:
|
||||||
gossip_node = Gossip('gossip4.db',120)
|
too_old = time.time() - 7*24*60*60 # 7 days
|
||||||
|
for ln in open('send_dsn.log'):
|
||||||
|
try:
|
||||||
|
rcpt,ts = ln.strip().split(None,1)
|
||||||
|
l = time.strptime(ts,time_format)
|
||||||
|
t = time.mktime(l)
|
||||||
|
if t > too_old:
|
||||||
|
cbv_cache[rcpt] = None
|
||||||
|
except:
|
||||||
|
cbv_cache[ln.strip()] = None
|
||||||
|
except IOError: pass
|
||||||
|
|
||||||
class MilterConfigParser(ConfigParser.ConfigParser):
|
class MilterConfigParser(ConfigParser.ConfigParser):
|
||||||
|
|
||||||
@@ -550,8 +499,6 @@ def read_config(list):
|
|||||||
# dspam section
|
# dspam section
|
||||||
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal
|
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal
|
||||||
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
|
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
|
||||||
global whitelist_senders
|
|
||||||
whitelist_senders = cp.getaddrset('dspam','whitelist_senders')
|
|
||||||
dspam_dict = cp.getdefault('dspam','dspam_dict')
|
dspam_dict = cp.getdefault('dspam','dspam_dict')
|
||||||
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
|
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
|
||||||
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
||||||
@@ -566,7 +513,6 @@ def read_config(list):
|
|||||||
# spf section
|
# spf section
|
||||||
global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
|
global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
|
||||||
global spf_accept_softfail,spf_accept_fail,supply_sender,access_file
|
global spf_accept_softfail,spf_accept_fail,supply_sender,access_file
|
||||||
global trusted_forwarder
|
|
||||||
if spf:
|
if spf:
|
||||||
spf.DELEGATE = cp.getdefault('spf','delegate')
|
spf.DELEGATE = cp.getdefault('spf','delegate')
|
||||||
spf_reject_neutral = cp.getlist('spf','reject_neutral')
|
spf_reject_neutral = cp.getlist('spf','reject_neutral')
|
||||||
@@ -576,7 +522,6 @@ def read_config(list):
|
|||||||
spf_reject_noptr = cp.getboolean('spf','reject_noptr')
|
spf_reject_noptr = cp.getboolean('spf','reject_noptr')
|
||||||
supply_sender = cp.getboolean('spf','supply_sender')
|
supply_sender = cp.getboolean('spf','supply_sender')
|
||||||
access_file = cp.getdefault('spf','access_file')
|
access_file = cp.getdefault('spf','access_file')
|
||||||
trusted_forwarder = cp.getlist('spf','trusted_forwarder')
|
|
||||||
srs_config = cp.getdefault('srs','config')
|
srs_config = cp.getdefault('srs','config')
|
||||||
if srs_config: cp.read([srs_config])
|
if srs_config: cp.read([srs_config])
|
||||||
srs_secret = cp.getdefault('srs','secret')
|
srs_secret = cp.getdefault('srs','secret')
|
||||||
@@ -598,7 +543,7 @@ def read_config(list):
|
|||||||
ses = SES.new(secret=srs_secret,expiration=maxage)
|
ses = SES.new(secret=srs_secret,expiration=maxage)
|
||||||
srs_domain = cp.getlist('srs','ses')
|
srs_domain = cp.getlist('srs','ses')
|
||||||
else:
|
else:
|
||||||
srs_domain = cp.getlist('srs','srs')
|
srs_domain = []
|
||||||
srs_domain.append(cp.getdefault('srs','fwdomain'))
|
srs_domain.append(cp.getdefault('srs','fwdomain'))
|
||||||
banned_users = cp.getlist('srs','banned_users')
|
banned_users = cp.getlist('srs','banned_users')
|
||||||
#print srs_domain
|
#print srs_domain
|
||||||
@@ -669,7 +614,7 @@ class SPFPolicy(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def getFailPolicy(self):
|
def getFailPolicy(self):
|
||||||
policy = self.getPolicy('spf-fail:')
|
policy = self.getPolicy('SPF-Fail:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if self.domain in spf_accept_fail:
|
if self.domain in spf_accept_fail:
|
||||||
policy = 'CBV'
|
policy = 'CBV'
|
||||||
@@ -678,7 +623,7 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getNonePolicy(self):
|
def getNonePolicy(self):
|
||||||
policy = self.getPolicy('spf-none:')
|
policy = self.getPolicy('SPF-None:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if spf_reject_noptr:
|
if spf_reject_noptr:
|
||||||
policy = 'REJECT'
|
policy = 'REJECT'
|
||||||
@@ -687,7 +632,7 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getSoftfailPolicy(self):
|
def getSoftfailPolicy(self):
|
||||||
policy = self.getPolicy('spf-softfail:')
|
policy = self.getPolicy('SPF-Softfail:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if self.domain in spf_accept_softfail:
|
if self.domain in spf_accept_softfail:
|
||||||
policy = 'OK'
|
policy = 'OK'
|
||||||
@@ -698,7 +643,7 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getNeutralPolicy(self):
|
def getNeutralPolicy(self):
|
||||||
policy = self.getPolicy('spf-neutral:')
|
policy = self.getPolicy('SPF-Neutral:')
|
||||||
if not policy:
|
if not policy:
|
||||||
if self.domain in spf_reject_neutral:
|
if self.domain in spf_reject_neutral:
|
||||||
policy = 'REJECT'
|
policy = 'REJECT'
|
||||||
@@ -706,80 +651,17 @@ class SPFPolicy(object):
|
|||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getPermErrorPolicy(self):
|
def getPermErrorPolicy(self):
|
||||||
policy = self.getPolicy('spf-permerror:')
|
policy = self.getPolicy('SPF-PermError:')
|
||||||
if not policy:
|
if not policy:
|
||||||
policy = 'REJECT'
|
policy = 'REJECT'
|
||||||
return policy
|
return policy
|
||||||
|
|
||||||
def getPassPolicy(self):
|
def getPassPolicy(self):
|
||||||
policy = self.getPolicy('spf-pass:')
|
policy = self.getPolicy('SPF-Pass:')
|
||||||
if not policy:
|
if not policy:
|
||||||
policy = 'OK'
|
policy = 'OK'
|
||||||
return policy
|
return policy
|
||||||
|
|
||||||
class AddrCache(object):
|
|
||||||
time_format = '%Y%b%d %H:%M:%S %Z'
|
|
||||||
|
|
||||||
def __init__(self,renew=7):
|
|
||||||
self.age = renew
|
|
||||||
|
|
||||||
def load(self,fname,age=0):
|
|
||||||
if not age:
|
|
||||||
age = self.age
|
|
||||||
self.fname = fname
|
|
||||||
cache = {}
|
|
||||||
self.cache = cache
|
|
||||||
now = time.time()
|
|
||||||
try:
|
|
||||||
too_old = now - age*24*60*60 # max age in days
|
|
||||||
for ln in open(self.fname):
|
|
||||||
try:
|
|
||||||
rcpt,ts = ln.strip().split(None,1)
|
|
||||||
l = time.strptime(ts,AddrCache.time_format)
|
|
||||||
t = time.mktime(l)
|
|
||||||
if t > too_old:
|
|
||||||
cache[rcpt.lower()] = (t,None)
|
|
||||||
except:
|
|
||||||
cache[ln.strip().lower()] = (now,None)
|
|
||||||
except IOError: pass
|
|
||||||
|
|
||||||
def has_key(self,sender):
|
|
||||||
try:
|
|
||||||
ts,res = self.cache[sender.lower()]
|
|
||||||
too_old = time.time() - self.age*24*60*60 # max age in days
|
|
||||||
if ts > too_old:
|
|
||||||
return True
|
|
||||||
del self.cache[sender.lower()]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __getitem__(self,sender):
|
|
||||||
ts,res = self.cache[sender.lower()]
|
|
||||||
too_old = time.time() - self.age*24*60*60 # max age in days
|
|
||||||
if ts > too_old:
|
|
||||||
return res
|
|
||||||
del self.cache[sender.lower()]
|
|
||||||
raise KeyError, sender
|
|
||||||
|
|
||||||
def __setitem__(self,sender,res):
|
|
||||||
lsender = sender.lower()
|
|
||||||
now = time.time()
|
|
||||||
cached = self.has_key(sender)
|
|
||||||
if not cached:
|
|
||||||
self.cache[lsender] = (now,res)
|
|
||||||
if not res:
|
|
||||||
s = time.strftime(AddrCache.time_format,time.localtime(now))
|
|
||||||
print >>open(self.fname,'a'),sender,s # log refreshed senders
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.cache)
|
|
||||||
|
|
||||||
cbv_cache = AddrCache(renew=7)
|
|
||||||
cbv_cache.load('send_dsn.log',age=7)
|
|
||||||
auto_whitelist = AddrCache(renew=30)
|
|
||||||
auto_whitelist.load('auto_whitelist.log',age=120)
|
|
||||||
|
|
||||||
class bmsMilter(Milter.Milter):
|
class bmsMilter(Milter.Milter):
|
||||||
"""Milter to replace attachments poisonous to Windows with a WARNING message,
|
"""Milter to replace attachments poisonous to Windows with a WARNING message,
|
||||||
check SPF, and other anti-forgery features, and implement wiretapping
|
check SPF, and other anti-forgery features, and implement wiretapping
|
||||||
@@ -812,15 +694,14 @@ class bmsMilter(Milter.Milter):
|
|||||||
|
|
||||||
# addheader can only be called from eom(). This accumulates added headers
|
# addheader can only be called from eom(). This accumulates added headers
|
||||||
# which can then be applied by alter_headers()
|
# which can then be applied by alter_headers()
|
||||||
def add_header(self,name,val,idx=-1):
|
def add_header(self,name,val):
|
||||||
self.new_headers.append((name,val,idx))
|
self.new_headers.append((name,val))
|
||||||
self.log('%s: %s' % (name,val))
|
self.log('%s: %s' % (name,val))
|
||||||
|
|
||||||
def connect(self,hostname,unused,hostaddr):
|
def connect(self,hostname,unused,hostaddr):
|
||||||
self.internal_connection = False
|
self.internal_connection = False
|
||||||
self.trusted_relay = False
|
self.trusted_relay = False
|
||||||
# sometimes people put extra space in sendmail config, so we strip
|
self.receiver = self.getsymval('j')
|
||||||
self.receiver = self.getsymval('j').strip()
|
|
||||||
if hostaddr and len(hostaddr) > 0:
|
if hostaddr and len(hostaddr) > 0:
|
||||||
ipaddr = hostaddr[0]
|
ipaddr = hostaddr[0]
|
||||||
for pat in internal_connect:
|
for pat in internal_connect:
|
||||||
@@ -897,7 +778,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.hidepath = False
|
self.hidepath = False
|
||||||
self.discard = False
|
self.discard = False
|
||||||
self.dspam = True
|
self.dspam = True
|
||||||
self.whitelist = False
|
|
||||||
self.reject_spam = True
|
self.reject_spam = True
|
||||||
self.data_allowed = True
|
self.data_allowed = True
|
||||||
self.trust_received = self.trusted_relay
|
self.trust_received = self.trusted_relay
|
||||||
@@ -907,7 +787,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.new_headers = []
|
self.new_headers = []
|
||||||
self.recipients = []
|
self.recipients = []
|
||||||
self.cbv_needed = None
|
self.cbv_needed = None
|
||||||
self.whitelist_sender = False
|
|
||||||
t = parse_addr(f)
|
t = parse_addr(f)
|
||||||
if len(t) == 2: t[1] = t[1].lower()
|
if len(t) == 2: t[1] = t[1].lower()
|
||||||
self.canon_from = '@'.join(t)
|
self.canon_from = '@'.join(t)
|
||||||
@@ -947,28 +826,22 @@ class bmsMilter(Milter.Milter):
|
|||||||
if len(t) == 2:
|
if len(t) == 2:
|
||||||
user,domain = t
|
user,domain = t
|
||||||
if not self.internal_connection:
|
if not self.internal_connection:
|
||||||
if not self.trusted_relay:
|
|
||||||
for pat in internal_domains:
|
for pat in internal_domains:
|
||||||
if fnmatchcase(domain,pat):
|
if fnmatchcase(domain,pat):
|
||||||
self.log("REJECT: spam from self",pat)
|
self.log("REJECT: spam from self",pat)
|
||||||
self.setreply('550','5.7.1','I hate talking to myself.')
|
self.setreply('550','5.7.1','I hate talking to myself.')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
else:
|
elif internal_domains:
|
||||||
if internal_domains:
|
|
||||||
for pat in internal_domains:
|
for pat in internal_domains:
|
||||||
if fnmatchcase(domain,pat): break
|
if fnmatchcase(domain,pat): break
|
||||||
else:
|
else:
|
||||||
self.log("REJECT: zombie PC at ",self.connectip,
|
self.log("REJECT: zombie PC at ",self.connectip," sending MAIL FROM ",
|
||||||
" sending MAIL FROM ",self.canon_from)
|
self.canon_from)
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
'Your PC is using an unauthorized MAIL FROM.',
|
'Your PC is using an unauthorized MAIL FROM.',
|
||||||
'It is either badly misconfigured or controlled by organized crime.'
|
'It is either badly misconfigured or controlled by organized crime.'
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
wl_users = whitelist_senders.get(domain,())
|
|
||||||
if user in wl_users or '' in wl_users:
|
|
||||||
self.whitelist_sender = True
|
|
||||||
|
|
||||||
self.rejectvirus = domain in reject_virus_from
|
self.rejectvirus = domain in reject_virus_from
|
||||||
if user in wiretap_users.get(domain,()):
|
if user in wiretap_users.get(domain,()):
|
||||||
self.add_recipient(wiretap_dest)
|
self.add_recipient(wiretap_dest)
|
||||||
@@ -980,32 +853,22 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.dspam = False
|
self.dspam = False
|
||||||
else:
|
else:
|
||||||
self.rejectvirus = False
|
self.rejectvirus = False
|
||||||
domain = None
|
|
||||||
if not self.hello_name:
|
if not self.hello_name:
|
||||||
self.log("REJECT: missing HELO")
|
self.log("REJECT: missing HELO")
|
||||||
self.setreply('550','5.7.1',"It's polite to say HELO first.")
|
self.setreply('550','5.7.1',"It's polite to say HELO first.")
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
self.umis = None
|
|
||||||
if not (self.internal_connection or self.trusted_relay) \
|
if not (self.internal_connection or self.trusted_relay) \
|
||||||
and self.connectip and spf:
|
and self.connectip and spf:
|
||||||
return self.check_spf()
|
return self.check_spf()
|
||||||
else:
|
|
||||||
self.spf = None
|
self.spf = None
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def check_spf(self):
|
def check_spf(self):
|
||||||
receiver = self.receiver
|
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 == 'pass':
|
|
||||||
self.log("TRUSTED_FORWARDER:",tf)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
q = spf.query(self.connectip,self.canon_from,self.hello_name,
|
q = spf.query(self.connectip,self.canon_from,self.hello_name,
|
||||||
receiver=receiver,strict=False)
|
receiver=receiver,strict=False)
|
||||||
q.set_default_explanation(
|
q.set_default_explanation(
|
||||||
'SPF fail: see http://openspf.org/why.html?sender=%s&ip=%s' % (q.s,q.i))
|
'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i))
|
||||||
res,code,txt = q.check()
|
res,code,txt = q.check()
|
||||||
q.result = res
|
q.result = res
|
||||||
if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext:
|
if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext:
|
||||||
@@ -1035,13 +898,14 @@ class bmsMilter(Milter.Milter):
|
|||||||
if spf_best_guess and res == 'none':
|
if spf_best_guess and res == 'none':
|
||||||
#self.log('SPF: no record published, guessing')
|
#self.log('SPF: no record published, guessing')
|
||||||
q.set_default_explanation(
|
q.set_default_explanation(
|
||||||
'SPF guess: see http://openspf.org/why.html')
|
'SPF guess: see http://spf.pobox.com/why.html')
|
||||||
# best_guess should not result in fail
|
# best_guess should not result in fail
|
||||||
if self.missing_ptr:
|
if self.missing_ptr:
|
||||||
# ignore dynamic PTR for best guess
|
# ignore dynamic PTR for best guess
|
||||||
res,code,txt = q.best_guess('v=spf1 a/24 mx/24')
|
res,code,txt = q.best_guess('v=spf1 a/24 mx/24')
|
||||||
else:
|
else:
|
||||||
res,code,txt = q.best_guess()
|
res,code,txt = q.best_guess()
|
||||||
|
receiver += ': guessing'
|
||||||
if q.perm_error: # FIXME: should never happen?
|
if q.perm_error: # FIXME: should never happen?
|
||||||
res,code,txt = q.perm_error.ext # extended result
|
res,code,txt = q.perm_error.ext # extended result
|
||||||
txt = 'EXT: ' + txt
|
txt = 'EXT: ' + txt
|
||||||
@@ -1055,11 +919,10 @@ class bmsMilter(Milter.Milter):
|
|||||||
elif policy != 'OK':
|
elif policy != 'OK':
|
||||||
self.log('REJECT: no PTR, HELO or SPF')
|
self.log('REJECT: no PTR, HELO or SPF')
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
"You must have a valid HELO or publish SPF: http://www.openspf.org ",
|
"You must have a reverse lookup or publish SPF: http://spf.pobox.com",
|
||||||
"Contact your mail administrator IMMEDIATELY! Your mail server is",
|
"Contact your mail administrator IMMEDIATELY! Your mail server is",
|
||||||
"severely misconfigured. It has no PTR record (dynamic PTR records",
|
"severely misconfigured. It has no PTR record (dynamic PTR records",
|
||||||
"that contain your IP don't count), an invalid or dynamic HELO, ",
|
"that contain your IP don't count), an invalid HELO, and no SPF record."
|
||||||
"and no SPF record."
|
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if res in ('deny', 'fail'):
|
if res in ('deny', 'fail'):
|
||||||
@@ -1089,16 +952,15 @@ class bmsMilter(Milter.Milter):
|
|||||||
'notify your administrator of the problem immediately.'
|
'notify your administrator of the problem immediately.'
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if res == 'neutral':
|
if res == 'neutral' and q.o in spf_reject_neutral:
|
||||||
policy = p.getNeutralPolicy()
|
policy = p.getNeutralPolicy()
|
||||||
if policy == 'CBV' and hres == 'pass':
|
if policy == 'CBV' and hres == 'pass':
|
||||||
if self.mailfrom != '<>':
|
if self.mailfrom != '<>':
|
||||||
self.cbv_needed = q
|
self.cbv_needed = q
|
||||||
q.result = res # select neutral DSN template
|
|
||||||
elif policy != 'OK':
|
elif policy != 'OK':
|
||||||
self.log('REJECT: SPF neutral for',q.s)
|
self.log('REJECT: SPF neutral for',q.s)
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
'mail from %s must pass SPF: http://openspf.org/why.html' % q.o,
|
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o,
|
||||||
'The %s domain is one that spammers love to forge. Due to' % 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 volume of forged mail, we can only accept mail that',
|
||||||
'the SPF record for %s explicitly designates as legitimate.' % q.o,
|
'the SPF record for %s explicitly designates as legitimate.' % q.o,
|
||||||
@@ -1123,21 +985,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
|
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
|
||||||
self.setreply(str(code),'4.3.0',txt)
|
self.setreply(str(code),'4.3.0',txt)
|
||||||
return Milter.TEMPFAIL
|
return Milter.TEMPFAIL
|
||||||
self.add_header('Received-SPF',q.get_header(q.result,receiver),0)
|
self.add_header('Received-SPF',q.get_header(res,receiver))
|
||||||
if res != q.result:
|
|
||||||
self.add_header('X-Guessed-SPF',res,0)
|
|
||||||
self.spf = q
|
self.spf = q
|
||||||
if res == 'pass' and auto_whitelist.has_key(self.canon_from):
|
|
||||||
self.whitelist = True
|
|
||||||
self.log("WHITELIST",self.canon_from)
|
|
||||||
if gossip:
|
|
||||||
if res == 'pass':
|
|
||||||
qual = 'SPF'
|
|
||||||
else:
|
|
||||||
qual = self.connectip
|
|
||||||
self.umis = gossip.umis(q.o+qual,self.id+time.time())
|
|
||||||
res,hdr,val = gossip_node.query(self.umis,q.o,qual,1)
|
|
||||||
self.add_header(hdr,val)
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
# hide_path causes a copy of the message to be saved - until we
|
# hide_path causes a copy of the message to be saved - until we
|
||||||
@@ -1149,9 +998,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log('DISCARD: RCPT TO:',to,str)
|
self.log('DISCARD: RCPT TO:',to,str)
|
||||||
return Milter.DISCARD
|
return Milter.DISCARD
|
||||||
self.log("rcpt to",to,str)
|
self.log("rcpt to",to,str)
|
||||||
t = parse_addr(to)
|
t = parse_addr(to.lower())
|
||||||
if len(t) == 2:
|
if len(t) == 2:
|
||||||
t[1] = t[1].lower()
|
|
||||||
user,domain = t
|
user,domain = t
|
||||||
if self.is_bounce and srs and domain in srs_domain:
|
if self.is_bounce and srs and domain in srs_domain:
|
||||||
oldaddr = '@'.join(parse_addr(to))
|
oldaddr = '@'.join(parse_addr(to))
|
||||||
@@ -1166,7 +1014,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
newaddr = srs.reverse(oldaddr)
|
newaddr = srs.reverse(oldaddr)
|
||||||
# Currently, a sendmail map reverses SRS. We just log it here.
|
# Currently, a sendmail map reverses SRS. We just log it here.
|
||||||
self.log("srs rcpt:",newaddr)
|
self.log("srs rcpt:",newaddr)
|
||||||
self.dspam = False # verified as reply to mail we sent
|
|
||||||
except:
|
except:
|
||||||
if not (self.internal_connection or self.trusted_relay):
|
if not (self.internal_connection or self.trusted_relay):
|
||||||
if srsre.match(oldaddr):
|
if srsre.match(oldaddr):
|
||||||
@@ -1180,8 +1027,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.data_allowed = not srs_reject_spoofed
|
self.data_allowed = not srs_reject_spoofed
|
||||||
|
|
||||||
# non DSN mail to SRS address will bounce due to invalid local part
|
# non DSN mail to SRS address will bounce due to invalid local part
|
||||||
canon_to = '@'.join(t)
|
self.recipients.append('@'.join(t))
|
||||||
self.recipients.append(canon_to)
|
|
||||||
users = check_user.get(domain)
|
users = check_user.get(domain)
|
||||||
if self.discard:
|
if self.discard:
|
||||||
self.del_recipient(to)
|
self.del_recipient(to)
|
||||||
@@ -1198,7 +1044,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
if not domain in dspam_reject:
|
if not domain in dspam_reject:
|
||||||
self.reject_spam = False
|
self.reject_spam = False
|
||||||
self.smart_alias(to)
|
self.smart_alias(to)
|
||||||
# get recipient after virtusertable aliasing
|
|
||||||
#rcpt = self.getsymval("{rcpt_addr}")
|
#rcpt = self.getsymval("{rcpt_addr}")
|
||||||
#self.log("rcpt-addr",rcpt);
|
#self.log("rcpt-addr",rcpt);
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
@@ -1270,7 +1115,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.log('REJECT: bounce with no SRS encoding')
|
self.log('REJECT: bounce with no SRS encoding')
|
||||||
self.setreply('550','5.7.1',
|
self.setreply('550','5.7.1',
|
||||||
"I did not send you that message. Please consider implementing SPF",
|
"I did not send you that message. Please consider implementing SPF",
|
||||||
"(http://openspf.org) to avoid bouncing mail to spoofed senders.",
|
"(http://openspf.com) to avoid bouncing mail to spoofed senders.",
|
||||||
"Thank you."
|
"Thank you."
|
||||||
)
|
)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
@@ -1291,11 +1136,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
rc = self.check_header(name,val)
|
rc = self.check_header(name,val)
|
||||||
if rc != Milter.CONTINUE: return rc
|
if rc != Milter.CONTINUE: return rc
|
||||||
elif self.whitelist_sender and lname == 'subject':
|
|
||||||
# check for AutoReplys
|
|
||||||
if val.lower().find('autoreply:') >= 0:
|
|
||||||
self.whitelist_sender = False
|
|
||||||
|
|
||||||
# log selected headers
|
# log selected headers
|
||||||
if log_headers or lname in ('subject','x-mailer'):
|
if log_headers or lname in ('subject','x-mailer'):
|
||||||
self.log('%s: %s' % (name,val))
|
self.log('%s: %s' % (name,val))
|
||||||
@@ -1317,19 +1157,18 @@ class bmsMilter(Milter.Milter):
|
|||||||
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
|
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
|
||||||
if not self.data_allowed:
|
if not self.data_allowed:
|
||||||
return self.forged_bounce()
|
return self.forged_bounce()
|
||||||
for name,val,idx in self.new_headers:
|
for name,val in self.new_headers:
|
||||||
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
|
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
|
||||||
self.fp.write("\n") # terminate headers
|
self.fp.write("\n") # terminate headers
|
||||||
# log when neither sender nor from domains matches mail from domain
|
# log when neither sender nor from domains matches mail from domain
|
||||||
if supply_sender and self.mailfrom != '<>' and not self.internal_connection:
|
if supply_sender and self.mailfrom != '<>':
|
||||||
mf_domain = self.canon_from.split('@')[-1]
|
mf_domain = self.canon_from.split('@')[-1]
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
msg = rfc822.Message(self.fp)
|
msg = rfc822.Message(self.fp)
|
||||||
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
|
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
|
||||||
t = parse_addr(hf)
|
t = parse_addr(hf)
|
||||||
if len(t) == 2:
|
if len(t) == 2 and t[1].lower() == mf_domain:
|
||||||
hd = t[1].lower()
|
break
|
||||||
if hd == mf_domain or mf_domain.endswith('.'+hd): break
|
|
||||||
else:
|
else:
|
||||||
for f in msg.getallmatchingheaders('from'):
|
for f in msg.getallmatchingheaders('from'):
|
||||||
self.log(f)
|
self.log(f)
|
||||||
@@ -1355,7 +1194,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
dspam.DSF_CHAINED|dspam.DSF_CLASSIFY)
|
dspam.DSF_CHAINED|dspam.DSF_CLASSIFY)
|
||||||
try:
|
try:
|
||||||
ds.process(headers)
|
ds.process(headers)
|
||||||
if ds.probability > 0.93 and self.dspam and not self.whitelist:
|
if ds.probability > 0.93 and self.dspam:
|
||||||
self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability)
|
self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability)
|
||||||
self.setreply('550','5.7.1','Your Message looks spammy')
|
self.setreply('550','5.7.1','Your Message looks spammy')
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
@@ -1429,8 +1268,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
ds.headerchange = self._headerChange
|
ds.headerchange = self._headerChange
|
||||||
modified = False
|
modified = False
|
||||||
for rcpt in self.recipients:
|
for rcpt in self.recipients:
|
||||||
if dspam_users.has_key(rcpt.lower()):
|
if dspam_users.has_key(rcpt):
|
||||||
user = dspam_users.get(rcpt.lower())
|
user = dspam_users.get(rcpt)
|
||||||
if user:
|
if user:
|
||||||
try:
|
try:
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
@@ -1462,10 +1301,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.fp = None
|
self.fp = None
|
||||||
if len(self.recipients) > 1:
|
if len(self.recipients) > 1:
|
||||||
self.log("HONEYPOT:",rcpt,'SCREENED')
|
self.log("HONEYPOT:",rcpt,'SCREENED')
|
||||||
if self.whitelist:
|
if self.spf:
|
||||||
# don't train when recipients includes honeypot
|
|
||||||
return False
|
|
||||||
if self.spf and self.mailfrom != '<>':
|
|
||||||
# check that sender accepts quarantine DSN
|
# check that sender accepts quarantine DSN
|
||||||
msg = mime.message_from_file(StringIO.StringIO(txt))
|
msg = mime.message_from_file(StringIO.StringIO(txt))
|
||||||
rc = self.send_dsn(self.spf,msg,'quarantine.txt')
|
rc = self.send_dsn(self.spf,msg,'quarantine.txt')
|
||||||
@@ -1479,12 +1315,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
force_result=dspam.DSR_ISSPAM)
|
force_result=dspam.DSR_ISSPAM)
|
||||||
self.log("HONEYPOT:",rcpt)
|
self.log("HONEYPOT:",rcpt)
|
||||||
return Milter.DISCARD
|
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)
|
|
||||||
else:
|
|
||||||
txt = ds.check_spam(user,txt,self.recipients)
|
txt = ds.check_spam(user,txt,self.recipients)
|
||||||
if not txt:
|
if not txt:
|
||||||
# DISCARD if quarrantined for any recipient. It
|
# DISCARD if quarrantined for any recipient. It
|
||||||
@@ -1509,13 +1339,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
screener = dspam_screener[self.id % len(dspam_screener)]
|
screener = dspam_screener[self.id % len(dspam_screener)]
|
||||||
if not ds.check_spam(screener,txt,self.recipients,
|
if not ds.check_spam(screener,txt,self.recipients,
|
||||||
classify=True,quarantine=False):
|
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:
|
if self.reject_spam:
|
||||||
self.log("DSPAM:",screener,
|
self.log("DSPAM:",screener,
|
||||||
'REJECT: X-DSpam-Score: %f' % ds.probability)
|
'REJECT: X-DSpam-Score: %f' % ds.probability)
|
||||||
@@ -1523,7 +1346,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.fp = None
|
self.fp = None
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
self.log("DSPAM:",screener,"SCREENED")
|
self.log("DSPAM:",screener,"SCREENED")
|
||||||
if self.spf and self.mailfrom != '<>':
|
if self.spf:
|
||||||
# check that sender accepts quarantine DSN
|
# check that sender accepts quarantine DSN
|
||||||
self.fp.seek(0)
|
self.fp.seek(0)
|
||||||
msg = mime.message_from_file(self.fp)
|
msg = mime.message_from_file(self.fp)
|
||||||
@@ -1546,8 +1369,6 @@ class bmsMilter(Milter.Milter):
|
|||||||
# analyze external mail for spam
|
# analyze external mail for spam
|
||||||
spam_checked = self.check_spam() # tag or quarantine for spam
|
spam_checked = self.check_spam() # tag or quarantine for spam
|
||||||
if not self.fp:
|
if not self.fp:
|
||||||
if gossip and self.umis:
|
|
||||||
gossip_node.feedback(self.umis,1)
|
|
||||||
return spam_checked
|
return spam_checked
|
||||||
|
|
||||||
# analyze all mail for dangerous attachments and scripts
|
# analyze all mail for dangerous attachments and scripts
|
||||||
@@ -1610,25 +1431,8 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.addrcpt(self.mailfrom)
|
self.addrcpt(self.mailfrom)
|
||||||
else:
|
else:
|
||||||
self.alter_recipients(self.discard_list,self.redirect_list)
|
self.alter_recipients(self.discard_list,self.redirect_list)
|
||||||
# auto whitelist original recipients
|
for name,val in self.new_headers:
|
||||||
if not defanged and self.whitelist_sender:
|
self.addheader(name,val)
|
||||||
for canon_to in self.recipients:
|
|
||||||
user,domain = canon_to.split('@')
|
|
||||||
if internal_domains:
|
|
||||||
for pat in internal_domains:
|
|
||||||
if fnmatchcase(domain,pat): break
|
|
||||||
else:
|
|
||||||
auto_whitelist[canon_to] = None
|
|
||||||
self.log('Auto-Whitelist:',canon_to)
|
|
||||||
else:
|
|
||||||
auto_whitelist[canon_to] = None
|
|
||||||
self.log('Auto-Whitelist:',canon_to)
|
|
||||||
|
|
||||||
for name,val,idx in self.new_headers:
|
|
||||||
try:
|
|
||||||
self.addheader(name,val,idx)
|
|
||||||
except:
|
|
||||||
self.addheader(name,val) # older sendmail can't insheader
|
|
||||||
|
|
||||||
if self.cbv_needed:
|
if self.cbv_needed:
|
||||||
q = self.cbv_needed
|
q = self.cbv_needed
|
||||||
@@ -1670,10 +1474,7 @@ class bmsMilter(Milter.Milter):
|
|||||||
buf = out.read(8192)
|
buf = out.read(8192)
|
||||||
if len(buf) == 0: break
|
if len(buf) == 0: break
|
||||||
self.replacebody(buf) # feed modified message to sendmail
|
self.replacebody(buf) # feed modified message to sendmail
|
||||||
if spam_checked:
|
if spam_checked: self.log("dspam")
|
||||||
if gossip and self.umis:
|
|
||||||
gossip_node.feedback(self.umis,0)
|
|
||||||
self.log("dspam")
|
|
||||||
return rc
|
return rc
|
||||||
finally:
|
finally:
|
||||||
out.close()
|
out.close()
|
||||||
@@ -1706,6 +1507,9 @@ class bmsMilter(Milter.Milter):
|
|||||||
self.setreply('550','5.7.1',*desc.splitlines())
|
self.setreply('550','5.7.1',*desc.splitlines())
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
cbv_cache[sender] = res
|
cbv_cache[sender] = res
|
||||||
|
if not cached:
|
||||||
|
s = time.strftime(time_format,time.localtime())
|
||||||
|
print >>open('send_dsn.log','a'),sender,s # log who we sent DSNs to
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
-173
@@ -1,173 +0,0 @@
|
|||||||
Title: Recent Changes
|
|
||||||
|
|
||||||
<h2> Recent Changes </h2>
|
|
||||||
|
|
||||||
Python milter has been moved to
|
|
||||||
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
|
|
||||||
project</a> for development and release downloads.
|
|
||||||
|
|
||||||
<h3> 0.8.5 </h3>
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
<h3> GOSSiP </h3>
|
|
||||||
An alpha release of <a href="pygossip.html">pygossip</a> 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.
|
|
||||||
|
|
||||||
<h3> New website design </h3>
|
|
||||||
|
|
||||||
Hey, I'm no artist, so I just used the
|
|
||||||
<a href="http://ht2html.sourceforge.net/">ht2html</a> package
|
|
||||||
by <a href="http://barry.wooz.org/">Barry Warsaw</a>. The mascot
|
|
||||||
is by <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>,
|
|
||||||
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
|
|
||||||
<a href="http://alphard.ethz.ch/hafner/PPS/PPS2002/Maxwell/simulation.htm">
|
|
||||||
simulation</a> that lets you try your hand at sorting molecules.
|
|
||||||
|
|
||||||
<h3> 0.8.4 </h3>
|
|
||||||
|
|
||||||
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().
|
|
||||||
|
|
||||||
<h3> 0.8.3 </h3>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
<h3> 0.8.2 </h3>
|
|
||||||
|
|
||||||
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
|
|
||||||
in line with the newly official RFC. It adds
|
|
||||||
<a href="http://ses.codeshare.ca/">SES</a>
|
|
||||||
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.
|
|
||||||
|
|
||||||
<h3> 0.8.0 </h3>
|
|
||||||
|
|
||||||
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
|
|
||||||
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.
|
|
||||||
|
|
||||||
<h3> Older Releases </h3>
|
|
||||||
|
|
||||||
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.
|
|
||||||
<p>
|
|
||||||
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 <code>rhsbl</code> sendmail hack so that spammer domains
|
|
||||||
can be blacklisted. With the RPM installed, add a line like the following
|
|
||||||
to your <code>sendmail.mc</code>.
|
|
||||||
<pre>
|
|
||||||
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
|
|
||||||
</pre>
|
|
||||||
<p>
|
|
||||||
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 <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
|
|
||||||
framework provides a custom DNS server - but I
|
|
||||||
would like a smaller implementation for our use.
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
<p>
|
|
||||||
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 <a
|
|
||||||
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
|
|
||||||
the problem with sender-ID</a>, and Debian <a
|
|
||||||
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
|
|
||||||
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
|
|
||||||
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
|
|
||||||
software in general</a> and the <a
|
|
||||||
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
|
|
||||||
particular</a>, Python milter will not be able to implement sender-ID in its
|
|
||||||
current form. This was, no doubt, Microsoft's intent all along.
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="http://openspf.com">
|
|
||||||
<img src="SPF.gif" align=left alt="SPF logo"></a>
|
|
||||||
Release 0.6.6 adds support for <a href="http://openspf.com/">SPF</a>,
|
|
||||||
a protocol to prevent forging of the envelope from address.
|
|
||||||
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
|
|
||||||
The included spf.py module is an updated version of the original 1.6
|
|
||||||
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
|
|
||||||
The updated version tracks the draft RFC and test suite.
|
|
||||||
<p>
|
|
||||||
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
|
|
||||||
<p>
|
|
||||||
Release 0.6.1 adds a full milter based dspam application.
|
|
||||||
<p>
|
|
||||||
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
|
|
||||||
dspam bayes filter project</a> and <a href="dspam.html">
|
|
||||||
packaged it for python</a>.
|
|
||||||
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 <a href="dspam.html">DSPAM</a> 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."
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
Title: Credits
|
|
||||||
|
|
||||||
<h1> CREDITS </h1>
|
|
||||||
|
|
||||||
<a href="mailto:Jim Niemira <urmane@urmane.org>">Jim Niemira</a>
|
|
||||||
wrote the original C module and some quick
|
|
||||||
and dirty python to use it.
|
|
||||||
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">Stuart D. Gathman</a>
|
|
||||||
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.
|
|
||||||
|
|
||||||
<h2>Other contributors (in random order):</h2>
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt> <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>
|
|
||||||
<dd>for the pymilter mascot image of
|
|
||||||
<a href="http://maxwelld.netfirms.com/">
|
|
||||||
Maxwell's daemon</a>
|
|
||||||
<dt>Stephen Figgins
|
|
||||||
<dd>for reporting problems building with sendmail-8.12, and when
|
|
||||||
building milter.so for the first time.
|
|
||||||
<dt>Dave MacQuigg
|
|
||||||
<dd>for noticing that smfi_insheader wasn't supported, and creating
|
|
||||||
a template to help first time pymilter users create their own milter.
|
|
||||||
<dt>Terence Way
|
|
||||||
<dd>for providing a Python port of SPF
|
|
||||||
<dt>Scott Kitterman
|
|
||||||
<dd>for doing lots of testing and debugging of SPF against draft standard,
|
|
||||||
and for putting up a <a href="http://www.kitterman.com/spf/validate.html">
|
|
||||||
web page that validates SPF</a> records using spf.py
|
|
||||||
<dt>Alexander Kourakos
|
|
||||||
<dd>for plugging several memory leaks
|
|
||||||
<dt>George Graf at Vienna University of Economics and Business Administration
|
|
||||||
<dd>for handling None passed to setreply and chgheader.
|
|
||||||
<dt>Deron Meranda
|
|
||||||
<dd>for IPv6 patches
|
|
||||||
<dt>Jason Erikson
|
|
||||||
<dd>for handling NULL hostaddr in connect callback.
|
|
||||||
<dt>John Draper
|
|
||||||
<dd>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.
|
|
||||||
<dt>Eric S. Johansson
|
|
||||||
<dd>for helpful design discussions while working on camram
|
|
||||||
<dt>Alex Savguira
|
|
||||||
<dd>for finding bugs with international headers and
|
|
||||||
suggesting the scan_zip option.
|
|
||||||
<dt><a href="http://www.bmsi.com">Business Management Systems</a>
|
|
||||||
<dd>for hosting the website, and providing paying clients who need milter
|
|
||||||
service so I can work on it as part of my day job.
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
If I have left anybody out, send me a reminder:
|
|
||||||
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">stuart@bmsi.com</a>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
Title: Python Milter Log Documentation
|
|
||||||
<style>
|
|
||||||
DT { font-weight: bolder; padding-top: 1em }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<h1> Milter Log Documentation </h1>
|
|
||||||
|
|
||||||
The milter log from the bms.py application has a variety of "tags" in it that
|
|
||||||
indicate what it did.
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt> DSPAM: honeypot SCREENED
|
|
||||||
<dd> message was quarantined to the honeypot quarantine
|
|
||||||
|
|
||||||
<dt> REJECT: hello SPF: fail 550 access denied
|
|
||||||
<dt> REJECT: hello SPF: softfail 550 domain in transition
|
|
||||||
<dt> REJECT: hello SPF: neutral 550 access neither permitted nor denied
|
|
||||||
<dd> message was rejected because there was an SPF policy for the
|
|
||||||
HELO name, and it did not pass.
|
|
||||||
|
|
||||||
<dt> CBV: sender-17-44662668-643@bluepenmagic.com
|
|
||||||
<dd> we performed a call back verification
|
|
||||||
|
|
||||||
<dt> dspam
|
|
||||||
<dd> dspam identifier was added to the message
|
|
||||||
|
|
||||||
<dt> REJECT: spam from self: jsconnor.com
|
|
||||||
<dd> message was reject because HELO was us (jsconnor.com)
|
|
||||||
|
|
||||||
<dt> INNOC: richh
|
|
||||||
<dd> message was used to update richh's dspam dictionary
|
|
||||||
|
|
||||||
<dt> HONEYPOT: michaelb@jsconnor.com
|
|
||||||
<dd> message was sent to a honeypot address (michaelb@jsconnor.com), the
|
|
||||||
message was added to the honeypot dspam dictionary as spam
|
|
||||||
|
|
||||||
<dt> REJECT: numeric hello name: 63.217.19.146
|
|
||||||
<dd> message was rejected because helo name was invalid (numeric)
|
|
||||||
|
|
||||||
<dt> eom
|
|
||||||
<dd> message was successfully received
|
|
||||||
|
|
||||||
<dt> TEMPFAIL: CBV: 450 No MX servers available
|
|
||||||
<dd> we tried to do a call back verification but could not look up
|
|
||||||
MX record, we told the sender to try again later
|
|
||||||
|
|
||||||
<dt> CBV: info@emailpizzahut.com (cached)
|
|
||||||
<dd> call back verification was needed, we had already done it recently
|
|
||||||
|
|
||||||
<dt> abort after 0 body chars
|
|
||||||
<dd> sender hung up on us
|
|
||||||
|
|
||||||
<dt> REJECT: SPF fail 550 SPF fail: see
|
|
||||||
http://openspf.com/why.html?sender=m.hendersonxk@163.net&ip=213.47.161.100
|
|
||||||
<dd> message was reject because its sender's spf policy said to
|
|
||||||
|
|
||||||
<dt> REJECT: Subject: Cialis - No prescription needed!
|
|
||||||
<dd> message was rejected because its subject contained a bad expression
|
|
||||||
|
|
||||||
<dt> DSPAM: tonyc tonyc@jsconnor.com
|
|
||||||
<dd> message was sent to tonyc@jsconnor.com and it was identified as spam
|
|
||||||
and placed in the tonyc dspam quarantine
|
|
||||||
|
|
||||||
<dt> REJECT: CBV: 550 calvinalstonis@ix.netcom.com...User unknown
|
|
||||||
<dt> REJECT: CBV: 553 sorry, that domain isn't in my list
|
|
||||||
<dt> REJECT: CBV: 554 delivery error: dd This user doesn't have an account
|
|
||||||
<dd> message was rejected because call back verification gave us a fatal
|
|
||||||
error
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
Please add more tags to this list if you know of any. Thanks.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,99 +0,0 @@
|
|||||||
Title: Requirements
|
|
||||||
|
|
||||||
<h2> Requirements </h2>
|
|
||||||
|
|
||||||
<menu>
|
|
||||||
<li> 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.
|
|
||||||
<li> Python must be configured with thread support. This is because
|
|
||||||
pymilter uses sendmail's libmilter which requires thread support.
|
|
||||||
<li> 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.
|
|
||||||
<li> 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.
|
|
||||||
<li> 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.
|
|
||||||
<li> Python milter has been tested against sendmail-8.11 through sendmail-8.13.
|
|
||||||
<li> 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.
|
|
||||||
<li> 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!
|
|
||||||
</menu>
|
|
||||||
|
|
||||||
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
|
|
||||||
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
|
|
||||||
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
|
|
||||||
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
|
|
||||||
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
|
|
||||||
to represent the stuff that comes with AIX. You might also want
|
|
||||||
my <a href="/aix/python.spec">python-2.1.1</a> 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 <a href="/aix">AIX RPM directory</a>.
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
|
|
||||||
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
|
|
||||||
|
|
||||||
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 <a href="mfapi.h">mfapi.h</a>
|
|
||||||
header for sendmail-8.6.11 from here and manually install it as
|
|
||||||
<code>/usr/include/libmilter/mfapi.h</code>.
|
|
||||||
<p>
|
|
||||||
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 <code>libs = ["milter", "smutil"]</code> in setup.py.
|
|
||||||
<p>
|
|
||||||
If you have installed python2, and want
|
|
||||||
python-milter to use python2, add <code>python=python2</code> to setup.cfg
|
|
||||||
and build with <code>python2 setup.py bdist_rpm</code>.
|
|
||||||
|
|
||||||
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
|
|
||||||
|
|
||||||
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:
|
|
||||||
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
|
|
||||||
sendmail-rhmilter.spec</a>. The <a
|
|
||||||
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
|
|
||||||
SRPM for sendmail-8.11.6</a> is available from
|
|
||||||
<a href="http://www.redhat.com">Redhat</a> under
|
|
||||||
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
|
|
||||||
Errata for RH6.2</a>. But that doesn't include the latest security
|
|
||||||
patches since RH6.2 is no longer supported.
|
|
||||||
<p>
|
|
||||||
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
|
|
||||||
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> 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.
|
|
||||||
<p>
|
|
||||||
If you have installed python2, and want
|
|
||||||
python-milter to use python2, add <code>python=python2</code> to setup.cfg
|
|
||||||
and build with <code>python2 setup.py bdist_rpm</code>.
|
|
||||||
You'll need to install the sendmail-devel package to compile milter.
|
|
||||||
+14
-14
@@ -1,4 +1,8 @@
|
|||||||
Title: Python Milter FAQ
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Python Milter FAQ</title>
|
||||||
|
</head><body>
|
||||||
|
|
||||||
<h1> Python Milter <a name=faq>FAQ</a> </h1>
|
<h1> Python Milter <a name=faq>FAQ</a> </h1>
|
||||||
|
|
||||||
@@ -18,7 +22,7 @@ shows you how to install libmilter with a separate invocation of make.
|
|||||||
<li> Q. Why is mfapi.h not found when I try to compile Python milter on
|
<li> Q. Why is mfapi.h not found when I try to compile Python milter on
|
||||||
RedHat 7.2?
|
RedHat 7.2?
|
||||||
<p> A. RedHat forgot to include the header in the RPM. See the
|
<p> A. RedHat forgot to include the header in the RPM. See the
|
||||||
<a href="requirements.html#rh72">RedHat 7.2 requirements</a>.
|
<a href="milter.html#rh72">RedHat 7.2 requirements</a>.
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
<h3> Running Python Milter </h3>
|
<h3> Running Python Milter </h3>
|
||||||
@@ -99,14 +103,13 @@ If you are running bms.py, then the block_chinese option in
|
|||||||
|
|
||||||
<li> Q. Why does sendmail coredump with milters on OpenBSD?
|
<li> Q. Why does sendmail coredump with milters on OpenBSD?
|
||||||
<p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD.
|
<p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD.
|
||||||
OpenBSD users report that this problem has been fixed, so upgrading
|
Use an internet domain socket instead. For example, in
|
||||||
OpenBSD will fix this. Otherwise, you can
|
|
||||||
use an internet domain socket instead. For example, in
|
|
||||||
<code>sendmail.cf</code> use
|
<code>sendmail.cf</code> use
|
||||||
<pre>
|
<pre>
|
||||||
Xpythonfilter, S=inet:1234@localhost
|
Xpythonfilter, S=inet:1234@localhost
|
||||||
</pre>
|
</pre>
|
||||||
and change sample.py accordingly.
|
and change sample.py accordingly.
|
||||||
|
<p> OpenBSD users report that this problem has been fixed.
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
<li> Q. How can I change the bounce message for an invalid recipient?
|
<li> Q. How can I change the bounce message for an invalid recipient?
|
||||||
@@ -140,15 +143,10 @@ Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
|
|||||||
</pre>
|
</pre>
|
||||||
<li> Q. There is a Python traceback in the log file! What happened to
|
<li> Q. There is a Python traceback in the log file! What happened to
|
||||||
my email?
|
my email?
|
||||||
<p> A. By default, when the milter fails with an untrapped exception, a
|
<p> A. When the milter fails with an untrapped exception, a TEMPFAIL
|
||||||
TEMPFAIL result (451) is returned to the sender. The sender will then retry
|
result (451) is returned to the sender. The sender will then retry every
|
||||||
every hour or so for several days. Hopefully, someone will notice the
|
hour or so for several days. Hopefully, someone will notice the
|
||||||
traceback, and workaround or fix the problem. Beginning with milter-0.8.2,
|
traceback, and workaround or fix the problem.
|
||||||
you can call <code>milter.set_exception_policy(milter.CONTINUE)</code>
|
|
||||||
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
|
|
||||||
<code>milter.REJECT</code>.
|
|
||||||
|
|
||||||
<li> Q. I read some notes such as "Check valid domains allowed by internal
|
<li> Q. I read some notes such as "Check valid domains allowed by internal
|
||||||
senders to detect PCs infected with spam trojans." but could not
|
senders to detect PCs infected with spam trojans." but could not
|
||||||
@@ -196,3 +194,5 @@ everything up for you. For other systems:
|
|||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
</ol>
|
</ol>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+3
-14
@@ -8,7 +8,7 @@ tempdir = /var/log/milter/save
|
|||||||
log_headers = 0
|
log_headers = 0
|
||||||
# connection ips and hostnames are matched against this glob style list
|
# connection ips and hostnames are matched against this glob style list
|
||||||
# to recognize internal senders.
|
# to recognize internal senders.
|
||||||
;internal_connect = 192.168.*.*,127.*
|
;internal_connect = 192.168.*.*
|
||||||
|
|
||||||
# mail that is not an internal_connect and claims to be from an
|
# mail that is not an internal_connect and claims to be from an
|
||||||
# internal domain is rejected. Furthermore, internal mail that
|
# internal domain is rejected. Furthermore, internal mail that
|
||||||
@@ -17,7 +17,7 @@ log_headers = 0
|
|||||||
# flexible. However, SPF is not currently checked for outgoing
|
# flexible. However, SPF is not currently checked for outgoing
|
||||||
# (internal_connect) mail because it doesn't yet handle authorizing
|
# (internal_connect) mail because it doesn't yet handle authorizing
|
||||||
# internal IPs locally.
|
# internal IPs locally.
|
||||||
;internal_domains = mycorp.com,localhost.localdomain
|
;internal_domains = mycorp.com
|
||||||
|
|
||||||
# connections from a trusted relay can trust the first Received header
|
# connections from a trusted relay can trust the first Received header
|
||||||
# SPF checks are bypassed for internal connections and trusted relays.
|
# SPF checks are bypassed for internal connections and trusted relays.
|
||||||
@@ -99,11 +99,6 @@ reject_spoofed = 0
|
|||||||
# doesn't match MAIL FROM. Outlook and other email clients will then display
|
# 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"
|
# something like: "Sent by sender@domain.com on behalf of from@example.com"
|
||||||
;supply_sender = 0
|
;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
|
# features intended to clean up outgoing mail
|
||||||
[scrub]
|
[scrub]
|
||||||
@@ -151,13 +146,7 @@ blind = 1
|
|||||||
# only EXTERNAL messages are dspam filtered
|
# only EXTERNAL messages are dspam filtered
|
||||||
;dspam_dict=/var/lib/dspam/moderator.dict
|
;dspam_dict=/var/lib/dspam/moderator.dict
|
||||||
|
|
||||||
# Recipients of mail sent from these senders are added to the auto_whitelist.
|
# Opt-opt recipients from dspam screening and header triage
|
||||||
# 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
|
|
||||||
|
|
||||||
# Opt-out recipients entirely from dspam screening and header triage
|
|
||||||
;dspam_exempt=getitall@mycorp.com
|
;dspam_exempt=getitall@mycorp.com
|
||||||
# Do not scan mail (ostensibly) from these senders
|
# Do not scan mail (ostensibly) from these senders
|
||||||
;dspam_whitelist=getitall@sender.com
|
;dspam_whitelist=getitall@sender.com
|
||||||
|
|||||||
+261
-58
@@ -1,29 +1,33 @@
|
|||||||
Title: Python Milters
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
||||||
|
<title>Python Milters</title>
|
||||||
|
</head><body>
|
||||||
|
|
||||||
<P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
|
<P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
|
||||||
<IMG SRC="http://bmsi.com/art/brain1.gif"
|
<IMG SRC="/art/brain1.gif"
|
||||||
ALT="Viewable With Any Browser" BORDER="0"></A>
|
ALT="Viewable With Any Browser" BORDER="0"></A>
|
||||||
|
|
||||||
<img src="http://bmsi.com/art/banner_4.gif" width="468" height="60" border="0"
|
<img src="/art/banner_4.gif" width="468" height="60" border="0"
|
||||||
usemap="#banner_4" alt="Your vote?">
|
usemap="#banner_4" alt="Your vote?">
|
||||||
<map name="banner_4">
|
<map name="banner_4">
|
||||||
<area shape="rect" coords="330,25,426,59"
|
<area shape="rect" coords="330,25,426,59"
|
||||||
href="http://education-survey.org/" alt="I Disagree">
|
href="http://education-survey.org/" alt="I Disagree">
|
||||||
<area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
|
<area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
|
||||||
</map>
|
</map>
|
||||||
</P>
|
|
||||||
|
|
||||||
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align=left>
|
</P>
|
||||||
<h1 align=center>Sendmail Milters in Python</h1>
|
<h1 align=center>Sendmail Milters in Python</h1>
|
||||||
<h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a>
|
<h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a>
|
||||||
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
|
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
|
||||||
Stuart D. Gathman</a><br>
|
Stuart D. Gathman</a><br>
|
||||||
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
|
||||||
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
|
||||||
Last updated Dec 29, 2005</h4>
|
Last updated Aug 28, 2005</h4>
|
||||||
|
|
||||||
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
|
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
|
||||||
<a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
|
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
|
||||||
<a href="#overview">Overview</a> |
|
<a href="#overview">Overview</a> |
|
||||||
<a href="/python/dspam.html">pydspam</a> |
|
<a href="/python/dspam.html">pydspam</a> |
|
||||||
<a href="/libdspam/dspam.html">libdspam</a>
|
<a href="/libdspam/dspam.html">libdspam</a>
|
||||||
@@ -41,6 +45,159 @@ separation features to enhance security. Even better, sendmail 8.13
|
|||||||
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more
|
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more
|
||||||
efficient and secure. I recommend upgrading.
|
efficient and secure. I recommend upgrading.
|
||||||
|
|
||||||
|
<h2> Recent Changes </h2>
|
||||||
|
|
||||||
|
Python milter is being moved to
|
||||||
|
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
|
||||||
|
project</a> for development and release downloads.
|
||||||
|
<p>
|
||||||
|
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
|
||||||
|
in line with the newly official RFC. It adds
|
||||||
|
<a href="http://ses.codeshare.ca/">SES</a>
|
||||||
|
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.
|
||||||
|
<p>
|
||||||
|
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
|
||||||
|
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.
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
<p>
|
||||||
|
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 <code>rhsbl</code> sendmail hack so that spammer domains
|
||||||
|
can be blacklisted. With the RPM installed, add a line like the following
|
||||||
|
to your <code>sendmail.mc</code>.
|
||||||
|
<pre>
|
||||||
|
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
|
||||||
|
</pre>
|
||||||
|
<p>
|
||||||
|
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 <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
|
||||||
|
framework provides a custom DNS server - but I
|
||||||
|
would like a smaller implementation for our use.
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
<p>
|
||||||
|
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 <a
|
||||||
|
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
|
||||||
|
the problem with sender-ID</a>, and Debian <a
|
||||||
|
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
|
||||||
|
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
|
||||||
|
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
|
||||||
|
software in general</a> and the <a
|
||||||
|
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
|
||||||
|
particular</a>, Python milter will not be able to implement sender-ID in its
|
||||||
|
current form. This was, no doubt, Microsoft's intent all along.
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="http://openspf.com">
|
||||||
|
<img src="SPF.gif" align=left alt="SPF logo"></a>
|
||||||
|
Release 0.6.6 adds support for <a href="http://openspf.com/">SPF</a>,
|
||||||
|
a protocol to prevent forging of the envelope from address.
|
||||||
|
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
|
||||||
|
The included spf.py module is an updated version of the original 1.6
|
||||||
|
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
|
||||||
|
The updated version tracks the draft RFC and test suite.
|
||||||
|
<p>
|
||||||
|
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
|
||||||
|
<p>
|
||||||
|
Release 0.6.1 adds a full milter based dspam application.
|
||||||
|
<p>
|
||||||
|
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
|
||||||
|
dspam bayes filter project</a> and <a href="dspam.html">
|
||||||
|
packaged it for python</a>.
|
||||||
|
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 <a href="dspam.html">DSPAM</a> 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."
|
||||||
|
|
||||||
|
<h2> Enough Already! </h2>
|
||||||
|
|
||||||
|
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 for 0.5.6. In the meantime,
|
||||||
|
here are some things to consider:
|
||||||
|
<ul>
|
||||||
|
<li> 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.
|
||||||
|
<li> 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().
|
||||||
|
<li> 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.
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
|
||||||
|
determining whether you want to copy (e.g. whether the sender is local). For
|
||||||
|
example,
|
||||||
|
<pre>
|
||||||
|
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])
|
||||||
|
...
|
||||||
|
</pre>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
|
||||||
<h3><a name=overview>Overview</a></h3>
|
<h3><a name=overview>Overview</a></h3>
|
||||||
|
|
||||||
This package provides a robust toolkit for Python <a
|
This package provides a robust toolkit for Python <a
|
||||||
@@ -88,8 +245,6 @@ content filtering. SPF checking
|
|||||||
requires <a href="http://pydns.sourceforge.net/">
|
requires <a href="http://pydns.sourceforge.net/">
|
||||||
pydns</a>. Configuration documentation is currently included as comments
|
pydns</a>. Configuration documentation is currently included as comments
|
||||||
in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
|
in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
|
||||||
See also the <a href="HOWTO">HOWTO</a> and <a href="logmsgs.html">
|
|
||||||
Milter Log Message Tags</a>.
|
|
||||||
<p>
|
<p>
|
||||||
Python milter is under GPL. The authors can probably be convinced to
|
Python milter is under GPL. The authors can probably be convinced to
|
||||||
change this to LGPL if needed.
|
change this to LGPL if needed.
|
||||||
@@ -221,14 +376,8 @@ me if you successfully install milter on a system not mentioned below.
|
|||||||
<td>0.5.5</td><tr>
|
<td>0.5.5</td><tr>
|
||||||
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.13.1</td>
|
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.13.1</td>
|
||||||
<td>0.7.2</td><tr>
|
<td>0.7.2</td><tr>
|
||||||
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.4.1</td><td>8.13.5</td>
|
|
||||||
<td>0.8.4</td><tr>
|
|
||||||
<td>RedHat 8.0</td><td>gcc-3.2</td><td>2.2.1</td><td>8.12.6</td>
|
<td>RedHat 8.0</td><td>gcc-3.2</td><td>2.2.1</td><td>8.12.6</td>
|
||||||
<td>0.5.2</td><tr>
|
<td>0.5.2</td><tr>
|
||||||
<td>RedHat 9.0</td><td>gcc-3.2.2</td><td>2.4.1</td><td>8.13.1</td>
|
|
||||||
<td>0.8.2</td><tr>
|
|
||||||
<td>RedHat EL3</td><td>gcc-3.2.3</td><td>2.4.1</td><td>8.13.5</td>
|
|
||||||
<td>0.8.4</td><tr>
|
|
||||||
<td>Debian Linux</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.0</td>
|
<td>Debian Linux</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.0</td>
|
||||||
<td>0.3.7</td><tr>
|
<td>0.3.7</td><tr>
|
||||||
<td>Debian Linux</td><td>gcc-3.2.2</td><td>2.2.2</td><td>8.12.7</td>
|
<td>Debian Linux</td><td>gcc-3.2.2</td><td>2.2.2</td><td>8.12.7</td>
|
||||||
@@ -239,8 +388,8 @@ me if you successfully install milter on a system not mentioned below.
|
|||||||
<td>0.3.4</td><tr>
|
<td>0.3.4</td><tr>
|
||||||
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.3</td><td>8.12.3</td>
|
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.3</td><td>8.12.3</td>
|
||||||
<td>0.4.2</td><tr>
|
<td>0.4.2</td><tr>
|
||||||
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.4.1</td><td>8.13.1</td>
|
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.2.3</td><td>8.13.1</td>
|
||||||
<td>0.8.4</td><tr>
|
<td>0.7.1</td><tr>
|
||||||
<td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td>
|
<td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td>
|
||||||
<td>0.3.8</td><tr>
|
<td>0.3.8</td><tr>
|
||||||
<td>Slackware 9.0</td><td>gcc-3.2.2</td><td>2.2.3</td><td>8.12.9</td>
|
<td>Slackware 9.0</td><td>gcc-3.2.2</td><td>2.2.3</td><td>8.12.9</td>
|
||||||
@@ -254,54 +403,108 @@ me if you successfully install milter on a system not mentioned below.
|
|||||||
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td>
|
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td>
|
||||||
<td>0.5.5</td><tr>
|
<td>0.5.5</td><tr>
|
||||||
<td>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td>
|
<td>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td>
|
||||||
<td>0.6.6</td>
|
<td>0.6.6</td><tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2> Enough Already! </h2>
|
<h3> Requirements </h3>
|
||||||
|
|
||||||
Nearly a dozen people have emailed me begging for a feature to copy
|
<menu>
|
||||||
outgoing and/or incoming mail to a backup directory by user. Ok, it
|
<li> While the miltermodule will work with python 1.5, you probably
|
||||||
looks like this is a most requested feature for 0.5.6. In the meantime,
|
want to use python 2.0 or better. The python code uses a number of
|
||||||
here are some things to consider:
|
python 2 features.
|
||||||
<ul>
|
<li> Python must be configured with thread support. This is because
|
||||||
<li> If you want to equivalent of a Bcc added to each message, this
|
sendmail's libmilter requires thread support.
|
||||||
is very easy to do in the python code for bms.py. See below.
|
<li> You must compile sendmail with libmilter enabled. In versions of
|
||||||
<li> If you want to copy to a file in a directory (thus avoiding having to
|
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
|
||||||
set up aliases), this is slightly more involved. The bms.py milter already
|
is not installed by default.
|
||||||
copies the message to a temporary file for use in replacing the message body
|
Sendmail 8.12 still does not enable libmilter by default. You must
|
||||||
when banned attachments are found. You have to open a file, and copy the
|
explicitly select the "MILTER" option when compiling.
|
||||||
Mesage object to it in eom().
|
<li> Python milter has been tested against sendmail-8.11 and sendmail-8.12.
|
||||||
<li> Finally, you are probably aware that most email clients already
|
<li> Python milter must be compiled for the specific version of sendmail
|
||||||
keep a copy of outgoing mail? Presumably there is a good reason for
|
it will run with. (Since the result is dynamically loaded, there could
|
||||||
keeping another copy on the server.
|
conceivably be multiple versions available and selected at startup - but
|
||||||
</ul>
|
that will have to wait.) This situation may only exist for sendmail
|
||||||
<p>
|
versions prior to 8.12. The protocol seems designed for backward
|
||||||
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
|
compatibility - and 8.12 is the first official milter release.
|
||||||
determining whether you want to copy (e.g. whether the sender is local). For
|
<li> Mea Culpa! After reading the Python Style guide, I realize that
|
||||||
example,
|
my Python code is not up to snuff. Apparently mixed tabs and spaces
|
||||||
<pre>
|
are anathema to those using Windows editors, where tabs can be expanded using
|
||||||
def envfrom(...
|
any arbitrary algorithm. Other than that, my
|
||||||
...
|
intuition matched Guido's pretty well - although I like to indent by 2
|
||||||
if len(t) == 2:
|
rather than 4. I will arrange to have tabs expanded to spaces when
|
||||||
self.rejectvirus = t[1] in reject_virus_from
|
exporting new versions. Until then, beware!
|
||||||
if t[0] in wiretap_users.get(t[1],()):
|
</menu>
|
||||||
self.add_recipient(wiretap_dest)
|
|
||||||
if t[1] == 'mydomain.com':
|
|
||||||
self.add_recipient('<copy-%s>' % t[0])
|
|
||||||
...
|
|
||||||
</pre>
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
|
||||||
|
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
|
||||||
|
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
|
||||||
|
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
|
||||||
|
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
|
||||||
|
to represent the stuff that comes with AIX. You might also want
|
||||||
|
my <a href="/aix/python.spec">python-2.1.1</a> 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 <a href="/aix">AIX RPM directory</a>.
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
|
||||||
|
|
||||||
|
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 <a href="mfapi.h">mfapi.h</a>
|
||||||
|
header for sendmail-8.6.11 from here and manually install it as
|
||||||
|
<code>/usr/include/libmilter/mfapi.h</code>.
|
||||||
|
<p>
|
||||||
|
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 <code>libs = ["milter", "smutil"]</code> in setup.py.
|
||||||
|
<p>
|
||||||
|
If you have installed python2, and want
|
||||||
|
python-milter to use python2, add <code>python=python2</code> to setup.cfg
|
||||||
|
and build with <code>python2 setup.py bdist_rpm</code>.
|
||||||
|
|
||||||
|
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
|
||||||
|
|
||||||
|
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:
|
||||||
|
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
|
||||||
|
sendmail-rhmilter.spec</a>. The <a
|
||||||
|
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
|
||||||
|
SRPM for sendmail-8.11.6</a> is available from
|
||||||
|
<a href="http://www.redhat.com">Redhat</a> under
|
||||||
|
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
|
||||||
|
Errata for RH6.2</a>. But that doesn't include the latest security
|
||||||
|
patches since RH6.2 is no longer supported.
|
||||||
|
<p>
|
||||||
|
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
|
||||||
|
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> 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.
|
||||||
|
<p>
|
||||||
|
If you have installed python2, and want
|
||||||
|
python-milter to use python2, add <code>python=python2</code> to setup.cfg
|
||||||
|
and build with <code>python2 setup.py bdist_rpm</code>.
|
||||||
|
You'll need to install the sendmail-devel package to compile milter.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<p>
|
<p>
|
||||||
<a href="http://validator.w3.org/check/referer">
|
<a href="http://validator.w3.org/check/referer">
|
||||||
<img border=0 src="http://bmsi.com/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
|
<img border=0 src="/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
|
||||||
<a href="http://www.redhat.com">
|
<a href="http://www.redhat.com">
|
||||||
<img src="http://bmsi.com/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
|
<img src="/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
</body></html>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# chkconfig: 2345 80 30
|
# chkconfig: 2345 80 30
|
||||||
# description: Milter is a process that filters messages sent through sendmail.
|
# description: Milter is a process that filters messages sent through sendmail.
|
||||||
# processname: milter
|
# processname: milter
|
||||||
# config: /etc/mail/pymilter.cfg
|
# config: /var/log/milter/bms.py
|
||||||
# pidfile: /var/run/milter/milter.pid
|
# pidfile: /var/run/milter/milter.pid
|
||||||
|
|
||||||
python="python2.3"
|
python="python2.3"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
# chkconfig: 2345 80 30
|
# chkconfig: 2345 80 30
|
||||||
# description: Milter is a process that filters messages sent through sendmail.
|
# description: Milter is a process that filters messages sent through sendmail.
|
||||||
# processname: milter
|
# processname: milter
|
||||||
# config: /etc/mail/pymilter.cfg
|
# config: /var/log/milter/bms.py
|
||||||
# pidfile: /var/run/milter/milter.pid
|
# pidfile: /var/run/milter/milter.pid
|
||||||
|
|
||||||
python="python2.3"
|
python="python2.3"
|
||||||
|
|||||||
+6
-30
@@ -1,5 +1,5 @@
|
|||||||
%define name milter
|
%define name milter
|
||||||
%define version 0.8.5
|
%define version 0.8.3
|
||||||
%define release 1.RH7
|
%define release 1.RH7
|
||||||
# what version of RH are we building for?
|
# what version of RH are we building for?
|
||||||
%define redhat9 0
|
%define redhat9 0
|
||||||
@@ -39,11 +39,11 @@ Prefix: %{_prefix}
|
|||||||
Vendor: Stuart D. Gathman <stuart@bmsi.com>
|
Vendor: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
Packager: Stuart D. Gathman <stuart@bmsi.com>
|
Packager: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
Url: http://www.bmsi.com/python/milter.html
|
Url: http://www.bmsi.com/python/milter.html
|
||||||
Requires: %{python} >= 2.4, sendmail >= 8.13
|
Requires: %{python} >= 2.4, sendmail >= 8.12.10
|
||||||
%ifos Linux
|
%ifos Linux
|
||||||
Requires: chkconfig
|
Requires: chkconfig
|
||||||
%endif
|
%endif
|
||||||
BuildRequires: %{python}-devel , sendmail-devel >= 8.13
|
BuildRequires: %{python}-devel , sendmail-devel >= 8.12.10
|
||||||
|
|
||||||
%description
|
%description
|
||||||
This is a python extension module to enable python scripts to
|
This is a python extension module to enable python scripts to
|
||||||
@@ -52,15 +52,10 @@ modules provide for navigating and modifying MIME parts.
|
|||||||
|
|
||||||
%prep
|
%prep
|
||||||
%setup
|
%setup
|
||||||
#%patch -p0 -b .bms
|
#%patch -p1
|
||||||
|
|
||||||
%build
|
%build
|
||||||
if %{redhat9}; then
|
env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
|
||||||
LDFLAGS="-g"
|
|
||||||
else
|
|
||||||
LDFLAGS="-s"
|
|
||||||
fi
|
|
||||||
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build
|
|
||||||
|
|
||||||
%install
|
%install
|
||||||
rm -rf $RPM_BUILD_ROOT
|
rm -rf $RPM_BUILD_ROOT
|
||||||
@@ -174,26 +169,7 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
/usr/share/sendmail-cf/hack/rhsbl.m4
|
/usr/share/sendmail-cf/hack/rhsbl.m4
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Thu Dec 29 2005 Stuart Gathman <stuart@bmsi.com> 0.8.5-1
|
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-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 <stuart@bmsi.com> 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 <stuart@bmsi.com> 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 <stuart@bmsi.com> 0.8.3-1
|
|
||||||
- Keep screened honeypot mail, but optionally discard honeypot only mail.
|
- Keep screened honeypot mail, but optionally discard honeypot only mail.
|
||||||
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
|
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
|
||||||
- Consider SMTP AUTH connections internal.
|
- Consider SMTP AUTH connections internal.
|
||||||
|
|||||||
+4
-24
@@ -34,15 +34,6 @@ $ python setup.py help
|
|||||||
libraries=["milter","smutil","resolv"]
|
libraries=["milter","smutil","resolv"]
|
||||||
|
|
||||||
* $Log$
|
* $Log$
|
||||||
* Revision 1.8 2005/10/20 23:23:36 customdesigned
|
|
||||||
* Include smfi_progress is SMFIR_PROGRESS defined
|
|
||||||
*
|
|
||||||
* Revision 1.7 2005/10/20 23:04:46 customdesigned
|
|
||||||
* Add optional idx for position of added header.
|
|
||||||
*
|
|
||||||
* Revision 1.6 2005/07/15 22:18:17 customdesigned
|
|
||||||
* Support callback exception policy
|
|
||||||
*
|
|
||||||
* Revision 1.5 2005/06/24 04:20:07 customdesigned
|
* Revision 1.5 2005/06/24 04:20:07 customdesigned
|
||||||
* Report context allocation error.
|
* Report context allocation error.
|
||||||
*
|
*
|
||||||
@@ -976,39 +967,28 @@ milter_setreply(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static char milter_addheader__doc__[] =
|
static char milter_addheader__doc__[] =
|
||||||
"addheader(field, value, idx=-1) -> None\n\
|
"addheader(field, value) -> None\n\
|
||||||
Add a header to the message. This header is not passed to other\n\
|
Add a header to the message. This header is not passed to other\n\
|
||||||
filters. It is not checked for standards compliance;\n\
|
filters. It is not checked for standards compliance;\n\
|
||||||
the mail filter must ensure that no protocols are violated\n\
|
the mail filter must ensure that no protocols are violated\n\
|
||||||
as a result of adding this header.\n\
|
as a result of adding this header.\n\
|
||||||
field - header field name\n\
|
field - header field name\n\
|
||||||
value - header field value\n\
|
value - header field value\n\
|
||||||
idx - optional position in internal header list to insert new header\n\
|
|
||||||
Both are strings. This function can only be called from the EOM callback.";
|
Both are strings. This function can only be called from the EOM callback.";
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
milter_addheader(PyObject *self, PyObject *args) {
|
milter_addheader(PyObject *self, PyObject *args) {
|
||||||
char *headerf;
|
char *headerf;
|
||||||
char *headerv;
|
char *headerv;
|
||||||
int idx = -1;
|
|
||||||
SMFICTX *ctx;
|
SMFICTX *ctx;
|
||||||
PyThreadState *t;
|
PyThreadState *t;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "ss|i:addheader", &headerf, &headerv, &idx))
|
if (!PyArg_ParseTuple(args, "ss:addheader", &headerf, &headerv)) return NULL;
|
||||||
return NULL;
|
|
||||||
ctx = _find_context(self);
|
ctx = _find_context(self);
|
||||||
if (ctx == NULL) return NULL;
|
if (ctx == NULL) return NULL;
|
||||||
t = PyEval_SaveThread();
|
t = PyEval_SaveThread();
|
||||||
#ifdef SMFIR_INSHEADER
|
|
||||||
return _thread_return(t, (idx < 0) ? smfi_addheader(ctx, headerf, headerv) :
|
|
||||||
smfi_insheader(ctx, idx, headerf, headerv), "cannot add header");
|
|
||||||
#else
|
|
||||||
if (idx < 0)
|
|
||||||
return _thread_return(t,smfi_addheader(ctx, headerf, headerv),
|
return _thread_return(t,smfi_addheader(ctx, headerf, headerv),
|
||||||
"cannot add header");
|
"cannot add header");
|
||||||
PyErr_SetString(MilterError, "insheader not supported");
|
|
||||||
return NULL;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_chgheader__doc__[] =
|
static char milter_chgheader__doc__[] =
|
||||||
@@ -1163,7 +1143,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SMFIR_PROGRESS
|
#if _FFR_SMFI_PROGRESS
|
||||||
static char milter_progress__doc__[] =
|
static char milter_progress__doc__[] =
|
||||||
"progress() -> None\n\
|
"progress() -> None\n\
|
||||||
Notify the MTA that we are working on a message so it will reset timeouts.";
|
Notify the MTA that we are working on a message so it will reset timeouts.";
|
||||||
@@ -1194,7 +1174,7 @@ static PyMethodDef context_methods[] = {
|
|||||||
#ifdef SMFIF_QUARANTINE
|
#ifdef SMFIF_QUARANTINE
|
||||||
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
|
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
|
||||||
#endif
|
#endif
|
||||||
#ifdef SMFIR_PROGRESS
|
#if _FFR_SMFI_PROGRESS
|
||||||
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
|
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
|
||||||
#endif
|
#endif
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ 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
|
is no positive confirmation that the message is really from you, we have
|
||||||
to give it extra scrutiny - including verifying that the sender really
|
to give it extra scrutiny - including verifying that the sender really
|
||||||
exists by sending you this DSN. We will remember this sender and not
|
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
|
bother you again for while. You can avoid this message entirely for
|
||||||
legitimate mail by using an authorized SMTP server. Contact your mail
|
legitimate mail by using an authorized SMTP server. Contact your mail
|
||||||
administrator and ask how to configure your email client to use an
|
administrator and ask how to configure your email client to use an
|
||||||
authorized server.
|
authorized server.
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
Title: Python Milter Mail Policy
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Python Milter Mail Policy </title>
|
||||||
|
</head><body>
|
||||||
|
|
||||||
<h1> Python Milter Mail Policy </h1>
|
<h1> Python Milter Mail Policy </h1>
|
||||||
|
|
||||||
These are the policies implemented by the <code>bms.py</code> milter
|
|
||||||
application. The milter and Milter modules do not implement any policies
|
|
||||||
by themselves. Eventually, I'll get the bms.py milter moved to its
|
|
||||||
own package.
|
|
||||||
|
|
||||||
<h3> Classify connection </h3>
|
<h3> Classify connection </h3>
|
||||||
|
|
||||||
When the SMTP client connects, the connection IP address is
|
When the SMTP client connects, the connection IP address is
|
||||||
@@ -73,9 +72,7 @@ The wiretap feature can screen and/or monitor mail to/from certain
|
|||||||
users. If the MAIL FROM is being wiretapped, the recipients are
|
users. If the MAIL FROM is being wiretapped, the recipients are
|
||||||
altered accordingly.
|
altered accordingly.
|
||||||
|
|
||||||
<!--table-stop-->
|
<h4> SPF check </h4>
|
||||||
|
|
||||||
<h2> SPF check </h2>
|
|
||||||
|
|
||||||
Finally, the MAIL FROM, connect IP, and HELO name are checked against
|
Finally, the MAIL FROM, connect IP, and HELO name are checked against
|
||||||
any SPF records published via DNS for the alleged sender (MAIL FROM).
|
any SPF records published via DNS for the alleged sender (MAIL FROM).
|
||||||
@@ -235,3 +232,6 @@ should retry the message at a later time.
|
|||||||
</td></tr>
|
</td></tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from distutils.core import setup, Extension
|
from distutils.core import setup, Extension
|
||||||
|
import Milter
|
||||||
|
|
||||||
# FIXME: on some versions of sendmail, smutil is renamed to sm
|
# FIXME: on some versions of sendmail, smutil is renamed to sm
|
||||||
libs = ["milter", "smutil"]
|
libs = ["milter", "smutil"]
|
||||||
@@ -12,8 +13,7 @@ if sys.version < '2.2.3':
|
|||||||
DistributionMetadata.classifiers = None
|
DistributionMetadata.classifiers = None
|
||||||
DistributionMetadata.download_url = None
|
DistributionMetadata.download_url = None
|
||||||
|
|
||||||
# NOTE: importing Milter to obtain version fails when milter.so not built
|
setup(name = "milter", version = Milter.__version__,
|
||||||
setup(name = "milter", version = '0.8.5',
|
|
||||||
description="Python interface to sendmail milter API",
|
description="Python interface to sendmail milter API",
|
||||||
long_description="""\
|
long_description="""\
|
||||||
This is a python extension module to enable python scripts to
|
This is a python extension module to enable python scripts to
|
||||||
@@ -32,7 +32,6 @@ querying SPF records.
|
|||||||
ext_modules=[
|
ext_modules=[
|
||||||
Extension("milter", ["miltermodule.c"],
|
Extension("milter", ["miltermodule.c"],
|
||||||
libraries=libs,
|
libraries=libs,
|
||||||
# set MAX_ML_REPLY to 1 for sendmail < 8.13
|
|
||||||
define_macros = [ ('MAX_ML_REPLY',32) ]
|
define_macros = [ ('MAX_ML_REPLY',32) ]
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
|
|||||||
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||||
|
|
||||||
For more information about SPF, a tool against email forgery, see
|
For more information about SPF, a tool against email forgery, see
|
||||||
http://openspf.org/
|
http://spf.pobox.com/
|
||||||
|
|
||||||
For news, bugfixes, etc. visit the home page for this implementation at
|
For news, bugfixes, etc. visit the home page for this implementation at
|
||||||
http://www.wayforward.net/spf/
|
http://www.wayforward.net/spf/
|
||||||
@@ -47,20 +47,6 @@ For news, bugfixes, etc. visit the home page for this implementation at
|
|||||||
# Terrence is not responding to email.
|
# Terrence is not responding to email.
|
||||||
#
|
#
|
||||||
# $Log$
|
# $Log$
|
||||||
# Revision 1.17 2005/12/23 21:44:15 customdesigned
|
|
||||||
# Always include keyword data in Received-SPF header.
|
|
||||||
#
|
|
||||||
# Revision 1.16 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.15 2005/10/30 01:08:14 customdesigned
|
|
||||||
# Ignore records missing spaces.
|
|
||||||
#
|
|
||||||
# Revision 1.14 2005/08/12 17:36:51 customdesigned
|
|
||||||
# Trean non-existant include as no match in "lax" mode.
|
|
||||||
#
|
|
||||||
# Revision 1.13 2005/07/22 16:00:23 customdesigned
|
# Revision 1.13 2005/07/22 16:00:23 customdesigned
|
||||||
# Limit CNAME chains independently of DNS lookup limit
|
# Limit CNAME chains independently of DNS lookup limit
|
||||||
#
|
#
|
||||||
@@ -299,10 +285,6 @@ def DNSLookup(name,qtype):
|
|||||||
except DNS.DNSError,x:
|
except DNS.DNSError,x:
|
||||||
raise TempError,'DNS ' + str(x)
|
raise TempError,'DNS ' + str(x)
|
||||||
|
|
||||||
def isSPF(txt):
|
|
||||||
"Return True if txt has SPF record signature."
|
|
||||||
return txt.startswith('v=spf1 ') or txt == 'v=spf1'
|
|
||||||
|
|
||||||
# 32-bit IPv4 address mask
|
# 32-bit IPv4 address mask
|
||||||
MASK = 0xFFFFFFFFL
|
MASK = 0xFFFFFFFFL
|
||||||
|
|
||||||
@@ -607,15 +589,8 @@ class query(object):
|
|||||||
return ('none', 250, EXPLANATIONS['none'])
|
return ('none', 250, EXPLANATIONS['none'])
|
||||||
|
|
||||||
# split string by whitespace, drop the 'v=spf1'
|
# split string by whitespace, drop the 'v=spf1'
|
||||||
spf = spf.split()
|
#
|
||||||
# Catch case where SPF record has no spaces
|
spf = spf.split()[1:]
|
||||||
# Can never happen with conforming dns_spf(), however
|
|
||||||
# in the future we might want to give permerror
|
|
||||||
# for common mistakes like IN TXT "v=spf1" "mx" "-all"
|
|
||||||
# in relaxed mode.
|
|
||||||
if spf[0] != 'v=spf1':
|
|
||||||
raise PermError('Invalid SPF record in', self.d)
|
|
||||||
spf = spf[1:]
|
|
||||||
|
|
||||||
# copy of explanations to be modified by exp=
|
# copy of explanations to be modified by exp=
|
||||||
exps = self.exps
|
exps = self.exps
|
||||||
@@ -840,11 +815,11 @@ class query(object):
|
|||||||
is found.
|
is found.
|
||||||
"""
|
"""
|
||||||
# for performance, check for most common case of TXT first
|
# for performance, check for most common case of TXT first
|
||||||
a = [t for t in self.dns_txt(domain) if isSPF(t)]
|
a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
|
||||||
if len(a) == 1 and self.strict < 2:
|
if len(a) == 1 and self.strict < 2:
|
||||||
return a[0]
|
return a[0]
|
||||||
# check official SPF type first when it becomes more popular
|
# check official SPF type first when it becomes more popular
|
||||||
b = [t for t in self.dns_99(domain) if isSPF(t)]
|
b = [t for t in self.dns_99(domain) if t.startswith('v=spf1')]
|
||||||
if len(b) == 1:
|
if len(b) == 1:
|
||||||
# FIXME: really must fully parse each record
|
# FIXME: really must fully parse each record
|
||||||
# and compare with appropriate parts case insensitive.
|
# and compare with appropriate parts case insensitive.
|
||||||
@@ -857,7 +832,7 @@ class query(object):
|
|||||||
if DELEGATE: # use local record if neither found
|
if DELEGATE: # use local record if neither found
|
||||||
a = [t
|
a = [t
|
||||||
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
|
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
|
||||||
if isSPF(t)
|
if t.startswith('v=spf1')
|
||||||
]
|
]
|
||||||
if len(a) == 1: return a[0]
|
if len(a) == 1: return a[0]
|
||||||
return None
|
return None
|
||||||
@@ -888,9 +863,7 @@ class query(object):
|
|||||||
|
|
||||||
def dns_a(self, domainname):
|
def dns_a(self, domainname):
|
||||||
"""Get a list of IP addresses for a domainname."""
|
"""Get a list of IP addresses for a domainname."""
|
||||||
if domainname:
|
|
||||||
return self.dns(domainname, 'A')
|
return self.dns(domainname, 'A')
|
||||||
return []
|
|
||||||
|
|
||||||
def dns_aaaa(self, domainname):
|
def dns_aaaa(self, domainname):
|
||||||
"""Get a list of IPv6 addresses for a domainname."""
|
"""Get a list of IPv6 addresses for a domainname."""
|
||||||
@@ -948,13 +921,14 @@ class query(object):
|
|||||||
def get_header(self,res,receiver=None):
|
def get_header(self,res,receiver=None):
|
||||||
if not receiver:
|
if not receiver:
|
||||||
receiver = self.r
|
receiver = self.r
|
||||||
if res in ('unknown','permerror'):
|
if res in ('pass','fail','softfail'):
|
||||||
txt = ' '.join([res] + self.mech)
|
|
||||||
else:
|
|
||||||
txt = res
|
|
||||||
return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
|
return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
|
||||||
txt,receiver,self.get_header_comment(res),self.i,
|
res,receiver,self.get_header_comment(res),self.i,
|
||||||
self.l + '@' + self.o, self.h)
|
self.l + '@' + self.o, self.h)
|
||||||
|
if res == 'unknown':
|
||||||
|
return '%s (%s: %s)' % (' '.join([res] + self.mech),
|
||||||
|
receiver,self.get_header_comment(res))
|
||||||
|
return '%s (%s: %s)' % (res,receiver,self.get_header_comment(res))
|
||||||
|
|
||||||
def get_header_comment(self,res):
|
def get_header_comment(self,res):
|
||||||
"""Return comment for Received-SPF header.
|
"""Return comment for Received-SPF header.
|
||||||
@@ -976,10 +950,10 @@ class query(object):
|
|||||||
"%s is neither permitted nor denied by domain of %s" \
|
"%s is neither permitted nor denied by domain of %s" \
|
||||||
% (self.i,sender)
|
% (self.i,sender)
|
||||||
#"%s does not designate permitted sender hosts" % sender
|
#"%s does not designate permitted sender hosts" % sender
|
||||||
elif res in ('unknown','permerror'): return \
|
elif res == 'unknown': return \
|
||||||
"permanent error in processing domain of %s: %s" \
|
"permanent error in processing domain of %s: %s" \
|
||||||
% (sender, self.prob)
|
% (sender, self.prob)
|
||||||
elif res in ('error','temperror'): return \
|
elif res == 'error': return \
|
||||||
"temporary error in processing during lookup of %s" % sender
|
"temporary error in processing during lookup of %s" % sender
|
||||||
elif res == 'fail': return \
|
elif res == 'fail': return \
|
||||||
"domain of %s does not designate %s as permitted sender" \
|
"domain of %s does not designate %s as permitted sender" \
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ SMTP (email) servers to prevent criminals from forging your
|
|||||||
domain. The simplest step is usually to publish an SPF record
|
domain. The simplest step is usually to publish an SPF record
|
||||||
with your Sender Policy.
|
with your Sender Policy.
|
||||||
|
|
||||||
For more information, see: http://openspf.org
|
For more information, see: http://openspf.com
|
||||||
|
|
||||||
I hate to annoy you with a DSN (Delivery Status
|
I hate to annoy you with a DSN (Delivery Status
|
||||||
Notification) from a possibly forged email, but since you
|
Notification) from a possibly forged email, but since you
|
||||||
|
|||||||
Reference in New Issue
Block a user