Compare commits

...

40 Commits

Author SHA1 Message Date
cvs2svn d85512b32a This commit was manufactured by cvs2svn to create tag 'pymilter-0_8_11'.
Sprout from master 2008-10-11 15:58:00 UTC Stuart Gathman <stuart@gathman.org> 'Release 0.8.11'
Cherrypick from bmsi 2005-05-31 18:23:49 UTC Stuart Gathman <stuart@gathman.org> 'Development changes since 0.7.2':
    rejects.py
    rhsbl.m4
    sample.py
    test/amazon
    test/big5
    test/bounce
    test/bounce1
    test/bound
    test/honey
    test/missingboundary
    test/samp1
    test/spam44
    test/spam7
    test/spam8
    test/test1
    test/test8
    test/virus1
    test/virus13
    test/virus2
    test/virus3
    test/virus4
    test/virus5
    test/virus6
    test/virus7
    testsample.py
2008-10-11 15:58:01 +00:00
Stuart Gathman bc88a64d9b Release 0.8.11 2008-10-11 15:58:00 +00:00
Stuart Gathman a5078a6eb1 Release 0.8.11 2008-10-11 15:57:59 +00:00
Stuart Gathman 96f5b6e9dc Don't greylist DSNs. 2008-10-11 15:45:46 +00:00
Stuart Gathman 1c4878963b Skip greylisting for good reputation. 2008-10-09 18:44:54 +00:00
Stuart Gathman f8e1c15ccd Text for accepting SPF fail with DSN. 2008-10-09 02:54:13 +00:00
Stuart Gathman c86ad6f68c Pass rpmlint on spec file. 2008-10-09 02:43:16 +00:00
Stuart Gathman 0d1f2b7f4d Don't reset greylist timer on early retries. 2008-10-09 00:55:13 +00:00
Stuart Gathman d4cafcd435 Greylisting 2008-10-08 04:57:28 +00:00
Stuart Gathman d64aad95c1 Delay strike3 REJECT and don't reject if whitelisted.
Recognize vacation messages as autoreplies.
2008-10-02 03:19:00 +00:00
Stuart Gathman f9ed6f7194 Never ban a trusted relay. 2008-09-09 23:24:56 +00:00
Stuart Gathman 93e9644574 Wasn't reading banned_ips 2008-09-09 23:08:16 +00:00
Stuart Gathman d86b9f7312 Whitelists and Blacklists 2008-09-09 23:07:48 +00:00
Stuart Gathman cbf69f596b Top level credit link for mascot image. 2008-08-25 22:54:23 +00:00
Stuart Gathman 5b84d454da API docs on milter.org moved 2008-08-25 22:40:58 +00:00
Stuart Gathman e5bf1aee09 Fix /var/run/milter owner 2008-08-25 22:03:24 +00:00
Stuart Gathman 5df3a80f7b Fix /var/run/milter owner 2008-08-25 22:02:39 +00:00
Stuart Gathman df67ee9147 Report failure to remove milter socket 2008-08-25 22:00:46 +00:00
Stuart Gathman 593384d610 /var/run/milter must be owned by mail 2008-08-25 21:41:18 +00:00
Stuart Gathman 1280f1360e Release 0.8.10 2008-08-25 20:00:51 +00:00
Stuart Gathman 3e1e528abe Release 0.8.10 2008-08-25 19:49:02 +00:00
Stuart Gathman 04ce8f81b9 Release 0.8.10 2008-08-25 18:49:13 +00:00
Stuart Gathman bc390e69b9 Update docs 2008-08-25 18:45:21 +00:00
Stuart Gathman c07ed917ab Handle missing gossip_node so self tests pass. 2008-08-25 18:32:23 +00:00
Stuart Gathman a14d676fb6 Release 0.8.10 2008-08-25 18:18:30 +00:00
Stuart Gathman 600e3dfbfb Update docs for 0.8.10 2008-08-25 18:14:56 +00:00
Stuart Gathman 8cfa03bbc4 Log rcpt for SRS rejections. 2008-08-18 17:47:57 +00:00
Stuart Gathman 28a0e551bd CBV policy sends no DSN. DSN policy sends DSN. 2008-08-06 00:52:38 +00:00
Stuart Gathman be3f463450 Send quarantine DSN to SPF PASS only. 2008-08-05 18:04:06 +00:00
Stuart Gathman a420148b1e Parse ESMTP params 2008-07-29 21:59:29 +00:00
Stuart Gathman f4465ea816 Allow explicitly whitelisted email from banned_users. 2008-05-08 21:35:57 +00:00
Stuart Gathman 1845876665 Configure gossip TTL. 2008-04-10 14:59:35 +00:00
Stuart Gathman cee6bc3bea Release 0.8.10 2008-04-02 18:59:14 +00:00
Stuart Gathman 71403de50e Do not CBV whitelisted addresses. We already know they are good. 2008-04-01 00:13:10 +00:00
Stuart Gathman 017784b5a7 Handle multi-hop source path in parseaddr. 2008-01-10 16:41:04 +00:00
Stuart Gathman 632e7b4248 Handle unquoted fullname when parsing email. 2008-01-09 20:15:49 +00:00
Stuart Gathman 10f4f2613e Packaging tweaks. 2007-11-29 14:35:17 +00:00
Stuart Gathman 69369c3b2a Support temperror policy in access. 2007-11-01 20:09:14 +00:00
Stuart Gathman 5386e08ca5 Send quarantine DSN to SPF pass (official or guessed) only.
Reject blacklisted email too big for dspam.
2007-10-10 18:23:54 +00:00
Stuart Gathman d0fe3b0b84 Check porn keywords in From header field. 2007-10-10 18:07:50 +00:00
23 changed files with 836 additions and 408 deletions
+4 -4
View File
@@ -35,13 +35,13 @@ wish to install pydspam.
For basic pymilter you'll need: For basic pymilter you'll need:
python-2.4 python-2.4
milter-0.8.7 milter-0.8.10
sendmail-8.13.x (with milter support enabled) sendmail-8.13.x (with milter support enabled)
and for SPF you'll need: and for SPF you'll need:
pydns-2.3.0-2.4 pydns-2.3.3-2.4
pyspf-2.0.3-2.py24 pyspf-2.0.5-1.py24
and for SRS you'll need: and for SRS you'll need:
@@ -65,7 +65,7 @@ Start milter and pysrs with "service milter start", "service pysrs start".
Tail /var/log/milter/milter.log while SMTP clients connect to your Tail /var/log/milter/milter.log while SMTP clients connect to your
sendmail instance. This should show you what the milter is doing. sendmail instance. This should show you what the milter is doing.
By default, milter-0.8.7 rejects on SPF fail. By default, milter-0.8.10 rejects on SPF fail.
Step four. Tweaking the basic config. Step four. Tweaking the basic config.
+1
View File
@@ -16,6 +16,7 @@ include bms.py
include spf.py include spf.py
include cid2spf.py include cid2spf.py
include spfquery.py include spfquery.py
include ban2zone.py
include test.py include test.py
include sample.py include sample.py
include milter-template.py include milter-template.py
+4 -2
View File
@@ -184,8 +184,10 @@ def runmilter(name,socketname,timeout = 0):
print "Removing %s" % fname print "Removing %s" % fname
try: try:
os.unlink(fname) os.unlink(fname)
except: except os.error, x:
pass import errno
if x.errno != errno.ENOENT:
raise milter.error(x)
# The default flags set include everything # The default flags set include everything
# milter.set_flags(milter.ADDHDRS) # milter.set_flags(milter.ADDHDRS)
+17 -9
View File
@@ -10,6 +10,11 @@
# CBV results. # CBV results.
# #
# $Log$ # $Log$
# Revision 1.8 2007/09/03 16:18:45 customdesigned
# Delete unparseable timestamps when loading address cache. These have
# arisen because of failure to parse MAIL FROM properly. Will have to
# tighten up MAIL FROM parsing to match RFC.
#
# Revision 1.7 2007/01/25 22:47:26 customdesigned # Revision 1.7 2007/01/25 22:47:26 customdesigned
# Persist blacklisting from delayed DSNs. # Persist blacklisting from delayed DSNs.
# #
@@ -89,8 +94,10 @@ class AddrCache(object):
except IOError: except IOError:
lock.unlock() lock.unlock()
def has_key(self,sender): def has_precise_key(self,sender):
"True if sender is cached and has not expired." """True if precise sender is cached and has not expired. Don't
try looking up wildcard entries.
"""
try: try:
lsender = sender and sender.lower() lsender = sender and sender.lower()
ts,res = self.cache[lsender] ts,res = self.cache[lsender]
@@ -98,15 +105,16 @@ class AddrCache(object):
if not ts or ts > too_old: if not ts or ts > too_old:
return True return True
del self.cache[lsender] del self.cache[lsender]
except KeyError: pass
return False
def has_key(self,sender):
"True if sender is cached and has not expired."
if self.has_precise_key(sender):
return True
try: try:
user,host = sender.split('@',1) user,host = sender.split('@',1)
return self.has_key(host) return self.has_precise_key(host)
except ValueError:
pass
except KeyError:
try:
user,host = sender.split('@',1)
return self.has_key(host)
except: pass except: pass
return False return False
+74
View File
@@ -0,0 +1,74 @@
import time
import shelve
import thread
import logging
import urllib
log = logging.getLogger('milter.greylist')
def quoteAddress(s):
'''Quote an address so that it's safe to store in the file-system.
Address can either be a domain name, or local part.
Returns the quoted address.'''
s = urllib.quote(s, '@_-+~!.%')
if s.startswith('.'): s = '%2e' + s[1:]
return s
class Record(object):
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
def __init__(self):
now = time.time()
self.firstseen = now
self.lastseen = now
self.cnt = 0
self.umis = None
class Greylist(object):
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
self.ignoreLastByte = False
self.greylist_time = grey_time * 60 # minutes
self.greylist_expire = grey_expire * 3600 # hours
self.greylist_retain = grey_retain * 24 * 3600 # days
self.dbp = shelve.open(dbname,'c',protocol=2)
self.lock = thread.allocate_lock()
def check(self,ip,sender,recipient):
"Return number of allowed messages for greylist triple."
sender = quoteAddress(sender)
recipient = quoteAddress(recipient)
key = ip + ':' + sender + ':' + recipient
self.lock.acquire()
try:
dbp = self.dbp
try:
r = dbp[key]
now = time.time()
if now > r.lastseen + self.greylist_retain:
# expired
log.debug('Expired greylist: %s',key)
r = Record()
elif now < r.firstseen + self.greylist_time:
# still greylisted
log.debug('Early greylist: %s',key)
#r = Record()
r.lastseen = now
elif r.cnt or now < r.firstseen + self.greylist_expire:
# in greylist window or active
r.lastseen = now
r.cnt += 1
log.debug('Active greylist(%d): %s',r.cnt,key)
else:
# passed greylist window
log.debug('Late greylist: %s',key)
r = Record()
dbp[key] = r
except:
r = Record()
dbp[key] = r
dbp.sync()
finally:
self.lock.release()
return r.cnt
+40
View File
@@ -4,6 +4,8 @@ import socket
import email.Errors import email.Errors
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from email.Header import decode_header from email.Header import decode_header
#import email.Utils
import rfc822
ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$') ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$')
@@ -40,6 +42,44 @@ def iniplist(ipaddr,iplist):
return True return True
return False return False
def parseaddr(t):
"""Split email into Fullname and address.
>>> parseaddr('user@example.com')
('', 'user@example.com')
>>> parseaddr('"Full Name" <foo@example.com>')
('Full Name', 'foo@example.com')
>>> parseaddr('spam@spammer.com <foo@example.com>')
('spam@spammer.com', 'foo@example.com')
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
('God@heaven', 'jeff@spec.org')
>>> parseaddr('Real Name ((comment)) <addr...@example.com>')
('Real Name', 'addr...@example.com')
>>> parseaddr('a(WRONG)@b')
('WRONG', 'a@b')
"""
#return email.Utils.parseaddr(t)
res = rfc822.parseaddr(t)
# dirty fix for some broken cases
if not res[0]:
pos = t.find('<')
if pos > 0 and t[-1] == '>':
addrspec = t[pos+1:-1]
pos1 = addrspec.rfind(':')
if pos1 > 0:
addrspec = addrspec[pos1+1:]
return rfc822.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
if not res[1]:
pos = t.find('<')
if pos > 0 and t[-1] == '>':
addrspec = t[pos+1:-1]
pos1 = addrspec.rfind(':')
if pos1 > 0:
addrspec = addrspec[pos1+1:]
return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
return res
def parse_addr(t): def parse_addr(t):
"""Split email into user,domain. """Split email into user,domain.
+3 -1
View File
@@ -1,4 +1,6 @@
Here is a history of user visible changes to Python milter. See pymilter.spec for recent history.
Here is a history of older changes to Python milter.
0.8.8 move AddrCache, parse_addr, iniplist, parse_header to Milter package 0.8.8 move AddrCache, parse_addr, iniplist, parse_header to Milter package
fix plock for missing source and can't change owner/group fix plock for missing source and can't change owner/group
add sample spfmilter.py milter add sample spfmilter.py milter
+32 -10
View File
@@ -1,6 +1,21 @@
Convert DSN to REJECT unless sender gets SPF pass or best guess pass. Make The recent feature to let a REJECT policy for SPF None be overridden
configurable by SPF result with NOTSPAM policy (reject or deliver without DSN). by whitelisting is working for CSI and CMS. However, there could be
Maybe policy should be NODSN - still verify sender with CBV. a sender that we want to REJECT even when whitelisted - because they
normally get a guessed PASS. Need another policy name - or else just
add them to local SPF so they won't ever get 'None'.
When policy is OK, do not use cbv_cache for blacklist.
Add postmaster option or general rcpt list to dsn. Can send dsn to
user and postmaster on the same connection.
Check ESMTP NOTIFY before sending real DSNs. Just use CBV if DSNs are
not wanted.
Support CBV to local domains and cache results so that invalid users
can be rejected without maintaining valid user lists.
Now that we blacklist IPs for too many bad rcpts, delay SPF until RCPT TO.
When content filtering is not installed, reject BLACKLISTed MFROM When content filtering is not installed, reject BLACKLISTed MFROM
immediately. There is no use waiting until EOM. immediately. There is no use waiting until EOM.
@@ -12,7 +27,8 @@ MTA. The mail is flagged external, so we don't list example.com in
internal_domains (or we would get "spam from self"). But, if we try to do a internal_domains (or we would get "spam from self"). But, if we try to do a
CBV, we get "fraudulent MX", because the MX is ourself! So we need to CBV, we get "fraudulent MX", because the MX is ourself! So we need to
avoid doing CBV on such domains. Currently, we try to make sure the SPF avoid doing CBV on such domains. Currently, we try to make sure the SPF
policies don't do CBV. policies don't do CBV. The real solution is for users to use SMTP AUTH,
but some of them are stubborn.
We now don't check internal domains for incoming mail if there is an We now don't check internal domains for incoming mail if there is an
SPF record. SPF record.
@@ -77,10 +93,6 @@ Whitelisted senders from trusted relay get PROBATION. Need to extracted
SPF result from headers - and in the case of mail internal to relay SPF result from headers - and in the case of mail internal to relay
(e.g. bmsi.com), supply 'pass' result. (e.g. bmsi.com), supply 'pass' result.
For selected domains, check rcpts via CBV before accepting mail. Cache
results. This will kick out dictonary attacks against a mail domain
behind a gateway sooner.
Add auto-blacklisted senders to blacklist.log with timestamp. Add auto-blacklisted senders to blacklist.log with timestamp.
Add emails blacklisted via CBV so that they are remembered across milter Add emails blacklisted via CBV so that they are remembered across milter
restarts. restarts.
@@ -97,8 +109,6 @@ e.g. verizon.net).
Allow verified hostnames for trusted_relay. E.g. HELO name that Allow verified hostnames for trusted_relay. E.g. HELO name that
passes SPF. passes SPF.
Table of sendmail macros for documentation.
When do we get two hello calls? STARTTLS is one reason. When do we get two hello calls? STARTTLS is one reason.
Option: accept mail from auto-whitelisted senders even with spf-fail, Option: accept mail from auto-whitelisted senders even with spf-fail,
@@ -180,6 +190,18 @@ Need a test module to feed sample messages to a milter though a live
sendmail and SMTP. The mockup currently used is probably not very accurate, sendmail and SMTP. The mockup currently used is probably not very accurate,
and doesn't test the threading code. and doesn't test the threading code.
DONE Table of sendmail macros for documentation. In API docs on milter.org.
DONE For selected domains, check rcpts via CBV before accepting mail. Cache
results. This will kick out dictonary attacks against a mail domain
behind a gateway sooner.
DONE Convert DSN to REJECT unless sender gets SPF pass or best guess pass. Make
configurable by SPF result with NOTSPAM policy (reject or deliver without DSN).
Maybe policy should be NODSN - still verify sender with CBV.
DONE Add parseaddr test case for 'foo@bar.com <baz@barf.biz>'
DONE Require signed MFROM for all incoming bounces when signing all outgoing DONE Require signed MFROM for all incoming bounces when signing all outgoing
mail - except from trusted relays. mail - except from trusted relays.
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/python2.4
import socket
import sys
from glob import glob
banned_ips = set(socket.inet_aton(ip)
for fn in sys.argv[1:]
for ip in open(fn))
banned_ips = list(banned_ips)
banned_ips.sort()
for ip in banned_ips:
a = socket.inet_ntoa(ip).split('.')
a.reverse()
print "%s\tIN A 127.0.0.2"%('.'.join(a))
+252 -82
View File
@@ -1,6 +1,71 @@
#!/usr/bin/env python #!/usr/bin/env python
# A simple milter that has grown quite a bit. # A simple milter that has grown quite a bit.
# $Log$ # $Log$
# Revision 1.133 2008/10/09 18:44:54 customdesigned
# Skip greylisting for good reputation.
#
# Revision 1.132 2008/10/09 00:55:13 customdesigned
# Don't reset greylist timer on early retries.
#
# Revision 1.131 2008/10/08 04:57:28 customdesigned
# Greylisting
#
# Revision 1.130 2008/10/02 03:19:00 customdesigned
# Delay strike3 REJECT and don't reject if whitelisted.
# Recognize vacation messages as autoreplies.
#
# Revision 1.129 2008/09/09 23:24:56 customdesigned
# Never ban a trusted relay.
#
# Revision 1.128 2008/09/09 23:08:16 customdesigned
# Wasn't reading banned_ips
#
# Revision 1.127 2008/08/25 18:32:22 customdesigned
# Handle missing gossip_node so self tests pass.
#
# Revision 1.126 2008/08/18 17:47:57 customdesigned
# Log rcpt for SRS rejections.
#
# Revision 1.125 2008/08/06 00:52:38 customdesigned
# CBV policy sends no DSN. DSN policy sends DSN.
#
# Revision 1.124 2008/08/05 18:04:06 customdesigned
# Send quarantine DSN to SPF PASS only.
#
# Revision 1.123 2008/07/29 21:59:29 customdesigned
# Parse ESMTP params
#
# Revision 1.122 2008/05/08 21:35:56 customdesigned
# Allow explicitly whitelisted email from banned_users.
#
# Revision 1.121 2008/04/10 14:59:35 customdesigned
# Configure gossip TTL.
#
# Revision 1.120 2008/04/02 18:59:14 customdesigned
# Release 0.8.10
#
# Revision 1.119 2008/04/01 00:13:10 customdesigned
# Do not CBV whitelisted addresses. We already know they are good.
#
# Revision 1.118 2008/01/09 20:15:49 customdesigned
# Handle unquoted fullname when parsing email.
#
# Revision 1.117 2007/11/29 14:35:17 customdesigned
# Packaging tweaks.
#
# Revision 1.116 2007/11/01 20:09:14 customdesigned
# Support temperror policy in access.
#
# Revision 1.115 2007/10/10 18:23:54 customdesigned
# Send quarantine DSN to SPF pass (official or guessed) only.
# Reject blacklisted email too big for dspam.
#
# Revision 1.114 2007/10/10 18:07:50 customdesigned
# Check porn keywords in From header field.
#
# Revision 1.113 2007/09/25 16:37:26 customdesigned
# Tested on RH7
#
# Revision 1.112 2007/09/13 14:51:03 customdesigned # Revision 1.112 2007/09/13 14:51:03 customdesigned
# Report domain on reputation reject. # Report domain on reputation reject.
# #
@@ -158,17 +223,20 @@ import gc
import anydbm import anydbm
import Milter.dsn as dsn import Milter.dsn as dsn
from Milter.dynip import is_dynip as dynip from Milter.dynip import is_dynip as dynip
from Milter.utils import iniplist,parse_addr,parse_header,ip4re,addr2bin from Milter.utils import \
iniplist,parse_addr,parse_header,ip4re,addr2bin,parseaddr
from Milter.config import MilterConfigParser from Milter.config import MilterConfigParser
from Milter.greylist import Greylist
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from email.Utils import getaddresses,parseaddr from email.Utils import getaddresses
# Import gossip if available # Import gossip if available
try: try:
import gossip import gossip
import gossip.client import gossip.client
import gossip.server import gossip.server
gossip_node = None
except: gossip = None except: gossip = None
# Import pysrs if available # Import pysrs if available
@@ -250,6 +318,7 @@ hello_blacklist = ()
smart_alias = {} smart_alias = {}
dspam_dict = None dspam_dict = None
dspam_users = {} dspam_users = {}
dspam_train = {}
dspam_userdir = None dspam_userdir = None
dspam_exempt = {} dspam_exempt = {}
dspam_whitelist = {} dspam_whitelist = {}
@@ -271,6 +340,7 @@ supply_sender = False
access_file = None access_file = None
timeout = 600 timeout = 600
banned_ips = set() banned_ips = set()
greylist = None
logging.basicConfig( logging.basicConfig(
stream=sys.stdout, stream=sys.stdout,
@@ -381,6 +451,7 @@ def read_config(list):
dspam_users = cp.getaddrdict('dspam','dspam_users') dspam_users = cp.getaddrdict('dspam','dspam_users')
dspam_userdir = cp.getdefault('dspam','dspam_userdir') dspam_userdir = cp.getdefault('dspam','dspam_userdir')
dspam_screener = cp.getlist('dspam','dspam_screener') dspam_screener = cp.getlist('dspam','dspam_screener')
dspam_train = set(cp.getlist('dspam','dspam_train'))
dspam_reject = cp.getlist('dspam','dspam_reject') dspam_reject = cp.getlist('dspam','dspam_reject')
dspam_internal = cp.getboolean('dspam','dspam_internal') dspam_internal = cp.getboolean('dspam','dspam_internal')
if cp.has_option('dspam','dspam_sizelimit'): if cp.has_option('dspam','dspam_sizelimit'):
@@ -428,7 +499,7 @@ def read_config(list):
banned_users = cp.getlist('srs','banned_users') banned_users = cp.getlist('srs','banned_users')
if gossip: if gossip:
global gossip_node global gossip_node, gossip_ttl
if cp.has_option('gossip','server'): if cp.has_option('gossip','server'):
server = cp.get('gossip','server') server = cp.get('gossip','server')
host,port = gossip.splitaddr(server) host,port = gossip.splitaddr(server)
@@ -438,6 +509,25 @@ def read_config(list):
for p in cp.getlist('gossip','peers'): for p in cp.getlist('gossip','peers'):
host,port = gossip.splitaddr(p) host,port = gossip.splitaddr(p)
gossip_node.peers.append(gossip.server.Peer(host,port)) gossip_node.peers.append(gossip.server.Peer(host,port))
if cp.has_option('gossip','ttl'):
gossip_ttl = cp.getint('gossip','ttl')
else:
gossip_ttl = 1
# greylist section
if cp.has_option('greylist','dbfile'):
global greylist
grey_db = cp.getdefault('greylist','dbfile')
grey_days = 36
if cp.has_option('greylist','retain'):
grey_days = cp.getint('greylist','retain')
grey_expire = 4
if cp.has_option('greylist','expire'):
grey_expire = cp.getint('greylist','expire')
grey_time = 10
if cp.has_option('greylist','time'):
grey_time = cp.getint('greylist','expire')
greylist = Greylist(grey_db,grey_time,grey_expire,grey_days)
def findsrs(fp): def findsrs(fp):
lastln = None lastln = None
@@ -461,6 +551,12 @@ def findsrs(fp):
lastln = ln lastln = ln
break break
def param2dict(str):
pairs = [x.split('=',1) for x in str]
for e in pairs:
if len(e) < 2: e.append(None)
return dict([(k.upper(),v) for k,v in pairs])
class SPFPolicy(object): class SPFPolicy(object):
"Get SPF policy by result from sendmail style access file." "Get SPF policy by result from sendmail style access file."
def __init__(self,sender): def __init__(self,sender):
@@ -533,6 +629,12 @@ class SPFPolicy(object):
policy = 'REJECT' policy = 'REJECT'
return policy return policy
def getTempErrorPolicy(self):
policy = self.getPolicy('spf-temperror:')
if not policy:
policy = 'REJECT'
return policy
def getPassPolicy(self): def getPassPolicy(self):
policy = self.getPolicy('spf-pass:') policy = self.getPolicy('spf-pass:')
if not policy: if not policy:
@@ -542,7 +644,7 @@ class SPFPolicy(object):
from Milter.cache import AddrCache from Milter.cache import AddrCache
cbv_cache = AddrCache(renew=7) cbv_cache = AddrCache(renew=7)
auto_whitelist = AddrCache(renew=30) auto_whitelist = AddrCache(renew=60)
blacklist = AddrCache(renew=30) blacklist = AddrCache(renew=30)
class bmsMilter(Milter.Milter): class bmsMilter(Milter.Milter):
@@ -666,12 +768,13 @@ class bmsMilter(Milter.Milter):
def offense(self,inc=1): def offense(self,inc=1):
self.offenses += inc self.offenses += inc
if self.offenses > 3: if self.offenses > 3 and not self.trusted_relay:
try: try:
ip = addr2bin(self.connectip) ip = addr2bin(self.connectip)
if ip not in banned_ips: if ip not in banned_ips:
banned_ips.add(ip) banned_ips.add(ip)
print >>open('banned_ips','a'),self.connectip print >>open('banned_ips','a'),self.connectip
self.log("BANNED IP:",self.connectip)
except: pass except: pass
return Milter.REJECT return Milter.REJECT
@@ -680,6 +783,9 @@ class bmsMilter(Milter.Milter):
# of each message. # of each message.
def envfrom(self,f,*str): def envfrom(self,f,*str):
self.log("mail from",f,str) self.log("mail from",f,str)
#param = param2dict(str)
#self.envid = param.get('ENVID',None)
#self.mail_param = param
self.fp = StringIO.StringIO() self.fp = StringIO.StringIO()
self.tempname = None self.tempname = None
self.mailfrom = f self.mailfrom = f
@@ -690,6 +796,7 @@ class bmsMilter(Milter.Milter):
self.dspam = True self.dspam = True
self.whitelist = False self.whitelist = False
self.blacklist = False self.blacklist = False
self.greylist = False
self.reject_spam = True self.reject_spam = True
self.data_allowed = True self.data_allowed = True
self.delayed_failure = None self.delayed_failure = None
@@ -722,9 +829,9 @@ class bmsMilter(Milter.Milter):
self.user = self.getsymval('{auth_authen}') self.user = self.getsymval('{auth_authen}')
if self.user: if self.user:
# Very simple SMTP AUTH policy by defaul: # Very simple SMTP AUTH policy by default:
# any successful authentication is considered INTERNAL # any successful authentication is considered INTERNAL
# FIXME: configure allowed MAIL FROM by user # Detailed authorization policy is configured in the access file below.
self.internal_connection = True self.internal_connection = True
self.log( self.log(
"SMTP AUTH:",self.user, self.getsymval('{auth_type}'), "SMTP AUTH:",self.user, self.getsymval('{auth_type}'),
@@ -791,12 +898,14 @@ class bmsMilter(Milter.Milter):
return Milter.REJECT return Milter.REJECT
self.umis = None self.umis = None
self.spf = None self.spf = None
self.policy = None
if not (self.internal_connection or self.trusted_relay) \ if not (self.internal_connection or self.trusted_relay) \
and self.connectip and spf: and self.connectip and spf:
rc = self.check_spf() rc = self.check_spf()
if rc != Milter.CONTINUE: if rc != Milter.CONTINUE:
if rc != Milter.TEMPFAIL: self.offense() if rc != Milter.TEMPFAIL: self.offense()
return rc return rc
self.greylist = True
else: else:
rc = Milter.CONTINUE rc = Milter.CONTINUE
# FIXME: parse Received-SPF from trusted_relay for SPF result # FIXME: parse Received-SPF from trusted_relay for SPF result
@@ -804,16 +913,20 @@ class bmsMilter(Milter.Milter):
hres = self.spf and self.spf_helo hres = self.spf and self.spf_helo
# Check whitelist and blacklist # Check whitelist and blacklist
if auto_whitelist.has_key(self.canon_from): if auto_whitelist.has_key(self.canon_from):
self.greylist = False
if res == 'pass' or self.trusted_relay: if res == 'pass' or self.trusted_relay:
self.whitelist = True self.whitelist = True
self.log("WHITELIST",self.canon_from) self.log("WHITELIST",self.canon_from)
else: else:
self.dspam = False self.dspam = False
self.log("PROBATION",self.canon_from) self.log("PROBATION",self.canon_from)
if res not in ('permerror','softfail'):
self.cbv_needed = None
elif cbv_cache.has_key(self.canon_from) and cbv_cache[self.canon_from] \ elif cbv_cache.has_key(self.canon_from) and cbv_cache[self.canon_from] \
or domain in blacklist: or domain in blacklist:
# FIXME: don't use cbv_cache for blacklist if policy is 'OK'
if not self.internal_connection: if not self.internal_connection:
self.offense() self.offense(inc=2)
if not dspam_userdir: if not dspam_userdir:
if domain in blacklist: if domain in blacklist:
self.log('REJECT: BLACKLIST',self.canon_from) self.log('REJECT: BLACKLIST',self.canon_from)
@@ -824,12 +937,25 @@ class bmsMilter(Milter.Milter):
self.log('REJECT:',desc) self.log('REJECT:',desc)
self.setreply('550','5.7.1',*desc.splitlines()) self.setreply('550','5.7.1',*desc.splitlines())
return Milter.REJECT return Milter.REJECT
self.greylist = False # don't delay - use spam for training
self.blacklist = True self.blacklist = True
self.log("BLACKLIST",self.canon_from) self.log("BLACKLIST",self.canon_from)
else: else:
# REJECT delayed until after checking whitelist
if self.policy == 'REJECT':
self.log('REJECT: no PTR, HELO or SPF')
self.setreply('550','5.7.1',
"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 self.offense() # ban ip if too many bad MFROMs
global gossip global gossip
if gossip and domain and rc == Milter.CONTINUE \ if gossip and domain and rc == Milter.CONTINUE \
and not (self.internal_connection or self.trusted_relay): and not (self.internal_connection or self.trusted_relay) \
and gossip_node:
if self.spf and self.spf.result == 'pass': if self.spf and self.spf.result == 'pass':
qual = 'SPF' qual = 'SPF'
elif res == 'pass': elif res == 'pass':
@@ -861,6 +987,8 @@ class bmsMilter(Milter.Milter):
self.setreply('550','5.7.1', self.setreply('550','5.7.1',
'Your domain has been sending nothing but spam') 'Your domain has been sending nothing but spam')
return Milter.REJECT return Milter.REJECT
if self.reputation > 40 and self.confidence > 1:
self.greylist = False
except: except:
gossip = None gossip = None
raise raise
@@ -884,13 +1012,22 @@ class bmsMilter(Milter.Milter):
res,code,txt = q.check() res,code,txt = q.check()
q.result = res q.result = res
if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext: if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext:
self.cbv_needed = (q,res) # report SPF syntax error to sender self.cbv_needed = (q,'permerror') # report SPF syntax error to sender
res,code,txt = q.perm_error.ext # extended (lax processing) result res,code,txt = q.perm_error.ext # extended (lax processing) result
txt = 'EXT: ' + txt txt = 'EXT: ' + txt
p = SPFPolicy(q.s) p = SPFPolicy(q.s)
# FIXME: try:finally to close policy db, or reuse with lock # FIXME: try:finally to close policy db, or reuse with lock
if res in ('error','temperror'):
if self.need_cbv(p.getTempErrorPolicy(),q,'temperror'):
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'4.3.0',txt,
'We cannot accept your email until the DNS server for %s' % q.o,
'is operational for TXT record queries.'
)
return Milter.TEMPFAIL
res,code,txt = 'none',250,'EXT: ignoring DNS error'
hres = None hres = None
if res not in ('pass','error','temperror'): if res != 'pass':
if self.mailfrom != '<>': if self.mailfrom != '<>':
# check hello name via spf unless spf pass # check hello name via spf unless spf pass
h = spf.query(self.connectip,'',self.hello_name,receiver=receiver) h = spf.query(self.connectip,'',self.hello_name,receiver=receiver)
@@ -934,27 +1071,13 @@ class bmsMilter(Milter.Milter):
if self.missing_ptr and ores == 'none' and res != 'pass' \ if self.missing_ptr and ores == 'none' and res != 'pass' \
and hres != 'pass': and hres != 'pass':
# this bad boy has no credentials whatsoever # this bad boy has no credentials whatsoever
res = 'none'
policy = p.getNonePolicy() policy = p.getNonePolicy()
if policy == 'CBV': if policy in ('CBV','DSN'):
if self.mailfrom != '<>':
self.cbv_needed = (q,ores) # accept, but inform sender via DSN
self.offenses = 3 # ban ip if any bad recipient self.offenses = 3 # ban ip if any bad recipient
elif policy != 'OK': self.need_cbv(policy,q,'strike3')
self.log('REJECT: no PTR, HELO or SPF')
self.setreply('550','5.7.1',
"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'): if res in ('deny', 'fail'):
policy = p.getFailPolicy() if self.need_cbv(p.getFailPolicy(),q,'fail'):
if policy == 'CBV':
if self.mailfrom != '<>':
self.cbv_needed = (q,res)
elif policy != 'OK':
self.log('REJECT: SPF %s %i %s' % (res,code,txt)) self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt) self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read: # A proper SPF fail error message would read:
@@ -962,11 +1085,7 @@ class bmsMilter(Milter.Milter):
# "forged.org" in the sender address. Contact <postmaster@forged.org>. # "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT return Milter.REJECT
if res == 'softfail': if res == 'softfail':
policy = p.getSoftfailPolicy() if self.need_cbv(p.getSoftfailPolicy(),q,'softfail'):
if policy == 'CBV':
if self.mailfrom != '<>':
self.cbv_needed = (q,res)
elif policy != 'OK':
self.log('REJECT: SPF %s %i %s' % (res,code,txt)) self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply('550','5.7.1', self.setreply('550','5.7.1',
'SPF softfail: If you get this Delivery Status Notice, your email', 'SPF softfail: If you get this Delivery Status Notice, your email',
@@ -977,12 +1096,7 @@ class bmsMilter(Milter.Milter):
) )
return Milter.REJECT return Milter.REJECT
if res == 'neutral': if res == 'neutral':
policy = p.getNeutralPolicy() if self.need_cbv(p.getNeutralPolicy(),q,'neutral'):
if policy == 'CBV':
if self.mailfrom != '<>':
self.cbv_needed = (q,res)
# FIXME: this makes Received-SPF show wrong result
elif policy != 'OK':
self.log('REJECT: SPF neutral for',q.s) self.log('REJECT: SPF neutral for',q.s)
self.setreply('550','5.7.1', self.setreply('550','5.7.1',
'mail from %s must pass SPF: http://openspf.org/why.html' % q.o, 'mail from %s must pass SPF: http://openspf.org/why.html' % q.o,
@@ -994,11 +1108,7 @@ class bmsMilter(Milter.Milter):
) )
return Milter.REJECT return Milter.REJECT
if res in ('unknown','permerror'): if res in ('unknown','permerror'):
policy = p.getPermErrorPolicy() if self.need_cbv(p.getPermErrorPolicy(),q,'permerror'):
if policy == 'CBV':
if self.mailfrom != '<>':
self.cbv_needed = (q,res)
elif policy != 'OK':
self.log('REJECT: SPF %s %i %s' % (res,code,txt)) self.log('REJECT: SPF %s %i %s' % (res,code,txt))
# latest SPF draft recommends 5.5.2 instead of 5.7.1 # latest SPF draft recommends 5.5.2 instead of 5.7.1
self.setreply(str(code),'5.5.2',txt, self.setreply(str(code),'5.5.2',txt,
@@ -1006,10 +1116,6 @@ class bmsMilter(Milter.Milter):
'We cannot accept mail from %s until this is corrected.' % q.o 'We cannot accept mail from %s until this is corrected.' % q.o
) )
return Milter.REJECT return Milter.REJECT
if res in ('error','temperror'):
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'4.3.0',txt)
return Milter.TEMPFAIL
kv = {} kv = {}
if hres and q.h != q.o: if hres and q.h != q.o:
kv['helo_spf'] = hres kv['helo_spf'] = hres
@@ -1025,6 +1131,15 @@ class bmsMilter(Milter.Milter):
# track header mods separately from body mods - so use only # track header mods separately from body mods - so use only
# in emergencies. # in emergencies.
def envrcpt(self,to,*str): def envrcpt(self,to,*str):
try:
param = param2dict(str)
self.notify = param.get('NOTIFY','FAILURE,DELAY').upper().split(',')
if 'NEVER' in self.notify: self.notify = ()
#self.rcpt_param = param
except:
self.log("REJECT: invalid PARAM:",to,str)
self.setreply('550','5.7.1','Invalid SRS signature')
return Milter.REJECT
# mail to MAILER-DAEMON is generally spam that bounced # mail to MAILER-DAEMON is generally spam that bounced
if to.startswith('<MAILER-DAEMON@'): if to.startswith('<MAILER-DAEMON@'):
self.log('REJECT: RCPT TO:',to,str) self.log('REJECT: RCPT TO:',to,str)
@@ -1051,6 +1166,7 @@ class bmsMilter(Milter.Milter):
self.log("srs rcpt:",newaddr) self.log("srs rcpt:",newaddr)
self.dspam = False # verified as reply to mail we sent self.dspam = False # verified as reply to mail we sent
self.blacklist = False self.blacklist = False
self.greylist = False
self.delayed_failure = False self.delayed_failure = False
except: except:
if not (self.internal_connection or self.trusted_relay): if not (self.internal_connection or self.trusted_relay):
@@ -1063,9 +1179,12 @@ class bmsMilter(Milter.Milter):
self.setreply('550','5.7.1','Invalid SES signature') self.setreply('550','5.7.1','Invalid SES signature')
return Milter.REJECT return Milter.REJECT
# reject for certain recipients are delayed until after DATA # reject for certain recipients are delayed until after DATA
if auto_whitelist.has_precise_key(self.canon_from):
self.log("WHITELIST: DSN from",self.canon_from)
else:
if srs_reject_spoofed \ if srs_reject_spoofed \
and not user.lower() in ('postmaster','abuse'): and user.lower() not in ('postmaster','abuse'):
return self.forged_bounce() return self.forged_bounce(to)
self.data_allowed = not srs_reject_spoofed self.data_allowed = not srs_reject_spoofed
if not self.internal_connection and domain in private_relay: if not self.internal_connection and domain in private_relay:
@@ -1116,8 +1235,18 @@ class bmsMilter(Milter.Milter):
except: except:
self.log("rcpt to",to,str) self.log("rcpt to",to,str)
raise raise
self.log("rcpt to",to,str) if self.greylist and greylist and self.canon_from:
# no policy for trusted or internal
rc = greylist.check(self.connectip,self.canon_from,canon_to)
if rc == 0:
self.log("GREYLIST:",self.connectip,self.canon_from,canon_to)
self.setreply('451','4.7.1',
'Greylisted: http://projects.puremagic.com/greylisting/',
'Please retry in %.1f minutes'%(greylist.greylist_time/60.0))
return Milter.TEMPFAIL
self.log("GREYLISTED: %d"%rc)
self.log("rcpt to",to,str)
self.smart_alias(to) self.smart_alias(to)
# get recipient after virtusertable aliasing # get recipient after virtusertable aliasing
#rcpt = self.getsymval("{rcpt_addr}") #rcpt = self.getsymval("{rcpt_addr}")
@@ -1179,7 +1308,14 @@ class bmsMilter(Milter.Milter):
# original sender (encoded in Message-ID) is blacklisted # original sender (encoded in Message-ID) is blacklisted
elif lname == 'from': elif lname == 'from':
name,email = parseaddr(val) fname,email = parseaddr(val)
# check for porn keywords
lval = fname.lower().strip()
for w in porn_words:
if lval.find(w) >= 0:
self.log('REJECT: %s: %s' % (name,val))
self.setreply('550','5.7.1','Watch your language')
return Milter.REJECT
if email.lower().startswith('postmaster@'): if email.lower().startswith('postmaster@'):
# Yes, if From header comes last, this might not help much. # Yes, if From header comes last, this might not help much.
# But this is a heuristic - if MTAs would send proper DSNs in # But this is a heuristic - if MTAs would send proper DSNs in
@@ -1200,9 +1336,9 @@ class bmsMilter(Milter.Milter):
return Milter.REJECT return Milter.REJECT
return Milter.CONTINUE return Milter.CONTINUE
def forged_bounce(self): def forged_bounce(self,rcpt='-'):
if self.mailfrom != '<>': if self.mailfrom != '<>':
self.log("REJECT: bogus DSN") self.log("REJECT: bogus DSN",rcpt)
self.setreply('550','5.7.1', self.setreply('550','5.7.1',
"I do not accept normal mail from %s." % self.mailfrom.split('@')[0], "I do not accept normal mail from %s." % self.mailfrom.split('@')[0],
"All such mail has turned out to be Delivery Status Notifications", "All such mail has turned out to be Delivery Status Notifications",
@@ -1210,7 +1346,7 @@ class bmsMilter(Milter.Milter):
"you need to. Use another MAIL FROM if you need to send me mail." "you need to. Use another MAIL FROM if you need to send me mail."
) )
else: else:
self.log('REJECT: bounce with no SRS encoding') self.log('REJECT: bounce with no SRS encoding',rcpt)
self.setreply('550','5.7.1', self.setreply('550','5.7.1',
"I did not send you that message. Please consider implementing SPF", "I did not send you that message. Please consider implementing SPF",
"(http://openspf.org) to avoid bouncing mail to spoofed senders.", "(http://openspf.org) to avoid bouncing mail to spoofed senders.",
@@ -1231,9 +1367,10 @@ class bmsMilter(Milter.Milter):
if gossip and self.umis: if gossip and self.umis:
gossip_node.feedback(self.umis,1) gossip_node.feedback(self.umis,1)
return rc return rc
elif self.whitelist_sender and lname == 'subject': elif self.whitelist_sender:
# check for AutoReplys # check for AutoReplys
if reautoreply.match(val): if (lname == 'subject' and reautoreply.match(val)) \
or (lname == 'user-agent' and val.lower().startswith('vacation')):
self.whitelist_sender = False self.whitelist_sender = False
self.log('AUTOREPLY: not whitelisted') self.log('AUTOREPLY: not whitelisted')
@@ -1420,6 +1557,12 @@ class bmsMilter(Milter.Milter):
elif not self.internal_connection or dspam_internal: elif not self.internal_connection or dspam_internal:
if len(txt) > dspam_sizelimit: if len(txt) > dspam_sizelimit:
self.log("Large message:",len(txt)) self.log("Large message:",len(txt))
if self.blacklist:
self.log('REJECT: BLACKLISTED')
self.setreply('550','5.7.1',
'%s has been blacklisted.'%self.canon_from)
self.fp = None
return Milter.REJECT
return False return False
if user == 'honeypot' and Dspam.VERSION >= '1.1.9': if user == 'honeypot' and Dspam.VERSION >= '1.1.9':
keep = False # keep honeypot mail keep = False # keep honeypot mail
@@ -1431,9 +1574,12 @@ class bmsMilter(Milter.Milter):
return False return False
if self.spf and self.mailfrom != '<>': if self.spf and self.mailfrom != '<>':
# check that sender accepts quarantine DSN # check that sender accepts quarantine DSN
if self.spf.result == 'pass':
msg = mime.message_from_file(StringIO.StringIO(txt)) msg = mime.message_from_file(StringIO.StringIO(txt))
rc = self.send_dsn(self.spf,msg,'quarantine') rc = self.send_dsn(self.spf,msg,'quarantine')
del msg del msg
else:
rc = self.send_dsn(self.spf)
if rc != Milter.CONTINUE: if rc != Milter.CONTINUE:
return rc return rc
ds.check_spam(user,txt,self.recipients,quarantine=True, ds.check_spam(user,txt,self.recipients,quarantine=True,
@@ -1451,8 +1597,13 @@ class bmsMilter(Milter.Milter):
elif self.blacklist: elif self.blacklist:
txt = ds.check_spam(user,txt,self.recipients, txt = ds.check_spam(user,txt,self.recipients,
force_result=dspam.DSR_ISSPAM) force_result=dspam.DSR_ISSPAM)
else: elif user in dspam_train:
txt = ds.check_spam(user,txt,self.recipients) txt = ds.check_spam(user,txt,self.recipients)
else:
txt = ds.check_spam(user,txt,self.recipients,classify=True)
if txt:
self.add_header("X-DSpam-Score",'%f' % ds.probability)
return False
if not txt: if not txt:
# DISCARD if quarrantined for any recipient. It # DISCARD if quarrantined for any recipient. It
# will be resent to all recipients if they submit # will be resent to all recipients if they submit
@@ -1483,7 +1634,7 @@ class bmsMilter(Milter.Milter):
ds.check_spam(screener,txt,self.recipients, ds.check_spam(screener,txt,self.recipients,
force_result=dspam.DSR_ISINNOCENT) force_result=dspam.DSR_ISINNOCENT)
return False return False
if self.reject_spam: if self.reject_spam and self.spf.result != 'pass':
self.log("DSPAM:",screener, self.log("DSPAM:",screener,
'REJECT: X-DSpam-Score: %f' % ds.probability) 'REJECT: X-DSpam-Score: %f' % ds.probability)
self.setreply('550','5.7.1','Your Message looks spammy') self.setreply('550','5.7.1','Your Message looks spammy')
@@ -1493,12 +1644,18 @@ class bmsMilter(Milter.Milter):
if self.spf and self.mailfrom != '<>': if self.spf and self.mailfrom != '<>':
# check that sender accepts quarantine DSN # check that sender accepts quarantine DSN
self.fp.seek(0) self.fp.seek(0)
if self.spf.result == 'pass' or self.cbv_needed:
msg = mime.message_from_file(self.fp) msg = mime.message_from_file(self.fp)
if self.spf.result == 'pass':
rc = self.send_dsn(self.spf,msg,'quarantine') rc = self.send_dsn(self.spf,msg,'quarantine')
else:
rc = self.do_needed_cbv(msg)
del msg
else:
rc = self.send_dsn(self.spf)
if rc != Milter.CONTINUE: if rc != Milter.CONTINUE:
self.fp = None self.fp = None
return rc return rc
del msg
if not ds.check_spam(screener,txt,self.recipients,classify=True): if not ds.check_spam(screener,txt,self.recipients,classify=True):
self.fp = None self.fp = None
return Milter.DISCARD return Milter.DISCARD
@@ -1541,6 +1698,23 @@ class bmsMilter(Milter.Milter):
quarantine=False) quarantine=False)
self.log("TRAINSPAM:",screener,'X-Dspam-Score: %f' % ds.probability) self.log("TRAINSPAM:",screener,'X-Dspam-Score: %f' % ds.probability)
def do_needed_cbv(self,msg):
q,template_name = self.cbv_needed
rc = self.send_dsn(q,msg,template_name)
self.cbv_needed = None
return rc
def need_cbv(self,policy,q,tname):
self.policy = policy
if policy == 'CBV':
if self.mailfrom != '<>' and not self.cbv_needed:
self.cbv_needed = (q,None)
elif policy == 'DSN':
if self.mailfrom != '<>' and not self.cbv_needed:
self.cbv_needed = (q,tname)
elif policy != 'OK': return True
return False
def eom(self): def eom(self):
if not self.fp: if not self.fp:
return Milter.ACCEPT # no message collected - so no eom processing return Milter.ACCEPT # no message collected - so no eom processing
@@ -1661,22 +1835,12 @@ class bmsMilter(Milter.Milter):
except Milter.error: except Milter.error:
self.addheader(name,val) # older sendmail can't insheader self.addheader(name,val) # older sendmail can't insheader
# do not send CBV to internal domains (since we'll just get # Do not send CBV to internal domains (since we'll just get
# the "Fraudulent MX" error). # the "Fraudulent MX" error). Whitelisted senders clearly do not
# need CBV. However, whitelisted domains might (to discover
# bogus localparts). Need a way to tell the difference.
if self.cbv_needed and not self.internal_domain: if self.cbv_needed and not self.internal_domain:
q,res = self.cbv_needed rc = self.do_needed_cbv(msg)
if res == 'softfail':
template_name = 'softfail'
elif res in ('fail','deny'):
template_name = 'fail'
elif res in ('unknown','permerror'):
template_name = 'permerror'
elif res == 'neutral':
template_name = 'neutral'
else:
template_name = 'strike3'
rc = self.send_dsn(q,msg,template_name)
self.cbv_needed = None
if rc == Milter.REJECT: if rc == Milter.REJECT:
# Do not feedback here, because feedback should only occur # Do not feedback here, because feedback should only occur
# for messages that have gone to DATA. Reputation lets us # for messages that have gone to DATA. Reputation lets us
@@ -1744,22 +1908,24 @@ class bmsMilter(Milter.Milter):
out.close() out.close()
return Milter.TEMPFAIL return Milter.TEMPFAIL
def send_dsn(self,q,msg,template_name): def send_dsn(self,q,msg=None,template_name=None):
sender = q.s sender = q.s
cached = cbv_cache.has_key(sender) cached = cbv_cache.has_key(sender)
if cached: if cached:
self.log('CBV:',sender,'(cached)') self.log('CBV:',sender,'(cached)')
res = cbv_cache[sender] res = cbv_cache[sender]
else: else:
m = None
if template_name:
fname = template_name+'.txt' fname = template_name+'.txt'
try: try:
template = file(template_name+'.txt').read() template = file(template_name+'.txt').read()
self.log('CBV:',sender,'Using:',fname)
except IOError:
template = None
self.log('CBV:',sender,'PLAIN')
m = dsn.create_msg(q,self.recipients,msg,template) m = dsn.create_msg(q,self.recipients,msg,template)
if m: self.log('CBV:',sender,'Using:',fname)
except IOError: pass
if not m:
self.log('CBV:',sender,'PLAIN (%s)'%q.result)
else:
if srs: if srs:
# Add SRS coded sender to various headers. When (incorrectly) # Add SRS coded sender to various headers. When (incorrectly)
# replying to our DSN, any of these which are preserved # replying to our DSN, any of these which are preserved
@@ -1783,7 +1949,10 @@ class bmsMilter(Milter.Milter):
return Milter.TEMPFAIL return Milter.TEMPFAIL
cbv_cache[sender] = res cbv_cache[sender] = res
self.log('REJECT:',desc) self.log('REJECT:',desc)
try:
self.setreply('550','5.7.1',*desc.splitlines()) self.setreply('550','5.7.1',*desc.splitlines())
except TypeError:
self.setreply('550','5.7.1',"Callback failure")
return Milter.REJECT return Milter.REJECT
cbv_cache[sender] = res cbv_cache[sender] = res
return Milter.CONTINUE return Milter.CONTINUE
@@ -1810,6 +1979,7 @@ def main():
return return
try: try:
from glob import glob from glob import glob
global banned_ips
banned_ips = set(addr2bin(ip) banned_ips = set(addr2bin(ip)
for fn in glob('banned_ips*') for fn in glob('banned_ips*')
for ip in open(fn)) for ip in open(fn))
+41
View File
@@ -2,6 +2,47 @@ Title: Recent Changes
<h2> Recent Changes </h2> <h2> Recent Changes </h2>
<h3> 0.8.10 </h3>
SRS rejections now log the recipient.
I have finally implemented plain CBV (no DSN). The CBV policy
will do a plain CBV from now on, and the DSN policy is required
if you want to send a DSN.
I started checking the MAIL FROM fullname (human readable part
of an email) for porn keywords. There is now a banned IP database.
IPs are banned for too many bad MAIL FROMs or RCPT TOs, and remain banned
for 7 days.
<h3> 0.8.9 </h3>
I use the <code>%ifarch</code> hack to build milter and milter-spf
packages as noarch, while pymilter is built as native.
I removed the spf dependency from dsn.py, so pymilter can be used without
installing pyspf, and added a Milter.dns module to let python milters do
general DNS lookups without loading pyspf.
<h3> 0.8.8 </h3>
Programs do not belong in the /var/log directory. I moved the
milter apps to /usr/lib/pymilter. Since having the programs and
data in the same directory is convenient for debugging, it will
still use an executable present in the datadir.
Several general utility classes and functions are now in the Milter package
for possible use by other python milters. In addition to the trivial example
milter, a simple SPF only milter is included as a realistic example.
The spec file now build 3 RPMs:
<ul>
<li> pymilter is the milter module and Milter package for use by all python
milters.
<li> milter is the all-singing, all-dancing python milter application, with
supporting <code>/etc/init.d</code>, logrotate and other scripts.
<li> milter-spf is the simple SPF only milter application.
</ul>
<h3> 0.8.7 </h3> <h3> 0.8.7 </h3>
The spf module has been moved to the The spf module has been moved to the
+1 -1
View File
@@ -5,7 +5,7 @@ Title: Credits
<a href="mailto:Jim Niemira <urmane@urmane.org>">Jim Niemira</a> <a href="mailto:Jim Niemira <urmane@urmane.org>">Jim Niemira</a>
wrote the original C module and some quick wrote the original C module and some quick
and dirty python to use it. and dirty python to use it.
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">Stuart D. Gathman</a> <a href="http://gathman.org/vitae">Stuart D. Gathman</a>
took that kludge and added threading and context objects to it, wrote a proper 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 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 it with distutils, and generally transformed it from a quick hack to a
+1 -1
View File
@@ -11,7 +11,7 @@
<li><a href="credits.html">CREDITS</a> <li><a href="credits.html">CREDITS</a>
<li><a href="http://sourceforge.net"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=139894&amp;type=1" width="88" height="31" border="0" alt="SourceForge.net Logo" /></a> <li><a href="http://sourceforge.net"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=139894&amp;type=1" width="88" height="31" border="0" alt="SourceForge.net Logo" /></a>
<h3>Links</h3> <h3>Links</h3>
<li><a href="http://www.milter.org/milter_api/api.html">C&nbsp;API</a> <li><a href="https://www.milter.org/developers/api/index">C&nbsp;API</a>
<li><a href="http://www.milter.org/">Milter.Org</a> <li><a href="http://www.milter.org/">Milter.Org</a>
<li><a href="http://www.python.org/">Python.Org</a> <li><a href="http://www.python.org/">Python.Org</a>
<li><a href="http://www.sendmail.org/">Sendmail.Org</a> <li><a href="http://www.sendmail.org/">Sendmail.Org</a>
+17 -60
View File
@@ -13,14 +13,19 @@ ALT="Viewable With Any Browser" BORDER="0"></A>
</map> </map>
</P> </P>
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align=left> <table rules="none">
<tr><td>
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align="top">
Mascot by <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>
</td>
<td>
<h1 align=center>Sendmail Milters in Python</h1> <h1 align=center>Sendmail Milters in Python</h1>
<h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a> <h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a>
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D"> and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
Stuart D. Gathman</a><br> Stuart D. Gathman</a><br>
This web page is written by Stuart D. Gathman<br>and<br>sponsored by This web page is written by Stuart D. Gathman<br>and<br>sponsored by
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br> <a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
Last updated Mar 30, 2007</h4> Last updated Aug 26, 2008</h4>
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> | See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
<a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> | <a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
@@ -31,7 +36,7 @@ See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/sho
<a href="//www.python.org"> <a href="//www.python.org">
<img src="python55.gif" align=left alt="A Python"></a> <img src="python55.gif" align=left alt="A Python"></a>
<a href="//www.sendmail.org/">Sendmail</a> introduced a <a href="//www.sendmail.org/">Sendmail</a> introduced a
<a href="http://www.milter.org/milter_api/api.html"> new API</a> beginning with version 8.10 - <a href="https://www.milter.org/developers/api/index"> new API</a> beginning with version 8.10 -
libmilter. The milter module for <a href="//www.python.org">Python</a> libmilter. The milter module for <a href="//www.python.org">Python</a>
provides a python interface to libmilter that exploits all its features. provides a python interface to libmilter that exploits all its features.
<p> <p>
@@ -39,7 +44,11 @@ Sendmail 8.12 officially releases libmilter.
Version 8.12 seems to be more robust, and includes new privilege Version 8.12 seems to be more robust, and includes new privilege
separation features to enhance security. Even better, sendmail 8.13 separation features to enhance security. Even better, sendmail 8.13
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more
efficient and secure. I recommend upgrading. efficient and secure. Sendmail 8.14 finally supports modifying
MAIL FROM via the milter API. Unfortunately, I haven't gotten around
to supporting that yet in python milter.
</td></tr>
</table>
<h3><a name=overview>Overview</a></h3> <h3><a name=overview>Overview</a></h3>
@@ -48,7 +57,7 @@ href="#milter">milters</a>, and the beginnings of a general purpose mail
filtering system written in Python. filtering system written in Python.
<p> <p>
At the lowest level, the 'milter' module provides a thin wrapper around the At the lowest level, the 'milter' module provides a thin wrapper around the
<a href="http://www.milter.org/milter_api/api.html"> <a href="https://www.milter.org/developers/api/index">
sendmail libmilter API</a>. This API lets you register callbacks for sendmail libmilter API</a>. This API lets you register callbacks for
a number of events in the a number of events in the
<a href="http://www.cs.concordia.ca/~group/fig/public/email/relay/milter+ruleset-checks.html">process of sendmail receiving a message via SMTP</a>. <a href="http://www.cs.concordia.ca/~group/fig/public/email/relay/milter+ruleset-checks.html">process of sendmail receiving a message via SMTP</a>.
@@ -121,7 +130,7 @@ copy mail.
<li> For more ideas, check the <a href="//www.milter.org">Milter Web Page</a>. <li> For more ideas, check the <a href="//www.milter.org">Milter Web Page</a>.
</menu> </menu>
<a href="http://www.milter.org/milter_api/api.html"> <a href="https://www.milter.org/developers/api/index">
Documentation</a> for the C API is provided with sendmail. Miltermodule Documentation</a> for the C API is provided with sendmail. Miltermodule
provides a thin python wrapper for the C API. Milter.py provides a simple provides a thin python wrapper for the C API. Milter.py provides a simple
OO wrapper on top of that. OO wrapper on top of that.
@@ -202,60 +211,8 @@ href="http://www.duh.org/cvsweb.cgi/~checkout~/pmilter/doc/milter-protocol.txt?r
<h3> Confirmed Installations </h3> <h3> Confirmed Installations </h3>
Please <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">email</a> Please <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">email</a>
me if you successfully install milter on a system not mentioned below. me if you do <i>not</i> successfully install milter. The confirmed
<p> installations are too numerous to list at this point.
<table>
<tr>
<th>Operating System</th> <th>Compiler</th> <th>Python</th> <th>Sendmail</th>
<th>milter</th>
<tr>
<td>Mandrake 8.0</td><td>gcc-3.0.1</td><td>2.1.1</td><td>8.12.0</td>
<td>0.3.3</td><tr>
<td>Mandrake 8.0</td><td>gcc-2.96</td><td>2.0</td><td>8.11.2</td>
<td>0.3.6</td><tr>
<td>RedHat 6.2</td><td>egcs-1.1.2</td><td>2.2.2</td><td>8.11.6</td>
<td>0.5.4</td><tr>
<td>RedHat 7.1</td><td>gcc-2.96</td><td>?</td><td>8.12.1</td>
<td>0.3.5</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.2.2</td><td>8.11.6</td>
<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>
<td>0.5.4</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.11.5</td>
<td>0.3.3</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.1</td>
<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.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>
<td>0.5.4</td><tr>
<td>OpenBSD</td><td>?</td><td>2.3.3?</td><td>8.13.1?</td>
<td>0.7.2</td><tr>
<td>SuSE 7.3</td><td>gcc-2.95.3</td><td>2.1.1</td><td>8.12.2</td>
<td>0.3.9</td><tr>
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.1</td><td>8.12.3</td>
<td>0.4.0</td><tr>
<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>
</table>
<h2> Enough Already! </h2> <h2> Enough Already! </h2>
+158 -146
View File
@@ -4,8 +4,7 @@ Title: Python Milter Mail Policy
These are the policies implemented by the <code>bms.py</code> milter These are the policies implemented by the <code>bms.py</code> milter
application. The milter and Milter modules do not implement any policies application. The milter and Milter modules do not implement any policies
by themselves. Eventually, I'll get the bms.py milter moved to its by themselves.
own package.
<h3> Classify connection </h3> <h3> Classify connection </h3>
@@ -77,161 +76,174 @@ altered accordingly.
<h2> SPF check </h2> <h2> SPF check </h2>
Finally, the MAIL FROM, connect IP, and HELO name are checked against The MAIL FROM, connect IP, and HELO name are checked against
any SPF records published via DNS for the alleged sender (MAIL FROM). any SPF records published via DNS for the alleged sender (MAIL FROM)
If there is no SPF record, we check for a local substitute under the to determine the official SPF policy result.
domain defined in the <code>[spf]delegate</code> configuration. The offical SPF result is then logged in the Received-SPF header field,
Further checks depend on the result. but certain results are subjected to further processing to create
an effective result for policy purposes.
<table border=1> <p>
<tr><th>NONE</th><td> If the official result is 'none', we try to turn it into an effective result of
If there is no SPF record (official or delegated), then we 'pass' or 'fail'. First, we check for a local substitute SPF record
initiate a "three strikes and your out" regime, which looks for under the domain defined in the <code>[spf]delegate</code> configuration.
<b>some</b> form of validated identification. It is often useful to add local SPF records for correspondents that are
<ol> too clueless to add their own. If there is no local substitute, we use a "best
<li>We try a "best guess" SPF record of "v=spf1 a/24 mx/24 ptr". If this guess" SPF record of "v=spf1 a/24 mx/24 ptr" for MAIL FROM or "v=spf1 a/24
passes, good. mx/24" for HELO. In addition, a HELO that is a subdomain of MAIL FROM and
<li> We try to validate the HELO name. First check for an SPF record. resolves to the connect IP results in an effective result of 'pass'.
Otherwise, check whether the connect IP matches any A record for <p>
the HELO name, or any A record for any MX name for the HELO name, If there is no local SPF record, and the effective result is still not
or is at least in the same /24 subnet as any of the above. 'pass', we check for either a valid HELO name or a valid PTR record for
(In other words, a HELO SPF "best guess" of "v=spf1 a/24 mx/24".) the connect IP. A valid HELO or PTR cannot look like a dynamic name
If so, good. We consider the HELO validated. If the HELO SPF as determined by the heuristic in <code>Milter.dynip</code>.
check fails, we reject the email. <p>
</ol> If HELO has an SPF record, and the result is anything but pass, we reject
the connection:
<pre> <pre>
2005Jul30 19:45:16 [93991] connect from [221.200.41.54] at ('221.200.41.54', 3581) EXTERNAL DYN 2005Jul30 19:45:16 [93991] connect from [221.200.41.54] at ('221.200.41.54', 3581) EXTERNAL DYN
2005Jul30 19:45:18 [93991] hello from adelphia.net 2005Jul30 19:45:18 [93991] hello from adelphia.net
2005Jul30 19:45:19 [93991] mail from <wendy.stubbsua@link-it.com> () 2005Jul30 19:45:19 [93991] mail from <wendy.stubbsua@link-it.com> ()
2005Jul30 19:45:19 [93991] REJECT: hello SPF: fail 550 access denied 2005Jul30 19:45:19 [93991] REJECT: hello SPF: fail 550 access denied
</pre> </pre>
<ol> Note that HELO does not have any forwarding issues like MAIL FROM, and so
<li> If there is a validated PTR name, and it doesn't look any result other than 'pass' or 'none' should be treated like 'fail'.
like a dynamic name, good. We consider the connection validated.
</ol>
If any of the above can be validated, we continue on.
If none of the above can be validated, and the <code>[SPF]reject_noptr</code>
option is true, we reject the message immediately with the explanation
that we need some form of valid identification before we accept an email.
If <code>[SPF]reject_noptr</code> is false, we flag the message as
needing Call Back Validation.
The Call Back Valildation sends a DSN to the purported sender informing
them of the lack of identification. If the message is legitimate, the
sender needs to know that their email setup is broken and should be corrected.
If the message is forged, the sender is informed of the forgery,
and their need to publish an SPF record or at least use a valid HELO name.
If the purported sender does not accept the DSN,
then the message is rejected. The CBV status is cached to avoid
annoying the purported sender with too many DSNs. Currently, the DSN
is repeated to the same sender once per month.
<p> <p>
In this example, although 3com.com has no SPF record, we assume that Only if nothing about the SMTP envelope can be validated does the effective
any legitimate mail from them will at least have a valid HELO or PTR. result remain 'none. I call this the "3 strikes" rule.
<pre>
2005Jul30 23:52:03 [96777] connect from [222.252.233.200] at ('222.252.233.200', 29934) EXTERNAL DYN
2005Jul30 23:52:03 [96777] hello from 3mail.3com.com
2005Jul30 23:52:04 [96777] mail from <etec_nic_family@3mail.3com.com> ()
2005Jul30 23:52:04 [96777] REJECT: no PTR, HELO or SPF
</pre>
</td></tr>
<tr><th>PASS</th><td>
A pass result normally lets the email continue on, but the domain is
tracked for reputation (and may be blocked), and may skip content scanning if
it matches a whitelist.
<pre>
2005Jul24 17:44:26 [2104] mail from <gnucash-devel-bounces@gnucash.org> ('SIZE=4410',)
2005Jul24 17:44:26 [2104] Received-SPF: pass (mail.bmsi.com: domain of gnucash.org
designates 204.107.200.65 as permitted sender)
client-ip=204.107.200.65; envelope-from=gnucash-devel-bounces@gnucash.org; helo=cvs.gnucash.org;
</pre>
</td></tr>
<tr><th>NEUTRAL</th><td>
A neutral result normally lets the email continue on, but the domain is not
tracked for reputation or matched against any whitelists.
Highly forged domains listed in <code>[SPF]reject_neutral</code> are
rejected.
<pre>
2005Jul24 17:41:37 [2070] connect from cp500627-a.dbsch1.nb.home.nl at ('84.27.225.3', 3465) EXTERNAL
2005Jul24 17:41:37 [2070] hello from cp500627-a.dbsch1.nb.home.nl
2005Jul24 17:41:38 [2070] mail from <nwarjejkw@yahoo.com> ()
2005Jul24 17:41:38 [2070] REJECT: SPF neutral for nwarjejkw@yahoo.com
</pre>
</td></tr>
<tr><th>SOFTFAIL</th><td>
A softfail result normally lets the email continue on, but the domain is not
tracked for reputation or matched against any whitelists. Furthermore,
the message is flagged as needing Call Back Validation,
and the highly forged domains listed in <code>[SPF]reject_neutral</code> are
rejected as well.
<p> <p>
At present, we also require a valid HELO or PTR to avoid rejecting If the official result is 'permerror' (a syntax error in the sender's
a softfail. But this should probably change to only require a policy), we use the 'lax' option in pyspf to try various heuristics to guess
successful CBV. what they really meant. For instance, the invalid mechanism "ip:1.2.3.4" is
treated as "ip4:1.2.3.4". The result of lax processing is then used
as the effective result for policy purposes.
<p> <p>
The Call Back Valildation sends a DSN to the purported sender informing With an effective SPF result in hand, we consult the sendmail access
them of the softfail. If the message is legitimate, the sender needs database to find our receiver policy for the sender.
to know about the softfail so that their email setup can be corrected.
If the message is forged, the sender is informed of the forgery, confirming
that SPF is protecting their reputation and encouraging a rapid transition
to a strict policy. If the purported sender does not accept the DSN,
then the message is rejected. The CBV status is cached to avoid
annoying the purported sender with too many DSNs. Currently, the DSN
is repeated to the same sender once per month.
<pre>
2005Jul24 15:41:33 [801] mail from <Aitp@horafeliz.com> ()
2005Jul24 15:41:33 [801] Received-SPF: softfail (mail.bmsi.com: transitioning domain of horafeliz.com
does not designate 221.184.83.185 as permitted sender)
client-ip=221.184.83.185; envelope-from=Aitp@horafeliz.com;
helo=p8185-ipad30funabasi.chiba.ocn.ne.jp;
2005Jul24 15:41:33 [801] rcpt to <david@example.com> ()
2005Jul24 15:41:35 [801] Subject: Microsoft, Adobe, Macromedia, Corel software. Up to 80% discount.
2005Jul24 15:41:35 [801] X-Mailer: Microsoft Outlook, Build 10.0.2605
2005Jul24 15:41:35 [801] CBV: Aitp@horafeliz.com
2005Jul24 15:41:38 [801] REJECT: CBV: 550 <Aitp@horafeliz.com>: User unknown
</pre>
</td></tr>
<tr><th>FAIL</th><td> <table border=1>
The message is rejected with a reference the SPF why page. <tr><th>REJECT</th><td>
<pre> Reject the sender with a 550 5.7.1 SMTP code. The SMTP rejection
2005Jul30 19:53:27 [94070] connect from [212.70.52.16] at ('212.70.52.16', 3192) EXTERNAL DYN includes a detailed description of the problem.
2005Jul30 19:53:27 [94070] hello from winzip.com
2005Jul30 19:53:27 [94070] mail from <dan@winzip.com> ()
2005Jul30 19:53:27 [94070] REJECT: SPF fail 550 SPF fail:
see http://openspf.com/why.html?sender=dan@winzip.com&ip=212.70.52.16
</pre>
</td></tr> </td></tr>
<tr><th>CBV</th><td>
<tr><th>PERMERROR</th><td> Do a Call Back Validation by connecting to an MX of the sender
Permanent errors were called "unknown", and are still show that way and checking that using the sender as the RCPT TO is not rejected.
in the log. The message is rejected. Previously, we enabled "lax" parsing We quit the CBV connection before actualling sending a message.
of the SPF record, but rejecting is better because it informs the If the CBV is rejected, our SMTP connection is rejected with the
sender about their problem. The next milter version will same error code and message. CBV results are cached.
look for a local substitute SPF record (as for a missing SPF record)
before rejecting. This will inform the sender of their problem, but
also let the receiver install a temporary workaround.
<pre>
2005Jul24 18:05:37 [2312] mail from <b-mihdbcgaacaa-becibijh-000-@msg.euxiphipops.com> ()
2005Jul24 18:05:37 [2312] REJECT: SPF unknown 550 SPF Permanent Error:
include mechanism missing domain: include
</pre>
The SPF record for msg.euxiphipops.com looked like this at the time of the
above error:
<pre>
msg.euxiphipops.com TXT "v=spf1 mx ptr a include"
</pre>
</td></tr> </td></tr>
<tr><th>DSN</th><td>
<tr><th>TEMPERROR</th><td> Do a Call Back Validation by connecting to an MX of the sender
Temporary errors result in a 451 "Try again later" response. The sender and checking that using the sender as the RCPT TO is not rejected.
should retry the message at a later time. Unlike a CBV, we continue on to data and send a detailed message
<pre> explaining the problem. This can be useful for reporting PermError
2005Jul24 07:33:13 [29846] mail from <quickenloans@rate.quicken.com> ('SIZE=73775', 'BODY=8BITMIME') or SoftFail to the sender. Keep in mind that for any result other
2005Jul24 07:33:43 [29846] TEMPFAIL: SPF error 450 SPF Temporary Error: DNS Timeout than 'pass', the sender could be forged, and your DSN could annoy the
</pre> wrong person. However, a SoftFail result is requesting such feedback
for debugging and a PermError result needs to be fixed by the sender ASAP
whether forged or not. DSN results are cached so that senders are
annoyed only weekly.
</td></tr>
<tr><th>OK</th><td>
Accept the sender. The message may still be rejected via reputation
or content filtering.
</td></tr> </td></tr>
</table> </table>
<h3> SPF policy syntax </h3>
First, the full sender is checked:
<pre>
SPF-Fail:abeb@adelphia.net DSN
</pre>
This says to accept mail from that adelphia.net user despite the
SPF fail, but only after annoying them with a DSN about their ISP's broken
policy.
<p>
If there is no match on the full sender, the domain is checked:
<pre>
SPF-Neutral:aol.com REJECT
</pre>
This says to reject mail from AOL with an SPF result of neutral.
This means AOL users can't use their AOL address with another mail service
to send us mail. This is good because the other mail service is
likely a badly configured greeting card site or a virus.
<p>
Finally, a default policy for the result is checked. While there are program
defaults, you should have defaults in the access database for SPF results:
<pre>
SPF-Neutral: CBV
SPF-Softfail: DSN
SPF-PermError: DSN
SPF-TempError: REJECT
SPF-None: REJECT
SPF-Fail: REJECT
SPF-Pass: OK
</pre>
<h2> Reputation </h2>
If the sender has not been rejected by this point, and if a GOSSiP server is
configured, we consult GOSSiP for the reputation score of the sender and
SPF result. The score is a number from -100 to 100 with a confidence
percentage from 0 to 100. A really bad reputation (less than -50 with
confidence greater than 3) is rejected. Note that the reputation is tracked
independently for each SPF result and sender combination. So aol.com:neutral
might have a really bad reputation, while aol.com:pass would be ok.
Furthermore, when a sender finally publishes an SPF policy and starts
getting SPF pass, their reputation is effectively reset.
<h2> Whitelists and Blacklists </h2>
The administrator can whitelist or blacklist senders and sending domains by
appending them to <code>${datadir}/auto_whitelist.log</code> or
<code>${datadir}/blacklist.log</code> respectively. In addition,
recipients of internal senders (except for automatic replies like vacation
messages and return receipts) are automatically whitelisted for 60 days, and
senders that fail CBV or DSN checks are automatically blacklisted for 30 days.
Whitelisted and blacklisted senders are used to automatically train the
bayesian content filter before being delivered or rejected, respectively.
<p>
Real Soon Now users will be able to maintain their own whitelist and
blacklist that applies only when they are the recipient.
<h2> Content Filter </h2>
Most messages have been rejected or delivered by now, but spammers
are always finding new places to send their junk from. For instance,
we get around 10000 emails a day, of which around 500 are first time
spam senders. A bayesian filter is trained by the whitelists and
blacklists, and scores the message. What is likely spam is either
rejected or quarantined. If the sender is an effective SPF pass,
then they get a DSN notifying them that their message has been
quarantined. (A DSN failure gets the sender auto blacklisted.)
Else, if the reject_spam option is set, the message is rejected.
Otherwise, a CBV is done (failure gets the sender auto blacklisted)
and the message is silently quarantined.
<p>
Normally, you don't want email messages to silently disappear into
a black hole, so you should set the reject_spam option. However,
if you don't want your correspondent's email to get rejected, you can
check your quarantine frequently instead.
<h3> Honeypot </h3>
You can also blacklist recipients by listing them as aliases of the
'honeypot' dspam user. These are collectively called
the honeypot. Any email to these recipients is used to train the
spam filter as spam and chalk up a reputation demerit for the sender, then
discarded. It might be a good idea to blacklist the sender if it has SPF pass
as well, but I'm afraid of accidents.
<h3> Reputation </h3>
Reputation is tracked by sending domain and effective SPF result.
The GOSSiP server tracks the spam/ham status of the last 1024 messages
for each domain:result combination. When the server is queried during
the SMTP envelope phase (MAIL FROM), it also queries any configured
peers, and the scores are combined. Domains with a history of spam for
a given SPF result are rejected at MAIL FROM. The GOSSiP system has
a command line utility to reset (delete) a reputation for cases where a
sender that was infected with malware is repaired. In addition,
the confidence score of a reputation decays with time, so a bad sender
will eventually be able to try again without manual intervention.
+35
View File
@@ -0,0 +1,35 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: SPF fail (EMAIL FORGERY)
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification.
*** WARNING! YOU ARE SENDING FROM AN UNAUTHORIZED LOCATION ***
The email administrator for '%(sender_domain)' (YOUR administrator)
has FORBIDDEN you to send email from this location. IMMEDIATELY contact your
email administrator and follow his instructions to properly send mail.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy indicated that the above email was forged.
Because we believe your policy is in error, we have accepted the
email anyway. Please ask your email administrator to review
your SPF policy. You may also have neglected to follow your
postmaster's instructions for configuring outgoing email.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
Stuart D Gathman
postmaster@%(receiver)s
+20 -2
View File
@@ -1,4 +1,6 @@
[milter] [milter]
# the directory with log and data files
datadir = /var/log/milter
# the socket used to communicate with sendmail. Must match sendmail.cf # the socket used to communicate with sendmail. Must match sendmail.cf
socket=/var/run/milter/pythonsock socket=/var/run/milter/pythonsock
# where to save original copies of defanged and failed messages # where to save original copies of defanged and failed messages
@@ -25,7 +27,10 @@ internal_connect = 192.168.0.0/16,127.*
;trusted_relay = 1.2.3.4, 66.12.34.56 ;trusted_relay = 1.2.3.4, 66.12.34.56
# Relaying to these domains is allowed from internal connections only. # Relaying to these domains is allowed from internal connections only.
;private_relay = mycorp.com # You might want to restrict aol.com, for instance, so that stupid
# users don't forward their spam to aol for filtering and get your MTA
# blacklisted by aol.
;private_relay = aol.com, yahoo.com
# Reject external senders with hello names no legit external sender would use. # Reject external senders with hello names no legit external sender would use.
# SPF will do this also, but listing your own domain and mailserver here # SPF will do this also, but listing your own domain and mailserver here
@@ -57,7 +62,7 @@ porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam, p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra, v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra,
x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin, x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin,
valium, rolex, sexual, fuck, adv1t valium, rolex, sexual, fuck, adv1t, vgaira, medz, acai berry
# reject mail with these case sensitive strings in the subject # reject mail with these case sensitive strings in the subject
spam_words = $$$, !!!, XXX, FREE, HGH spam_words = $$$, !!!, XXX, FREE, HGH
# attachments with these extensions will be replaced with a warning # attachments with these extensions will be replaced with a warning
@@ -186,6 +191,11 @@ blind = 1
# Map email addresses and aliases to dspam users # Map email addresses and aliases to dspam users
;dspam_users=david,goliath,spam,falsepositive ;dspam_users=david,goliath,spam,falsepositive
# List dspam users which train on all delivered messages, as opposed to
# "train on error" which trains only when a spam or falsepositive is reported.
# Training mode will build the dictionary faster, but requires close attention
# so as not to miss any spam or false positives.
;dspam_train=goliath
;david=david@foocorp.com,david.yelnetz@foocorp.com,david@bar.foocorp.com ;david=david@foocorp.com,david.yelnetz@foocorp.com,david@bar.foocorp.com
;goliath=giant@foocorp.com,goliath.philistine@foocorp.com ;goliath=giant@foocorp.com,goliath.philistine@foocorp.com
# address to forward spam to. milter will process these and not deliver # address to forward spam to. milter will process these and not deliver
@@ -211,7 +221,15 @@ blind = 1
# Use a dedicated GOSSiP server. If not specified, a local database # Use a dedicated GOSSiP server. If not specified, a local database
# will be used. # will be used.
;server=host:11900 ;server=host:11900
# To include peers of a peer in reputation, set ttl=2
;ttl=1
# If a local database is used, also consult these GOSSiP servers about # If a local database is used, also consult these GOSSiP servers about
# domains. Peer reputation is also tracked as to how often they # domains. Peer reputation is also tracked as to how often they
# agree with us, and weighted accordingly. # agree with us, and weighted accordingly.
;peers=host1:port,host2 ;peers=host1:port,host2
[greylist]
dbfile=greylist.db
grey_time=5 # mins (Google retries in 5 mins)
grey_expire=6 # hours (some legit sites don't retry for 6 hours)
grey_retain=36 # days (keep "first monday" type mailings on file)
+1 -1
View File
@@ -46,7 +46,7 @@ start() {
stop() { stop() {
# Stop daemons. # Stop daemons.
echo -n "Shutting down $prog: " echo -n "Shutting down $prog: "
killproc milter killproc -d 9 milter
RETVAL=$? RETVAL=$?
echo echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter
+67 -56
View File
@@ -1,5 +1,10 @@
%define version 0.8.9 # This spec file contains 2 noarch packages in addition to the pymilter
%define release 1 # module. To compile all three on 32-bit Intel, use:
# rpmbuild -ba --target=i386,noarch pymilter.spec
%define __python python2.4
%define version 0.8.11
%define release 1%{?dist}.py24
# what version of RH are we building for? # what version of RH are we building for?
%define redhat7 0 %define redhat7 0
@@ -17,22 +22,12 @@
%define sysvinit milter.rc %define sysvinit milter.rc
%endif %endif
# RH9, other systems (single ps line per process) # RH9, other systems (single ps line per process)
%ifos Linux # whether to use system default python?
%define python python
#define python python2.4
%else
%define python python
%endif
%ifos aix4.1 %ifos aix4.1
%define libdir /var/log/milter %define libdir /var/log/milter
%else %else
%define libdir /usr/lib/pymilter %define libdir %{_libdir}/pymilter
%endif %endif
# This spec file contains 2 noarch packages in addition to the pymilter
# module. To compile all three, use:
# rpmbuild -ba --target=i386,noarch pymilter.spec
%ifarch noarch %ifarch noarch
Name: milter Name: milter
Group: Applications/System Group: Applications/System
@@ -44,31 +39,31 @@ Source: pymilter-%{version}.tar.gz
License: GPL License: GPL
Group: Development/Libraries Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot BuildRoot: %{_tmppath}/%{name}-buildroot
Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com> Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, pyspf >= 2.0.4, pymilter Requires: %{__python} >= 2.4, pyspf >= 2.0.4, pymilter
%ifos Linux %ifos Linux
Requires: chkconfig Requires: chkconfig
%endif %endif
%description -n milter %description -n milter
A complex but effective spam filtering, SPF checking, and reputation tracking A complex but effective spam filtering, SPF checking, greylisting,
mail application. It uses pydspam if installed for bayesian filtering. and reputation tracking mail application. It uses pydspam if installed for
bayesian filtering.
%package spf %package spf
Group: Applications/System Group: Applications/System
Summary: BMS spam and reputation milter Summary: BMS spam and reputation milter
Requires: pyspf >= 2.0.4, pymilter Requires: pyspf >= 2.0.4, pymilter
Obsoletes: pymilter-spf Obsoletes: pymilter-spf < 0.8.10
%description spf %description spf
A simple mail filter to add Received-SPF headers and reject forged mail. A simple mail filter to add Received-SPF headers and reject forged mail.
Rejection policy is configured via sendmail access file. Rejection policy is configured via sendmail access file and can be
tailored by domain.
%prep %prep
%setup -n pymilter-%{version} %setup -q -n pymilter-%{version}
#patch -p0 -b .bms #patch -p0 -b .bms
%install %install
@@ -78,7 +73,7 @@ mkdir -p $RPM_BUILD_ROOT/etc/mail
mkdir $RPM_BUILD_ROOT/var/log/milter/save mkdir $RPM_BUILD_ROOT/var/log/milter/save
mkdir -p $RPM_BUILD_ROOT%{libdir} mkdir -p $RPM_BUILD_ROOT%{libdir}
cp *.txt $RPM_BUILD_ROOT/var/log/milter cp *.txt $RPM_BUILD_ROOT/var/log/milter
cp bms.py spfmilter.py $RPM_BUILD_ROOT%{libdir} cp -p bms.py spfmilter.py ban2zone.py $RPM_BUILD_ROOT%{libdir}
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
cp spfmilter.cfg $RPM_BUILD_ROOT/etc/mail cp spfmilter.cfg $RPM_BUILD_ROOT/etc/mail
@@ -90,7 +85,7 @@ cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF'
compress compress
} }
/var/log/milter/banned_ips { /var/log/milter/banned_ips {
rotate 3 rotate 7
daily daily
copytruncate copytruncate
} }
@@ -112,23 +107,14 @@ find /var/log/milter/save -mtime +7 | xargs $R rm
EOF EOF
chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
%ifos aix4.1 %ifnos aix4.1
cat >$RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
#!/bin/sh
cd /var/log/milter
# uncomment to enable sgmlop if installed
#export PYTHONPATH=/usr/local/lib/python2.1/site-packages
exec /usr/local/bin/python bms.py >>milter.log 2>&1
EOF
%else # not aix4.1
cp start.sh $RPM_BUILD_ROOT%{libdir}
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
cp spfmilter.rc $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter cp spfmilter.rc $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF' ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
/^python=/ /^python=/
c c
python="%{python}" python="%{__python}"
. .
w w
q q
@@ -136,23 +122,13 @@ EOF
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter <<'EOF' ed $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter <<'EOF'
/^python=/ /^python=/
c c
python="%{python}" python="%{__python}"
.
w
q
EOF
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^python=/
c
python="%{python}"
. .
w w
q q
EOF EOF
%endif # aix4.1 %endif # aix4.1
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
mkdir -p $RPM_BUILD_ROOT/var/run/milter
mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
@@ -185,10 +161,8 @@ fi
%files %files
%defattr(-,root,root) %defattr(-,root,root)
%config %{libdir}/start.sh
/etc/logrotate.d/milter /etc/logrotate.d/milter
/etc/cron.daily/milter /etc/cron.daily/milter
%{libdir}/bms.py?
%ifos aix4.1 %ifos aix4.1
%defattr(-,smmsp,mail) %defattr(-,smmsp,mail)
%else %else
@@ -198,12 +172,14 @@ fi
%dir /var/log/milter %dir /var/log/milter
%dir /var/log/milter/save %dir /var/log/milter/save
%config %{libdir}/bms.py %config %{libdir}/bms.py
%config %{libdir}/ban2zone.py
%config(noreplace) /var/log/milter/strike3.txt %config(noreplace) /var/log/milter/strike3.txt
%config(noreplace) /var/log/milter/softfail.txt %config(noreplace) /var/log/milter/softfail.txt
%config(noreplace) /var/log/milter/fail.txt %config(noreplace) /var/log/milter/fail.txt
%config(noreplace) /var/log/milter/neutral.txt %config(noreplace) /var/log/milter/neutral.txt
%config(noreplace) /var/log/milter/quarantine.txt %config(noreplace) /var/log/milter/quarantine.txt
%config(noreplace) /var/log/milter/permerror.txt %config(noreplace) /var/log/milter/permerror.txt
%config(noreplace) /var/log/milter/temperror.txt
%config(noreplace) /etc/mail/pymilter.cfg %config(noreplace) /etc/mail/pymilter.cfg
/usr/share/sendmail-cf/hack/rhsbl.m4 /usr/share/sendmail-cf/hack/rhsbl.m4
@@ -226,12 +202,10 @@ Source: %{name}-%{version}.tar.gz
License: GPL License: GPL
Group: Development/Libraries Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot BuildRoot: %{_tmppath}/%{name}-buildroot
Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com> Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, sendmail >= 8.13 Requires: %{__python} >= 2.4, sendmail >= 8.13
BuildRequires: %{python}-devel >= 2.4, sendmail-devel >= 8.13 BuildRequires: %{__python}-devel >= 2.4, sendmail-devel >= 8.13
%description %description
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
@@ -240,7 +214,7 @@ modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV. DSNs, and doing CBV.
%prep %prep
%setup %setup -q
#patch -p0 -b .bms #patch -p0 -b .bms
%build %build
@@ -249,18 +223,42 @@ DSNs, and doing CBV.
%else # Redhat builds debug packages after 7.3 %else # Redhat builds debug packages after 7.3
LDFLAGS="-g" LDFLAGS="-g"
%endif %endif
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{__python} setup.py build
%install %install
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES %{__python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
mkdir -p $RPM_BUILD_ROOT/var/run/milter
mkdir -p $RPM_BUILD_ROOT%{libdir}
%ifos aix4.1
cat >$RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
#!/bin/sh
cd /var/log/milter
exec /usr/local/bin/python bms.py >>milter.log 2>&1
EOF
%else # not aix4.1
cp start.sh $RPM_BUILD_ROOT%{libdir}
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^python=/
c
python="%{__python}"
.
w
q
EOF
%endif
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
%if !%{redhat7} %if !%{redhat7}
grep '.pyc$' INSTALLED_FILES | sed -e 's/c$/o/' >>INSTALLED_FILES #grep '.pyc$' INSTALLED_FILES | sed -e 's/c$/o/' >>INSTALLED_FILES
%endif %endif
# start.sh is used by spfmilter and milter, and could be used by
# other milters running on redhat
%files -f INSTALLED_FILES %files -f INSTALLED_FILES
%defattr(-,root,root) %defattr(-,root,root)
%doc README HOWTO ChangeLog NEWS TODO CREDITS sample.py milter-template.py %doc README HOWTO ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%config %{libdir}/start.sh
%dir %attr(0755,mail,mail) /var/run/milter
%endif # noarch %endif # noarch
@@ -268,8 +266,20 @@ grep '.pyc$' INSTALLED_FILES | sed -e 's/c$/o/' >>INSTALLED_FILES
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%changelog %changelog
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
- /var/run/milter directory must be owned by mail
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
- log rcpt for SRS rejections
- improved parsing into email and fullname (still 2 self test failures)
- implement no-DSN CBV, reduce full DSNs
- check for porn words in MAIL FROM fullname
- ban IP for too many bad MAIL FROMs or RCPT TOs
- temperror policy in access
- no CBV for whitelisted MAIL FROM except permerror, softfail
- Allow explicitly whitelisted email from banned_users.
- configure gossip TTL
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1 * Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
- Use %ifarch hack to build milter and milter-spf packages as noarch - Use ifarch hack to build milter and milter-spf packages as noarch
- Remove spf dependency from dsn.py, add dns.py - Remove spf dependency from dsn.py, add dns.py
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1 * Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
- move AddrCache, parse_addr, iniplist to Milter package - move AddrCache, parse_addr, iniplist to Milter package
@@ -286,6 +296,7 @@ rm -rf $RPM_BUILD_ROOT
- SPF moved to pyspf RPM - SPF moved to pyspf RPM
- wiretap archive option - wiretap archive option
- Do plain CBV if missing template - Do plain CBV if missing template
- SMTP AUTH policy in access
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2 * Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
- Support CBV timeout - Support CBV timeout
- Support fail template, headers in templates - Support fail template, headers in templates
-13
View File
@@ -22,19 +22,6 @@ their quarantined mail and may notice your message. If your message is
important, please contact them via other means. You may also try sending important, please contact them via other means. You may also try sending
them a simple plain text message. them a simple plain text message.
If you never sent the above message, then your domain, %(sender_domain)s,
was forged - i.e. used without your knowlege or authorization by
someone attempting to steal your mail identity. This is a very
serious problem, and you need to provide authentication for your
SMTP (email) servers to prevent criminals from forging your
domain. The simplest step is usually to publish an SPF record
with your Sender Policy.
For more information, see: http://www.openspf.org
Your mail admin needs to publish a strict SPF record so that I can reject
those forgeries instead of bugging you with them.
If you need further assistance, please do not hesitate to contact me. If you need further assistance, please do not hesitate to contact me.
Kind regards, Kind regards,
+1 -1
View File
@@ -16,7 +16,7 @@ if sys.version < '2.2.3':
DistributionMetadata.download_url = None DistributionMetadata.download_url = None
# NOTE: importing Milter to obtain version fails when milter.so not built # NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "pymilter", version = '0.8.9', setup(name = "pymilter", version = '0.8.11',
description="Python interface to sendmail milter API", description="Python interface to sendmail milter API",
long_description="""\ long_description="""\
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
+33
View File
@@ -0,0 +1,33 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: Critical DNS configuration error
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your DNS server is not responding to TXT queries. In other words,
it is BROKEN. You need to get somebody to fix it ASAP. We
are attempting to do TXT queries to see if you have an SPF record.
See http://openspf.org
We are sending you this message to alert you to the fact that
you have problems with your DNS.
If you need further assistance, please do not hesitate to
contact me again.
Kind regards,
postmaster@%(receiver)s
+1 -1
View File
@@ -44,7 +44,7 @@ class TestMilter(bms.bmsMilter):
self._msg[field] = value self._msg[field] = value
self.headerschanged = True self.headerschanged = True
def addheader(self,field,value): def addheader(self,field,value,idx=-1):
if not self._body: if not self._body:
raise IOError,"addheader not called from eom()" raise IOError,"addheader not called from eom()"
self.log('addheader: %s=%s' % (field,value)) self.log('addheader: %s=%s' % (field,value))