Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e6d1a8cb7 | |||
| 63e45eb884 | |||
| 28bc84eda0 | |||
| 7f7f2500dc | |||
| 4f220b48cf | |||
| a9ca154a92 | |||
| 65672fb26f | |||
| 155eb4e675 | |||
| 14d5869019 | |||
| 28ca3b2837 | |||
| 52b0ac9377 | |||
| 8bc182cb37 | |||
| fb3c140d4c | |||
| 52d23604f7 | |||
| 15f8b797bf | |||
| 3b544a4076 | |||
| 36a7dce2e5 | |||
| a418f34491 | |||
| ba5854fc91 | |||
| a0878320fa | |||
| d1583d88c9 | |||
| 3ad67bd33b | |||
| eb2e730b5d | |||
| daa1eacff3 | |||
| aaf23f35f8 | |||
| 25b6378631 | |||
| c6ac3ddad8 | |||
| b3dce26928 | |||
| fcd85dbfb5 | |||
| 3a1c964f0d | |||
| 36ae390f01 | |||
| 4c0cf4fb95 | |||
| 8f8de8fa97 | |||
| bc516456c1 |
@@ -5,8 +5,11 @@ wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
||||
it with distutils, and generally transformed it from a quick hack to a
|
||||
real, usable Python extension.
|
||||
|
||||
Other contributors:
|
||||
Other contributors (in random order):
|
||||
|
||||
Dave MacQuigg
|
||||
for noticing that smfi_insheader wasn't supported, and creating
|
||||
a template to help first time pymilter users create their own milter.
|
||||
Terence Way
|
||||
for providing a Python port of SPF
|
||||
Scott Kitterman
|
||||
|
||||
@@ -10,6 +10,7 @@ include testmime.py
|
||||
include testbms.py
|
||||
include testdspam.py
|
||||
include rejects.py
|
||||
include report.py
|
||||
include bms.py
|
||||
include spf.py
|
||||
include cid2spf.py
|
||||
@@ -17,6 +18,7 @@ include spfquery.py
|
||||
include test.py
|
||||
include sample.py
|
||||
include test/*
|
||||
include doc/*
|
||||
include Milter/*.py
|
||||
include *.spec
|
||||
include start.sh
|
||||
|
||||
+4
-4
@@ -16,7 +16,7 @@ from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
|
||||
try: from milter import QUARANTINE
|
||||
except: pass
|
||||
|
||||
__version__ = '0.8.3'
|
||||
__version__ = '0.8.5'
|
||||
|
||||
_seq_lock = thread.allocate_lock()
|
||||
_seq = 0
|
||||
@@ -44,7 +44,7 @@ class Milter:
|
||||
for i in msg: print i,
|
||||
print
|
||||
|
||||
def connect(self,hostname,unused,hostaddr):
|
||||
def connect(self,hostname,family,hostaddr):
|
||||
"Called for each connection to sendmail."
|
||||
self.log("connect from %s at %s" % (hostname,hostaddr))
|
||||
return CONTINUE
|
||||
@@ -106,8 +106,8 @@ class Milter:
|
||||
return self.__ctx.setreply(rcode,xcode,msg,*ml)
|
||||
|
||||
# Milter methods which can only be called from eom callback.
|
||||
def addheader(self,field,value):
|
||||
return self.__ctx.addheader(field,value)
|
||||
def addheader(self,field,value,idx=-1):
|
||||
return self.__ctx.addheader(field,value,idx)
|
||||
|
||||
def chgheader(self,field,idx,value):
|
||||
return self.__ctx.chgheader(field,idx,value)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
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.
|
||||
spf_accept_fail option for braindead SPF senders
|
||||
(treats fail like softfail)
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
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.
|
||||
|
||||
Use /etc/mail/access for domain specific SPF policies.
|
||||
|
||||
SPF-Fail: REJECT
|
||||
SPF-Softfail: OK
|
||||
SPF-Neutral: OK
|
||||
Support explicit errors for SPF policy in access file:
|
||||
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
|
||||
|
||||
Defer TEMPERROR in SPF evaluation - give precedence to security
|
||||
@@ -16,9 +22,6 @@ Option to add Received-SPF header, but never reject on 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.
|
||||
|
||||
Auto whitelist based on outgoing email - perhaps with magic subject
|
||||
or recipient prefix.
|
||||
|
||||
Can't output messages with malformed rfc822 attachments.
|
||||
|
||||
Move milter,Milter,mime,spf modules to pymilter
|
||||
@@ -33,7 +36,8 @@ check spam keywords with character classes, e.g.
|
||||
|
||||
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
|
||||
forwarder accounts, and a util provides a special local alias for the
|
||||
user to give to the forwarder. Alias only works for mail from that
|
||||
user to give to the forwarder. (Or user just adds arbitrary alias
|
||||
unique to that forwarder to a database.) Alias only works for mail from that
|
||||
forwarder. Milter gets forwarder domain from alias and uses it to
|
||||
SPF check forwarder.
|
||||
|
||||
|
||||
@@ -1,6 +1,60 @@
|
||||
#!/usr/bin/env python
|
||||
# A simple milter that has grown quite a bit.
|
||||
# $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
|
||||
# Always check HELO except for SPF pass, temperror.
|
||||
#
|
||||
@@ -290,6 +344,12 @@ from Milter.dynip import is_dynip as dynip
|
||||
from fnmatch import fnmatchcase
|
||||
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
|
||||
try:
|
||||
import SRS
|
||||
@@ -328,6 +388,7 @@ scan_html = True
|
||||
scan_rfc822 = True
|
||||
internal_connect = ()
|
||||
trusted_relay = ()
|
||||
trusted_forwarder = ()
|
||||
internal_domains = ()
|
||||
banned_users = ()
|
||||
hello_blacklist = ()
|
||||
@@ -337,6 +398,7 @@ dspam_users = {}
|
||||
dspam_userdir = None
|
||||
dspam_exempt = {}
|
||||
dspam_whitelist = {}
|
||||
whitelist_senders = {}
|
||||
dspam_screener = ()
|
||||
dspam_internal = True # True if internal mail should be dspammed
|
||||
dspam_reject = ()
|
||||
@@ -352,9 +414,8 @@ spf_best_guess = False
|
||||
spf_reject_noptr = False
|
||||
supply_sender = False
|
||||
access_file = None
|
||||
time_format = '%Y%b%d %H:%M:%S %Z'
|
||||
timeout = 600
|
||||
cbv_cache = {}
|
||||
|
||||
logging.basicConfig(
|
||||
stream=sys.stdout,
|
||||
level=logging.INFO,
|
||||
@@ -363,18 +424,8 @@ logging.basicConfig(
|
||||
)
|
||||
milter_log = logging.getLogger('milter')
|
||||
|
||||
try:
|
||||
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
|
||||
if gossip:
|
||||
gossip_node = Gossip('gossip4.db',120)
|
||||
|
||||
class MilterConfigParser(ConfigParser.ConfigParser):
|
||||
|
||||
@@ -499,6 +550,8 @@ def read_config(list):
|
||||
# dspam section
|
||||
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal
|
||||
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_exempt = cp.getaddrset('dspam','dspam_exempt')
|
||||
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
|
||||
@@ -513,6 +566,7 @@ def read_config(list):
|
||||
# spf section
|
||||
global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr
|
||||
global spf_accept_softfail,spf_accept_fail,supply_sender,access_file
|
||||
global trusted_forwarder
|
||||
if spf:
|
||||
spf.DELEGATE = cp.getdefault('spf','delegate')
|
||||
spf_reject_neutral = cp.getlist('spf','reject_neutral')
|
||||
@@ -522,6 +576,7 @@ def read_config(list):
|
||||
spf_reject_noptr = cp.getboolean('spf','reject_noptr')
|
||||
supply_sender = cp.getboolean('spf','supply_sender')
|
||||
access_file = cp.getdefault('spf','access_file')
|
||||
trusted_forwarder = cp.getlist('spf','trusted_forwarder')
|
||||
srs_config = cp.getdefault('srs','config')
|
||||
if srs_config: cp.read([srs_config])
|
||||
srs_secret = cp.getdefault('srs','secret')
|
||||
@@ -543,7 +598,7 @@ def read_config(list):
|
||||
ses = SES.new(secret=srs_secret,expiration=maxage)
|
||||
srs_domain = cp.getlist('srs','ses')
|
||||
else:
|
||||
srs_domain = []
|
||||
srs_domain = cp.getlist('srs','srs')
|
||||
srs_domain.append(cp.getdefault('srs','fwdomain'))
|
||||
banned_users = cp.getlist('srs','banned_users')
|
||||
#print srs_domain
|
||||
@@ -614,7 +669,7 @@ class SPFPolicy(object):
|
||||
return None
|
||||
|
||||
def getFailPolicy(self):
|
||||
policy = self.getPolicy('SPF-Fail:')
|
||||
policy = self.getPolicy('spf-fail:')
|
||||
if not policy:
|
||||
if self.domain in spf_accept_fail:
|
||||
policy = 'CBV'
|
||||
@@ -623,7 +678,7 @@ class SPFPolicy(object):
|
||||
return policy
|
||||
|
||||
def getNonePolicy(self):
|
||||
policy = self.getPolicy('SPF-None:')
|
||||
policy = self.getPolicy('spf-none:')
|
||||
if not policy:
|
||||
if spf_reject_noptr:
|
||||
policy = 'REJECT'
|
||||
@@ -632,7 +687,7 @@ class SPFPolicy(object):
|
||||
return policy
|
||||
|
||||
def getSoftfailPolicy(self):
|
||||
policy = self.getPolicy('SPF-Softfail:')
|
||||
policy = self.getPolicy('spf-softfail:')
|
||||
if not policy:
|
||||
if self.domain in spf_accept_softfail:
|
||||
policy = 'OK'
|
||||
@@ -643,7 +698,7 @@ class SPFPolicy(object):
|
||||
return policy
|
||||
|
||||
def getNeutralPolicy(self):
|
||||
policy = self.getPolicy('SPF-Neutral:')
|
||||
policy = self.getPolicy('spf-neutral:')
|
||||
if not policy:
|
||||
if self.domain in spf_reject_neutral:
|
||||
policy = 'REJECT'
|
||||
@@ -651,17 +706,80 @@ class SPFPolicy(object):
|
||||
return policy
|
||||
|
||||
def getPermErrorPolicy(self):
|
||||
policy = self.getPolicy('SPF-PermError:')
|
||||
policy = self.getPolicy('spf-permerror:')
|
||||
if not policy:
|
||||
policy = 'REJECT'
|
||||
return policy
|
||||
|
||||
def getPassPolicy(self):
|
||||
policy = self.getPolicy('SPF-Pass:')
|
||||
policy = self.getPolicy('spf-pass:')
|
||||
if not policy:
|
||||
policy = 'OK'
|
||||
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):
|
||||
"""Milter to replace attachments poisonous to Windows with a WARNING message,
|
||||
check SPF, and other anti-forgery features, and implement wiretapping
|
||||
@@ -694,14 +812,15 @@ class bmsMilter(Milter.Milter):
|
||||
|
||||
# addheader can only be called from eom(). This accumulates added headers
|
||||
# which can then be applied by alter_headers()
|
||||
def add_header(self,name,val):
|
||||
self.new_headers.append((name,val))
|
||||
def add_header(self,name,val,idx=-1):
|
||||
self.new_headers.append((name,val,idx))
|
||||
self.log('%s: %s' % (name,val))
|
||||
|
||||
def connect(self,hostname,unused,hostaddr):
|
||||
self.internal_connection = False
|
||||
self.trusted_relay = False
|
||||
self.receiver = self.getsymval('j')
|
||||
# sometimes people put extra space in sendmail config, so we strip
|
||||
self.receiver = self.getsymval('j').strip()
|
||||
if hostaddr and len(hostaddr) > 0:
|
||||
ipaddr = hostaddr[0]
|
||||
for pat in internal_connect:
|
||||
@@ -778,6 +897,7 @@ class bmsMilter(Milter.Milter):
|
||||
self.hidepath = False
|
||||
self.discard = False
|
||||
self.dspam = True
|
||||
self.whitelist = False
|
||||
self.reject_spam = True
|
||||
self.data_allowed = True
|
||||
self.trust_received = self.trusted_relay
|
||||
@@ -787,6 +907,7 @@ class bmsMilter(Milter.Milter):
|
||||
self.new_headers = []
|
||||
self.recipients = []
|
||||
self.cbv_needed = None
|
||||
self.whitelist_sender = False
|
||||
t = parse_addr(f)
|
||||
if len(t) == 2: t[1] = t[1].lower()
|
||||
self.canon_from = '@'.join(t)
|
||||
@@ -826,22 +947,28 @@ class bmsMilter(Milter.Milter):
|
||||
if len(t) == 2:
|
||||
user,domain = t
|
||||
if not self.internal_connection:
|
||||
if not self.trusted_relay:
|
||||
for pat in internal_domains:
|
||||
if fnmatchcase(domain,pat):
|
||||
self.log("REJECT: spam from self",pat)
|
||||
self.setreply('550','5.7.1','I hate talking to myself.')
|
||||
return Milter.REJECT
|
||||
elif internal_domains:
|
||||
else:
|
||||
if internal_domains:
|
||||
for pat in internal_domains:
|
||||
if fnmatchcase(domain,pat): break
|
||||
else:
|
||||
self.log("REJECT: zombie PC at ",self.connectip," sending MAIL FROM ",
|
||||
self.canon_from)
|
||||
self.log("REJECT: zombie PC at ",self.connectip,
|
||||
" sending MAIL FROM ",self.canon_from)
|
||||
self.setreply('550','5.7.1',
|
||||
'Your PC is using an unauthorized MAIL FROM.',
|
||||
'It is either badly misconfigured or controlled by organized crime.'
|
||||
)
|
||||
return Milter.REJECT
|
||||
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
|
||||
if user in wiretap_users.get(domain,()):
|
||||
self.add_recipient(wiretap_dest)
|
||||
@@ -853,22 +980,32 @@ class bmsMilter(Milter.Milter):
|
||||
self.dspam = False
|
||||
else:
|
||||
self.rejectvirus = False
|
||||
domain = None
|
||||
if not self.hello_name:
|
||||
self.log("REJECT: missing HELO")
|
||||
self.setreply('550','5.7.1',"It's polite to say HELO first.")
|
||||
return Milter.REJECT
|
||||
self.umis = None
|
||||
if not (self.internal_connection or self.trusted_relay) \
|
||||
and self.connectip and spf:
|
||||
return self.check_spf()
|
||||
else:
|
||||
self.spf = None
|
||||
return Milter.CONTINUE
|
||||
|
||||
def check_spf(self):
|
||||
receiver = self.receiver
|
||||
for tf in trusted_forwarder:
|
||||
q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False)
|
||||
res,code,txt = q.check()
|
||||
if res == 'pass':
|
||||
self.log("TRUSTED_FORWARDER:",tf)
|
||||
break
|
||||
else:
|
||||
q = spf.query(self.connectip,self.canon_from,self.hello_name,
|
||||
receiver=receiver,strict=False)
|
||||
q.set_default_explanation(
|
||||
'SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s' % (q.s,q.i))
|
||||
'SPF fail: see http://openspf.org/why.html?sender=%s&ip=%s' % (q.s,q.i))
|
||||
res,code,txt = q.check()
|
||||
q.result = res
|
||||
if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext:
|
||||
@@ -898,14 +1035,13 @@ class bmsMilter(Milter.Milter):
|
||||
if spf_best_guess and res == 'none':
|
||||
#self.log('SPF: no record published, guessing')
|
||||
q.set_default_explanation(
|
||||
'SPF guess: see http://spf.pobox.com/why.html')
|
||||
'SPF guess: see http://openspf.org/why.html')
|
||||
# best_guess should not result in fail
|
||||
if self.missing_ptr:
|
||||
# ignore dynamic PTR for best guess
|
||||
res,code,txt = q.best_guess('v=spf1 a/24 mx/24')
|
||||
else:
|
||||
res,code,txt = q.best_guess()
|
||||
receiver += ': guessing'
|
||||
if q.perm_error: # FIXME: should never happen?
|
||||
res,code,txt = q.perm_error.ext # extended result
|
||||
txt = 'EXT: ' + txt
|
||||
@@ -919,10 +1055,11 @@ class bmsMilter(Milter.Milter):
|
||||
elif policy != 'OK':
|
||||
self.log('REJECT: no PTR, HELO or SPF')
|
||||
self.setreply('550','5.7.1',
|
||||
"You must have a reverse lookup or publish SPF: http://spf.pobox.com",
|
||||
"Contact your mail administrator IMMEDIATELY! Your mail server is",
|
||||
"severely misconfigured. It has no PTR record (dynamic PTR records",
|
||||
"that contain your IP don't count), an invalid HELO, and no SPF record."
|
||||
"You must have a valid HELO or publish SPF: http://www.openspf.org ",
|
||||
"Contact your mail administrator IMMEDIATELY! Your mail server is ",
|
||||
"severely misconfigured. It has no PTR record (dynamic PTR records ",
|
||||
"that contain your IP don't count), an invalid or dynamic HELO, ",
|
||||
"and no SPF record."
|
||||
)
|
||||
return Milter.REJECT
|
||||
if res in ('deny', 'fail'):
|
||||
@@ -952,15 +1089,16 @@ class bmsMilter(Milter.Milter):
|
||||
'notify your administrator of the problem immediately.'
|
||||
)
|
||||
return Milter.REJECT
|
||||
if res == 'neutral' and q.o in spf_reject_neutral:
|
||||
if res == 'neutral':
|
||||
policy = p.getNeutralPolicy()
|
||||
if policy == 'CBV' and hres == 'pass':
|
||||
if self.mailfrom != '<>':
|
||||
self.cbv_needed = q
|
||||
q.result = res # select neutral DSN template
|
||||
elif policy != 'OK':
|
||||
self.log('REJECT: SPF neutral for',q.s)
|
||||
self.setreply('550','5.7.1',
|
||||
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o,
|
||||
'mail from %s must pass SPF: http://openspf.org/why.html' % q.o,
|
||||
'The %s domain is one that spammers love to forge. Due to' % q.o,
|
||||
'the volume of forged mail, we can only accept mail that',
|
||||
'the SPF record for %s explicitly designates as legitimate.' % q.o,
|
||||
@@ -985,8 +1123,21 @@ class bmsMilter(Milter.Milter):
|
||||
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
|
||||
self.setreply(str(code),'4.3.0',txt)
|
||||
return Milter.TEMPFAIL
|
||||
self.add_header('Received-SPF',q.get_header(res,receiver))
|
||||
self.add_header('Received-SPF',q.get_header(q.result,receiver),0)
|
||||
if res != q.result:
|
||||
self.add_header('X-Guessed-SPF',res,0)
|
||||
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
|
||||
|
||||
# hide_path causes a copy of the message to be saved - until we
|
||||
@@ -998,8 +1149,9 @@ class bmsMilter(Milter.Milter):
|
||||
self.log('DISCARD: RCPT TO:',to,str)
|
||||
return Milter.DISCARD
|
||||
self.log("rcpt to",to,str)
|
||||
t = parse_addr(to.lower())
|
||||
t = parse_addr(to)
|
||||
if len(t) == 2:
|
||||
t[1] = t[1].lower()
|
||||
user,domain = t
|
||||
if self.is_bounce and srs and domain in srs_domain:
|
||||
oldaddr = '@'.join(parse_addr(to))
|
||||
@@ -1014,6 +1166,7 @@ class bmsMilter(Milter.Milter):
|
||||
newaddr = srs.reverse(oldaddr)
|
||||
# Currently, a sendmail map reverses SRS. We just log it here.
|
||||
self.log("srs rcpt:",newaddr)
|
||||
self.dspam = False # verified as reply to mail we sent
|
||||
except:
|
||||
if not (self.internal_connection or self.trusted_relay):
|
||||
if srsre.match(oldaddr):
|
||||
@@ -1027,7 +1180,8 @@ class bmsMilter(Milter.Milter):
|
||||
self.data_allowed = not srs_reject_spoofed
|
||||
|
||||
# non DSN mail to SRS address will bounce due to invalid local part
|
||||
self.recipients.append('@'.join(t))
|
||||
canon_to = '@'.join(t)
|
||||
self.recipients.append(canon_to)
|
||||
users = check_user.get(domain)
|
||||
if self.discard:
|
||||
self.del_recipient(to)
|
||||
@@ -1044,6 +1198,7 @@ class bmsMilter(Milter.Milter):
|
||||
if not domain in dspam_reject:
|
||||
self.reject_spam = False
|
||||
self.smart_alias(to)
|
||||
# get recipient after virtusertable aliasing
|
||||
#rcpt = self.getsymval("{rcpt_addr}")
|
||||
#self.log("rcpt-addr",rcpt);
|
||||
return Milter.CONTINUE
|
||||
@@ -1115,7 +1270,7 @@ class bmsMilter(Milter.Milter):
|
||||
self.log('REJECT: bounce with no SRS encoding')
|
||||
self.setreply('550','5.7.1',
|
||||
"I did not send you that message. Please consider implementing SPF",
|
||||
"(http://openspf.com) to avoid bouncing mail to spoofed senders.",
|
||||
"(http://openspf.org) to avoid bouncing mail to spoofed senders.",
|
||||
"Thank you."
|
||||
)
|
||||
return Milter.REJECT
|
||||
@@ -1136,6 +1291,11 @@ class bmsMilter(Milter.Milter):
|
||||
return Milter.REJECT
|
||||
rc = self.check_header(name,val)
|
||||
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
|
||||
if log_headers or lname in ('subject','x-mailer'):
|
||||
self.log('%s: %s' % (name,val))
|
||||
@@ -1157,18 +1317,19 @@ class bmsMilter(Milter.Milter):
|
||||
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
|
||||
if not self.data_allowed:
|
||||
return self.forged_bounce()
|
||||
for name,val in self.new_headers:
|
||||
for name,val,idx in self.new_headers:
|
||||
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
|
||||
self.fp.write("\n") # terminate headers
|
||||
# log when neither sender nor from domains matches mail from domain
|
||||
if supply_sender and self.mailfrom != '<>':
|
||||
if supply_sender and self.mailfrom != '<>' and not self.internal_connection:
|
||||
mf_domain = self.canon_from.split('@')[-1]
|
||||
self.fp.seek(0)
|
||||
msg = rfc822.Message(self.fp)
|
||||
for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
|
||||
t = parse_addr(hf)
|
||||
if len(t) == 2 and t[1].lower() == mf_domain:
|
||||
break
|
||||
if len(t) == 2:
|
||||
hd = t[1].lower()
|
||||
if hd == mf_domain or mf_domain.endswith('.'+hd): break
|
||||
else:
|
||||
for f in msg.getallmatchingheaders('from'):
|
||||
self.log(f)
|
||||
@@ -1194,7 +1355,7 @@ class bmsMilter(Milter.Milter):
|
||||
dspam.DSF_CHAINED|dspam.DSF_CLASSIFY)
|
||||
try:
|
||||
ds.process(headers)
|
||||
if ds.probability > 0.93 and self.dspam:
|
||||
if ds.probability > 0.93 and self.dspam and not self.whitelist:
|
||||
self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability)
|
||||
self.setreply('550','5.7.1','Your Message looks spammy')
|
||||
return Milter.REJECT
|
||||
@@ -1268,8 +1429,8 @@ class bmsMilter(Milter.Milter):
|
||||
ds.headerchange = self._headerChange
|
||||
modified = False
|
||||
for rcpt in self.recipients:
|
||||
if dspam_users.has_key(rcpt):
|
||||
user = dspam_users.get(rcpt)
|
||||
if dspam_users.has_key(rcpt.lower()):
|
||||
user = dspam_users.get(rcpt.lower())
|
||||
if user:
|
||||
try:
|
||||
self.fp.seek(0)
|
||||
@@ -1301,7 +1462,10 @@ class bmsMilter(Milter.Milter):
|
||||
self.fp = None
|
||||
if len(self.recipients) > 1:
|
||||
self.log("HONEYPOT:",rcpt,'SCREENED')
|
||||
if self.spf:
|
||||
if self.whitelist:
|
||||
# don't train when recipients includes honeypot
|
||||
return False
|
||||
if self.spf and self.mailfrom != '<>':
|
||||
# check that sender accepts quarantine DSN
|
||||
msg = mime.message_from_file(StringIO.StringIO(txt))
|
||||
rc = self.send_dsn(self.spf,msg,'quarantine.txt')
|
||||
@@ -1315,6 +1479,12 @@ class bmsMilter(Milter.Milter):
|
||||
force_result=dspam.DSR_ISSPAM)
|
||||
self.log("HONEYPOT:",rcpt)
|
||||
return Milter.DISCARD
|
||||
if self.whitelist:
|
||||
# Sender whitelisted: tag, but force as ham.
|
||||
# User can change if actually spam.
|
||||
txt = ds.check_spam(user,txt,self.recipients,
|
||||
force_result=dspam.DSR_ISINNOCENT)
|
||||
else:
|
||||
txt = ds.check_spam(user,txt,self.recipients)
|
||||
if not txt:
|
||||
# DISCARD if quarrantined for any recipient. It
|
||||
@@ -1339,6 +1509,13 @@ class bmsMilter(Milter.Milter):
|
||||
screener = dspam_screener[self.id % len(dspam_screener)]
|
||||
if not ds.check_spam(screener,txt,self.recipients,
|
||||
classify=True,quarantine=False):
|
||||
if self.whitelist:
|
||||
# messages is whitelisted but looked like spam, Train on Error
|
||||
self.log("TRAIN:",screener,'X-Dspam-Score: %f' % ds.probability)
|
||||
# user can't correct anyway if really spam, so discard tag
|
||||
ds.check_spam(screener,txt,self.recipients,
|
||||
force_result=dspam.DSR_ISINNOCENT)
|
||||
return False
|
||||
if self.reject_spam:
|
||||
self.log("DSPAM:",screener,
|
||||
'REJECT: X-DSpam-Score: %f' % ds.probability)
|
||||
@@ -1346,7 +1523,7 @@ class bmsMilter(Milter.Milter):
|
||||
self.fp = None
|
||||
return Milter.REJECT
|
||||
self.log("DSPAM:",screener,"SCREENED")
|
||||
if self.spf:
|
||||
if self.spf and self.mailfrom != '<>':
|
||||
# check that sender accepts quarantine DSN
|
||||
self.fp.seek(0)
|
||||
msg = mime.message_from_file(self.fp)
|
||||
@@ -1369,6 +1546,8 @@ class bmsMilter(Milter.Milter):
|
||||
# analyze external mail for spam
|
||||
spam_checked = self.check_spam() # tag or quarantine for spam
|
||||
if not self.fp:
|
||||
if gossip and self.umis:
|
||||
gossip_node.feedback(self.umis,1)
|
||||
return spam_checked
|
||||
|
||||
# analyze all mail for dangerous attachments and scripts
|
||||
@@ -1431,8 +1610,25 @@ class bmsMilter(Milter.Milter):
|
||||
self.addrcpt(self.mailfrom)
|
||||
else:
|
||||
self.alter_recipients(self.discard_list,self.redirect_list)
|
||||
for name,val in self.new_headers:
|
||||
self.addheader(name,val)
|
||||
# auto whitelist original recipients
|
||||
if not defanged and self.whitelist_sender:
|
||||
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:
|
||||
q = self.cbv_needed
|
||||
@@ -1474,7 +1670,10 @@ class bmsMilter(Milter.Milter):
|
||||
buf = out.read(8192)
|
||||
if len(buf) == 0: break
|
||||
self.replacebody(buf) # feed modified message to sendmail
|
||||
if spam_checked: self.log("dspam")
|
||||
if spam_checked:
|
||||
if gossip and self.umis:
|
||||
gossip_node.feedback(self.umis,0)
|
||||
self.log("dspam")
|
||||
return rc
|
||||
finally:
|
||||
out.close()
|
||||
@@ -1507,9 +1706,6 @@ class bmsMilter(Milter.Milter):
|
||||
self.setreply('550','5.7.1',*desc.splitlines())
|
||||
return Milter.REJECT
|
||||
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
|
||||
|
||||
def close(self):
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
+173
@@ -0,0 +1,173 @@
|
||||
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."
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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>
|
||||
+14
-14
@@ -1,8 +1,4 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>Python Milter FAQ</title>
|
||||
</head><body>
|
||||
Title: Python Milter FAQ
|
||||
|
||||
<h1> Python Milter <a name=faq>FAQ</a> </h1>
|
||||
|
||||
@@ -22,7 +18,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
|
||||
RedHat 7.2?
|
||||
<p> A. RedHat forgot to include the header in the RPM. See the
|
||||
<a href="milter.html#rh72">RedHat 7.2 requirements</a>.
|
||||
<a href="requirements.html#rh72">RedHat 7.2 requirements</a>.
|
||||
<p>
|
||||
|
||||
<h3> Running Python Milter </h3>
|
||||
@@ -103,13 +99,14 @@ If you are running bms.py, then the block_chinese option in
|
||||
|
||||
<li> Q. Why does sendmail coredump with milters on OpenBSD?
|
||||
<p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD.
|
||||
Use an internet domain socket instead. For example, in
|
||||
OpenBSD users report that this problem has been fixed, so upgrading
|
||||
OpenBSD will fix this. Otherwise, you can
|
||||
use an internet domain socket instead. For example, in
|
||||
<code>sendmail.cf</code> use
|
||||
<pre>
|
||||
Xpythonfilter, S=inet:1234@localhost
|
||||
</pre>
|
||||
and change sample.py accordingly.
|
||||
<p> OpenBSD users report that this problem has been fixed.
|
||||
<p>
|
||||
|
||||
<li> Q. How can I change the bounce message for an invalid recipient?
|
||||
@@ -143,10 +140,15 @@ Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
|
||||
</pre>
|
||||
<li> Q. There is a Python traceback in the log file! What happened to
|
||||
my email?
|
||||
<p> A. When the milter fails with an untrapped exception, a TEMPFAIL
|
||||
result (451) is returned to the sender. The sender will then retry every
|
||||
hour or so for several days. Hopefully, someone will notice the
|
||||
traceback, and workaround or fix the problem.
|
||||
<p> A. By default, when the milter fails with an untrapped exception, a
|
||||
TEMPFAIL result (451) is returned to the sender. The sender will then retry
|
||||
every hour or so for several days. Hopefully, someone will notice the
|
||||
traceback, and workaround or fix the problem. Beginning with milter-0.8.2,
|
||||
you can call <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
|
||||
senders to detect PCs infected with spam trojans." but could not
|
||||
@@ -194,5 +196,3 @@ everything up for you. For other systems:
|
||||
</ol>
|
||||
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
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.
|
||||
+56
-259
@@ -1,33 +1,29 @@
|
||||
<!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>
|
||||
Title: Python Milters
|
||||
|
||||
<P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
|
||||
<IMG SRC="/art/brain1.gif"
|
||||
<IMG SRC="http://bmsi.com/art/brain1.gif"
|
||||
ALT="Viewable With Any Browser" BORDER="0"></A>
|
||||
|
||||
<img src="/art/banner_4.gif" width="468" height="60" border="0"
|
||||
<img src="http://bmsi.com/art/banner_4.gif" width="468" height="60" border="0"
|
||||
usemap="#banner_4" alt="Your vote?">
|
||||
<map name="banner_4">
|
||||
<area shape="rect" coords="330,25,426,59"
|
||||
href="http://education-survey.org/" alt="I Disagree">
|
||||
<area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
|
||||
</map>
|
||||
|
||||
</P>
|
||||
|
||||
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align=left>
|
||||
<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>
|
||||
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
|
||||
Stuart D. Gathman</a><br>
|
||||
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>
|
||||
Last updated Aug 28, 2005</h4>
|
||||
Last updated Dec 29, 2005</h4>
|
||||
|
||||
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
|
||||
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
|
||||
<a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
|
||||
<a href="#overview">Overview</a> |
|
||||
<a href="/python/dspam.html">pydspam</a> |
|
||||
<a href="/libdspam/dspam.html">libdspam</a>
|
||||
@@ -45,159 +41,6 @@ separation features to enhance security. Even better, sendmail 8.13
|
||||
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more
|
||||
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>
|
||||
|
||||
This package provides a robust toolkit for Python <a
|
||||
@@ -245,6 +88,8 @@ content filtering. SPF checking
|
||||
requires <a href="http://pydns.sourceforge.net/">
|
||||
pydns</a>. Configuration documentation is currently included as comments
|
||||
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>
|
||||
Python milter is under GPL. The authors can probably be convinced to
|
||||
change this to LGPL if needed.
|
||||
@@ -376,8 +221,14 @@ me if you successfully install milter on a system not mentioned below.
|
||||
<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>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>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>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>
|
||||
@@ -388,8 +239,8 @@ me if you successfully install milter on a system not mentioned below.
|
||||
<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>0.4.2</td><tr>
|
||||
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.2.3</td><td>8.13.1</td>
|
||||
<td>0.7.1</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>0.8.4</td><tr>
|
||||
<td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td>
|
||||
<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>
|
||||
@@ -403,108 +254,54 @@ 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>0.5.5</td><tr>
|
||||
<td>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td>
|
||||
<td>0.6.6</td><tr>
|
||||
|
||||
<td>0.6.6</td>
|
||||
</table>
|
||||
|
||||
<h3> Requirements </h3>
|
||||
<h2> Enough Already! </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.
|
||||
<li> Python must be configured with thread support. This is because
|
||||
sendmail's libmilter 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> Python milter has been tested against sendmail-8.11 and sendmail-8.12.
|
||||
<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>.
|
||||
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>
|
||||
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>.
|
||||
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>
|
||||
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>.
|
||||
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="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>
|
||||
<p>
|
||||
<a href="http://validator.w3.org/check/referer">
|
||||
<img border=0 src="/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
|
||||
<img border=0 src="http://bmsi.com/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
|
||||
<a href="http://www.redhat.com">
|
||||
<img src="/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
|
||||
<img src="http://bmsi.com/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
|
||||
</p>
|
||||
|
||||
</body></html>
|
||||
@@ -1,11 +1,12 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>Python Milter Mail Policy </title>
|
||||
</head><body>
|
||||
Title: Python Milter Mail Policy
|
||||
|
||||
<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>
|
||||
|
||||
When the SMTP client connects, the connection IP address is
|
||||
@@ -72,7 +73,9 @@ The wiretap feature can screen and/or monitor mail to/from certain
|
||||
users. If the MAIL FROM is being wiretapped, the recipients are
|
||||
altered accordingly.
|
||||
|
||||
<h4> SPF check </h4>
|
||||
<!--table-stop-->
|
||||
|
||||
<h2> SPF check </h2>
|
||||
|
||||
Finally, the MAIL FROM, connect IP, and HELO name are checked against
|
||||
any SPF records published via DNS for the alleged sender (MAIL FROM).
|
||||
@@ -232,6 +235,3 @@ should retry the message at a later time.
|
||||
</td></tr>
|
||||
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,99 @@
|
||||
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
-3
@@ -8,7 +8,7 @@ tempdir = /var/log/milter/save
|
||||
log_headers = 0
|
||||
# connection ips and hostnames are matched against this glob style list
|
||||
# to recognize internal senders.
|
||||
;internal_connect = 192.168.*.*
|
||||
;internal_connect = 192.168.*.*,127.*
|
||||
|
||||
# mail that is not an internal_connect and claims to be from an
|
||||
# internal domain is rejected. Furthermore, internal mail that
|
||||
@@ -17,7 +17,7 @@ log_headers = 0
|
||||
# flexible. However, SPF is not currently checked for outgoing
|
||||
# (internal_connect) mail because it doesn't yet handle authorizing
|
||||
# internal IPs locally.
|
||||
;internal_domains = mycorp.com
|
||||
;internal_domains = mycorp.com,localhost.localdomain
|
||||
|
||||
# connections from a trusted relay can trust the first Received header
|
||||
# SPF checks are bypassed for internal connections and trusted relays.
|
||||
@@ -99,6 +99,11 @@ reject_spoofed = 0
|
||||
# doesn't match MAIL FROM. Outlook and other email clients will then display
|
||||
# something like: "Sent by sender@domain.com on behalf of from@example.com"
|
||||
;supply_sender = 0
|
||||
# Connections that get an SPF pass for a pretend MAIL FROM of
|
||||
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
|
||||
# This is for non-SRS forwarders. It is a simple implementation that
|
||||
# is inefficient for more than a few entries.
|
||||
;trusted_forwarder = careerbuilder.com
|
||||
|
||||
# features intended to clean up outgoing mail
|
||||
[scrub]
|
||||
@@ -146,7 +151,13 @@ blind = 1
|
||||
# only EXTERNAL messages are dspam filtered
|
||||
;dspam_dict=/var/lib/dspam/moderator.dict
|
||||
|
||||
# Opt-opt recipients from dspam screening and header triage
|
||||
# Recipients of mail sent from these senders are added to the auto_whitelist.
|
||||
# Auto_whitelisted senders with an SPF PASS are never rejected by dspam, and
|
||||
# messages from auto_whitelisted senders will be used to train screener
|
||||
# dictionaries as innocent mail.
|
||||
;whitelist_senders = @mycorp.com
|
||||
|
||||
# Opt-out recipients entirely from dspam screening and header triage
|
||||
;dspam_exempt=getitall@mycorp.com
|
||||
# Do not scan mail (ostensibly) from these senders
|
||||
;dspam_whitelist=getitall@sender.com
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# chkconfig: 2345 80 30
|
||||
# description: Milter is a process that filters messages sent through sendmail.
|
||||
# processname: milter
|
||||
# config: /var/log/milter/bms.py
|
||||
# config: /etc/mail/pymilter.cfg
|
||||
# pidfile: /var/run/milter/milter.pid
|
||||
|
||||
python="python2.3"
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
# chkconfig: 2345 80 30
|
||||
# description: Milter is a process that filters messages sent through sendmail.
|
||||
# processname: milter
|
||||
# config: /var/log/milter/bms.py
|
||||
# config: /etc/mail/pymilter.cfg
|
||||
# pidfile: /var/run/milter/milter.pid
|
||||
|
||||
python="python2.3"
|
||||
|
||||
+30
-6
@@ -1,5 +1,5 @@
|
||||
%define name milter
|
||||
%define version 0.8.3
|
||||
%define version 0.8.5
|
||||
%define release 1.RH7
|
||||
# what version of RH are we building for?
|
||||
%define redhat9 0
|
||||
@@ -39,11 +39,11 @@ Prefix: %{_prefix}
|
||||
Vendor: Stuart D. Gathman <stuart@bmsi.com>
|
||||
Packager: Stuart D. Gathman <stuart@bmsi.com>
|
||||
Url: http://www.bmsi.com/python/milter.html
|
||||
Requires: %{python} >= 2.4, sendmail >= 8.12.10
|
||||
Requires: %{python} >= 2.4, sendmail >= 8.13
|
||||
%ifos Linux
|
||||
Requires: chkconfig
|
||||
%endif
|
||||
BuildRequires: %{python}-devel , sendmail-devel >= 8.12.10
|
||||
BuildRequires: %{python}-devel , sendmail-devel >= 8.13
|
||||
|
||||
%description
|
||||
This is a python extension module to enable python scripts to
|
||||
@@ -52,10 +52,15 @@ modules provide for navigating and modifying MIME parts.
|
||||
|
||||
%prep
|
||||
%setup
|
||||
#%patch -p1
|
||||
#%patch -p0 -b .bms
|
||||
|
||||
%build
|
||||
env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
|
||||
if %{redhat9}; then
|
||||
LDFLAGS="-g"
|
||||
else
|
||||
LDFLAGS="-s"
|
||||
fi
|
||||
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build
|
||||
|
||||
%install
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
@@ -169,7 +174,26 @@ rm -rf $RPM_BUILD_ROOT
|
||||
/usr/share/sendmail-cf/hack/rhsbl.m4
|
||||
|
||||
%changelog
|
||||
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-1
|
||||
* Thu Dec 29 2005 Stuart Gathman <stuart@bmsi.com> 0.8.5-1
|
||||
- Simple trusted_forwarder implementation.
|
||||
- Fix access_file neutral policy
|
||||
- Move Received-SPF header to beginning of headers
|
||||
- Supply keyword info for all results in Received-SPF header.
|
||||
- Move guessed SPF result to separate header
|
||||
- Activate smfi_insheader only when SMFIR_INSHEADER defined
|
||||
- Handle NULL MX in spf.py
|
||||
- in-process GOSSiP server support (to be extended later)
|
||||
- Expire CBV cache and renew auto-whitelist entries
|
||||
* Fri Oct 21 2005 Stuart Gathman <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.
|
||||
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
|
||||
- Consider SMTP AUTH connections internal.
|
||||
|
||||
+25
-5
@@ -34,6 +34,15 @@ $ python setup.py help
|
||||
libraries=["milter","smutil","resolv"]
|
||||
|
||||
* $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
|
||||
* Report context allocation error.
|
||||
*
|
||||
@@ -967,28 +976,39 @@ milter_setreply(PyObject *self, PyObject *args) {
|
||||
}
|
||||
|
||||
static char milter_addheader__doc__[] =
|
||||
"addheader(field, value) -> None\n\
|
||||
"addheader(field, value, idx=-1) -> None\n\
|
||||
Add a header to the message. This header is not passed to other\n\
|
||||
filters. It is not checked for standards compliance;\n\
|
||||
the mail filter must ensure that no protocols are violated\n\
|
||||
as a result of adding this header.\n\
|
||||
field - header field name\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.";
|
||||
|
||||
static PyObject *
|
||||
milter_addheader(PyObject *self, PyObject *args) {
|
||||
char *headerf;
|
||||
char *headerv;
|
||||
int idx = -1;
|
||||
SMFICTX *ctx;
|
||||
PyThreadState *t;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "ss:addheader", &headerf, &headerv)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "ss|i:addheader", &headerf, &headerv, &idx))
|
||||
return NULL;
|
||||
ctx = _find_context(self);
|
||||
if (ctx == NULL) return NULL;
|
||||
t = PyEval_SaveThread();
|
||||
return _thread_return(t,smfi_addheader(ctx, headerf, headerv),
|
||||
#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),
|
||||
"cannot add header");
|
||||
PyErr_SetString(MilterError, "insheader not supported");
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static char milter_chgheader__doc__[] =
|
||||
@@ -1143,7 +1163,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if _FFR_SMFI_PROGRESS
|
||||
#ifdef SMFIR_PROGRESS
|
||||
static char milter_progress__doc__[] =
|
||||
"progress() -> None\n\
|
||||
Notify the MTA that we are working on a message so it will reset timeouts.";
|
||||
@@ -1174,7 +1194,7 @@ static PyMethodDef context_methods[] = {
|
||||
#ifdef SMFIF_QUARANTINE
|
||||
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
|
||||
#endif
|
||||
#if _FFR_SMFI_PROGRESS
|
||||
#ifdef SMFIR_PROGRESS
|
||||
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
|
||||
#endif
|
||||
{ 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
|
||||
to give it extra scrutiny - including verifying that the sender really
|
||||
exists by sending you this DSN. We will remember this sender and not
|
||||
bother you again for while. You can avoid this message entirely for
|
||||
bother you again for a while. You can avoid this message entirely for
|
||||
legitimate mail by using an authorized SMTP server. Contact your mail
|
||||
administrator and ask how to configure your email client to use an
|
||||
authorized server.
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# 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,7 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
from distutils.core import setup, Extension
|
||||
import Milter
|
||||
|
||||
# FIXME: on some versions of sendmail, smutil is renamed to sm
|
||||
libs = ["milter", "smutil"]
|
||||
@@ -13,7 +12,8 @@ if sys.version < '2.2.3':
|
||||
DistributionMetadata.classifiers = None
|
||||
DistributionMetadata.download_url = None
|
||||
|
||||
setup(name = "milter", version = Milter.__version__,
|
||||
# NOTE: importing Milter to obtain version fails when milter.so not built
|
||||
setup(name = "milter", version = '0.8.5',
|
||||
description="Python interface to sendmail milter API",
|
||||
long_description="""\
|
||||
This is a python extension module to enable python scripts to
|
||||
@@ -32,6 +32,7 @@ querying SPF records.
|
||||
ext_modules=[
|
||||
Extension("milter", ["miltermodule.c"],
|
||||
libraries=libs,
|
||||
# set MAX_ML_REPLY to 1 for sendmail < 8.13
|
||||
define_macros = [ ('MAX_ML_REPLY',32) ]
|
||||
),
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
|
||||
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
||||
|
||||
For more information about SPF, a tool against email forgery, see
|
||||
http://spf.pobox.com/
|
||||
http://openspf.org/
|
||||
|
||||
For news, bugfixes, etc. visit the home page for this implementation at
|
||||
http://www.wayforward.net/spf/
|
||||
@@ -47,6 +47,20 @@ For news, bugfixes, etc. visit the home page for this implementation at
|
||||
# Terrence is not responding to email.
|
||||
#
|
||||
# $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
|
||||
# Limit CNAME chains independently of DNS lookup limit
|
||||
#
|
||||
@@ -285,6 +299,10 @@ def DNSLookup(name,qtype):
|
||||
except DNS.DNSError,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
|
||||
MASK = 0xFFFFFFFFL
|
||||
|
||||
@@ -589,8 +607,15 @@ class query(object):
|
||||
return ('none', 250, EXPLANATIONS['none'])
|
||||
|
||||
# split string by whitespace, drop the 'v=spf1'
|
||||
#
|
||||
spf = spf.split()[1:]
|
||||
spf = spf.split()
|
||||
# Catch case where SPF record has no spaces
|
||||
# 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=
|
||||
exps = self.exps
|
||||
@@ -815,11 +840,11 @@ class query(object):
|
||||
is found.
|
||||
"""
|
||||
# for performance, check for most common case of TXT first
|
||||
a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
|
||||
a = [t for t in self.dns_txt(domain) if isSPF(t)]
|
||||
if len(a) == 1 and self.strict < 2:
|
||||
return a[0]
|
||||
# check official SPF type first when it becomes more popular
|
||||
b = [t for t in self.dns_99(domain) if t.startswith('v=spf1')]
|
||||
b = [t for t in self.dns_99(domain) if isSPF(t)]
|
||||
if len(b) == 1:
|
||||
# FIXME: really must fully parse each record
|
||||
# and compare with appropriate parts case insensitive.
|
||||
@@ -832,7 +857,7 @@ class query(object):
|
||||
if DELEGATE: # use local record if neither found
|
||||
a = [t
|
||||
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
|
||||
if t.startswith('v=spf1')
|
||||
if isSPF(t)
|
||||
]
|
||||
if len(a) == 1: return a[0]
|
||||
return None
|
||||
@@ -863,7 +888,9 @@ class query(object):
|
||||
|
||||
def dns_a(self, domainname):
|
||||
"""Get a list of IP addresses for a domainname."""
|
||||
if domainname:
|
||||
return self.dns(domainname, 'A')
|
||||
return []
|
||||
|
||||
def dns_aaaa(self, domainname):
|
||||
"""Get a list of IPv6 addresses for a domainname."""
|
||||
@@ -921,14 +948,13 @@ class query(object):
|
||||
def get_header(self,res,receiver=None):
|
||||
if not receiver:
|
||||
receiver = self.r
|
||||
if res in ('pass','fail','softfail'):
|
||||
if res in ('unknown','permerror'):
|
||||
txt = ' '.join([res] + self.mech)
|
||||
else:
|
||||
txt = res
|
||||
return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
|
||||
res,receiver,self.get_header_comment(res),self.i,
|
||||
txt,receiver,self.get_header_comment(res),self.i,
|
||||
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):
|
||||
"""Return comment for Received-SPF header.
|
||||
@@ -950,10 +976,10 @@ class query(object):
|
||||
"%s is neither permitted nor denied by domain of %s" \
|
||||
% (self.i,sender)
|
||||
#"%s does not designate permitted sender hosts" % sender
|
||||
elif res == 'unknown': return \
|
||||
elif res in ('unknown','permerror'): return \
|
||||
"permanent error in processing domain of %s: %s" \
|
||||
% (sender, self.prob)
|
||||
elif res == 'error': return \
|
||||
elif res in ('error','temperror'): return \
|
||||
"temporary error in processing during lookup of %s" % sender
|
||||
elif res == 'fail': return \
|
||||
"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
|
||||
with your Sender Policy.
|
||||
|
||||
For more information, see: http://openspf.com
|
||||
For more information, see: http://openspf.org
|
||||
|
||||
I hate to annoy you with a DSN (Delivery Status
|
||||
Notification) from a possibly forged email, but since you
|
||||
|
||||
Reference in New Issue
Block a user