diff --git a/HOWTO b/HOWTO
new file mode 100644
index 0000000..797c0c1
--- /dev/null
+++ b/HOWTO
@@ -0,0 +1,122 @@
+ Step one. Which DSPAM is right for you?
+
+The DSPAM project makes dspam part of the LDA (Local Delivery Agent).
+Pydspam puts dspam into the MTA (Mail Transfer Agent - sendmail with pymilter).
+
+The advantage of doing dspam in the LDA is that any aliasing has already been
+resolved. You need only configure mailboxes.
+
+The advantage of doing dspam in the MTA is it can screen an entire
+company as a gateway with multiple domains. Unfortunately, this
+means you have to tell it about all the aliases that comprise each
+account. (Also, pydspam is still uses dspam-2.6.5.2 - the Dspam API
+has changed for newer versions.)
+
+If the LDA is right for you, you'll want to use the official Dspam
+package. http://www.nuclearelephant.com/projects/dspam/
+
+If the MTA approach is what you want, then pydspam is what you want.
+
+In either case, you will still want pymilter to block forgeries, Windows
+executables, etc.
+
+So, lets assume you want to install pymilter, and may or may not
+wish to install pydspam.
+
+ Step two. Obtaining RPMS.
+
+For basic pymilter you'll need:
+
+python-2.4
+milter-0.8.2 (the RH9 rpm should work on Fedora Core - let me know)
+sendmail-8.13.x (with milter support enabled)
+
+and for SPF you'll need:
+
+pydns-2.3.0-2.4
+
+and for SRS you'll need:
+
+pysrs-0.30.9-1.py24
+
+I'm pretty sure you will want to have SPF and SRS available.
+
+ Step three. Activate basic milter.
+
+Activate the basic milter by editing /etc/mail/sendmail.mc and adding:
+
+INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
+
+You can then "make sendmail.cf" and restart sendmail.
+
+Tail /var/log/milter/milter.log while SMTP clients connect to your
+sendmail instance. This should show you what the milter is doing.
+
+By default, milter-0.8.2 rejects on SPF fail, except for listed domains
+(that are known to be broken). Some admins don't like that, and 0.8.3 will use
+the /etc/mail/access database to configure SPF responses. For now,
+if you don't like SPF, you can disable spf by replacing "import spf"
+with "spf = None" around line 285 in /var/log/milter/bms.py.
+
+ Step four. Tweaking the basic config.
+
+Most pymilter configuration is in /etc/mail/pymilter.cfg.
+
+By default, milter scans attachments for executable extensions. You can
+turn this off by setting banned_exts to the empty list. There are options
+to scan ZIP attachments and rfc822 attachments. When it finds a banned
+file type, milter saves the original message in /var/log/milter/save,
+and replaces the attachment with a plain text warning message.
+
+Configure hello_blacklist with your own helo name and domains - which
+you know cannot legitimately be used by external MTAs.
+
+Configure trusted_relay with your secondary MX servers, if any. These
+should also run pymilter with similar policies. (But this isn't
+needed for initial testing.)
+
+Configure internal_connect with subnets of your internal SMTP clients.
+Internal connections skip SPF testing and other policies.
+
+Configure internal_domains with domains used by your internal SMTP clients.
+If they attempt to use any other domain, the attempt is blocked and the
+client is logged as a "zombie". Conversely, any attempt by an external
+MTA to use one of your internal domains is treated as a forgery and
+blocked (a simplified form of local SPF).
+
+Adjust porn_words and spam_words - these block emails with a Subject
+containing the listed strings. They can be empty to disable Subject
+string blocking.
+
+ Advanced SPF configuration.
+
+The sendmail access file, or another readonly database with that
+format, can be used for detail spf policy. SPF access policy
+record are tagged with "SPF-{Result}:". Results are
+Pass, Neutral, Softfail, Fail, TempError, PermError. Currently supported
+policy keywords are OK, CBV, REJECT, TEMPFAIL, ERROR:"550 description".
+
+The default policies are as follows:
+
+SPF-Fail: REJECT
+SPF-Softfail: CBV
+SPF-Neutral: OK
+SPF-PermError: REJECT
+SPF-TempError: TEMPFAIL
+SPF-Pass: OK
+
+The tag may be followed by a specific domain. For instance, to
+require a Pass from aol.com:
+
+SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
+SPF-Softfail:aol.com ERROR:"550 AOL mail must get SPF PASS"
+
+ To be continued.
+
+Forthcoming topics:
+
+SRS config
+
+
+pydspam config
+wiretap config
diff --git a/TODO b/TODO
index 27e7b32..a63cf7d 100644
--- a/TODO
+++ b/TODO
@@ -1,7 +1,17 @@
+Find rfc2822 policy for MFROM quoting.
+
+Use /etc/mail/access for domain specific SPF policies.
+
+SPF-Fail: REJECT
+SPF-Softfail: OK
+SPF-Neutral: OK
+SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
+
Defer TEMPERROR in SPF evaluation - give precedence to security
(only defer for PASS mechanisms).
Option to add Received-SPF header, but never reject on SPF.
+ I think the above will handle this.
Create null config that does nothing - except maybe add Received-SPF
headers. Many admins would like to turn features on one at a time.
diff --git a/bms.py b/bms.py
index 551f63b..d3f1fa5 100644
--- a/bms.py
+++ b/bms.py
@@ -1,6 +1,9 @@
#!/usr/bin/env python
# A simple milter that has grown quite a bit.
# $Log$
+# Revision 1.25 2005/09/08 03:55:08 customdesigned
+# Handle perverse MFROM quoting.
+#
# Revision 1.24 2005/08/18 03:36:54 customdesigned
# Don't innoculate with SCREENED mail.
#
@@ -312,6 +315,7 @@ scan_rfc822 = True
internal_connect = ()
trusted_relay = ()
internal_domains = ()
+banned_users = ()
hello_blacklist = ()
smart_alias = {}
dspam_dict = None
@@ -332,7 +336,6 @@ spf_accept_softfail = ()
spf_accept_fail = ()
spf_best_guess = False
spf_reject_noptr = False
-multiple_bounce_recipients = True
time_format = '%Y%b%d %H:%M:%S %Z'
timeout = 600
cbv_cache = {}
@@ -496,7 +499,7 @@ def read_config(list):
if srs_config: cp.read([srs_config])
srs_secret = cp.getdefault('srs','secret')
if SRS and srs_secret:
- global ses,srs,srs_reject_spoofed,srs_domain
+ global ses,srs,srs_reject_spoofed,srs_domain,banned_users
database = cp.getdefault('srs','database')
srs_reject_spoofed = cp.getboolean('srs','reject_spoofed')
maxage = cp.getint('srs','maxage')
@@ -515,6 +518,7 @@ def read_config(list):
else:
srs_domain = []
srs_domain.append(cp.getdefault('srs','fwdomain'))
+ banned_users = cp.getlist('srs','banned_users')
#print srs_domain
def parse_addr(t):
@@ -537,6 +541,8 @@ def parse_addr(t):
return t.split('@')
def parse_header(val):
+ """Decode headers gratuitously encoded to hide the content.
+ """
try:
h = decode_header(val)
if not len(h) or (not h[0][1] and len(h) == 1): return val
@@ -689,18 +695,37 @@ class bmsMilter(Milter.Milter):
t = parse_addr(f)
if len(t) == 2: t[1] = t[1].lower()
self.canon_from = '@'.join(t)
+ # Some braindead MTAs can't be relied upon to properly flag DSNs.
+ # This heuristic tries to recognize such.
+ self.is_bounce = (f == '<>' or t[0].lower() in banned_users
+ #and t[1] == self.hello_name
+ )
# Check SMTP AUTH, also available:
+ # auth_authen authenticated user
# auth_author (ESMTP AUTH= param)
# auth_ssf (connection security, 0 = unencrypted)
# auth_type (authentication method, CRAM-MD5, DIGEST-MD5, PLAIN, etc)
+ # cipher_bits SSL encryption strength
+ # cert_subject SSL cert subject
+ # verify SSL cert verified
+
self.user = self.getsymval('{auth_authen}')
if self.user:
- # any successful authentication is considered INTERNAL
+ # Very simple SMTP AUTH policy by defaul:
+ # any successful authentication is considered INTERNAL
+ # FIXME: configure allowed MAIL FROM by user
self.internal_connection = True
- self.log("SMTP AUTH:",self.user,
- self.getsymval('{auth_type}'),'ssf =',self.getsymval('{auth_ssf}'),
- "INTERNAL")
+ self.log(
+ "SMTP AUTH:",self.user, self.getsymval('{auth_type}'),
+ "sslbits =",self.getsymval('{cipher_bits}'),
+ "ssf =",self.getsymval('{auth_ssf}'), "INTERNAL"
+ )
+ if self.getsymval('{verify}'):
+ self.log("SSL AUTH:",
+ self.getsymval('{cert_subject}'),
+ "verify =",self.getsymval('{verify}')
+ )
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
if len(t) == 2:
@@ -859,34 +884,31 @@ class bmsMilter(Milter.Milter):
t = parse_addr(to.lower())
if len(t) == 2:
user,domain = t
- if self.mailfrom == '<>' or self.canon_from.startswith('postmaster@') \
- or self.canon_from.startswith('mailer-daemon@'):
- if self.recipients and not multiple_bounce_recipients:
- self.data_allowed = False
- if srs and domain in srs_domain:
- oldaddr = '@'.join(parse_addr(to))
- try:
- if ses:
- newaddr = ses.verify(oldaddr)
- else:
- newaddr = oldaddr,
- if len(newaddr) > 1:
- self.log("ses rcpt:",newaddr[0])
- else:
- newaddr = srs.reverse(oldaddr)
- # Currently, a sendmail map reverses SRS. We just log it here.
- self.log("srs rcpt:",newaddr)
- except:
- if not (self.internal_connection or self.trusted_relay):
- if srsre.match(oldaddr):
- self.log("REJECT: srs spoofed:",oldaddr)
- self.setreply('550','5.7.1','Invalid SRS signature')
- return Milter.REJECT
- if oldaddr.startswith('SES='):
- self.log("REJECT: ses spoofed:",oldaddr)
- self.setreply('550','5.7.1','Invalid SES signature')
- return Milter.REJECT
- self.data_allowed = not srs_reject_spoofed
+ if self.is_bounce and srs and domain in srs_domain:
+ oldaddr = '@'.join(parse_addr(to))
+ try:
+ if ses:
+ newaddr = ses.verify(oldaddr)
+ else:
+ newaddr = oldaddr,
+ if len(newaddr) > 1:
+ self.log("ses rcpt:",newaddr[0])
+ else:
+ newaddr = srs.reverse(oldaddr)
+ # Currently, a sendmail map reverses SRS. We just log it here.
+ self.log("srs rcpt:",newaddr)
+ except:
+ if not (self.internal_connection or self.trusted_relay):
+ if srsre.match(oldaddr):
+ self.log("REJECT: srs spoofed:",oldaddr)
+ self.setreply('550','5.7.1','Invalid SRS signature')
+ return Milter.REJECT
+ if oldaddr.startswith('SES='):
+ self.log("REJECT: ses spoofed:",oldaddr)
+ self.setreply('550','5.7.1','Invalid SES signature')
+ return Milter.REJECT
+ self.data_allowed = not srs_reject_spoofed
+
# non DSN mail to SRS address will bounce due to invalid local part
self.recipients.append('@'.join(t))
users = check_user.get(domain)
@@ -964,15 +986,20 @@ class bmsMilter(Milter.Milter):
return Milter.CONTINUE
def forged_bounce(self):
- if len(self.recipients) > 1:
- self.log('REJECT: Multiple bounce recipients')
- self.setreply('550','5.7.1','Multiple bounce recipients')
+ if self.mailfrom != '<>':
+ self.log("REJECT: bogus DSN")
+ self.setreply('550','5.7.1',
+ "I do not accept mail from postmaster, mailer-daemon, or clamav.",
+ "All such mail has turned out to be Delivery Status Notifications",
+ "which failed to be marked as such. Please send a real DSN if",
+ "you need to. Use another MAIL FROM if you need to send me mail."
+ )
else:
self.log('REJECT: bounce with no SRS encoding')
self.setreply('550','5.7.1',
- "I did not send you that message. Please consider implementing SPF",
- "(http://spf.pobox.com) to avoid bouncing mail to spoofed senders.",
- "Thank you."
+ "I did not send you that message. Please consider implementing SPF",
+ "(http://openspf.com) to avoid bouncing mail to spoofed senders.",
+ "Thank you."
)
return Milter.REJECT
@@ -1018,18 +1045,24 @@ class bmsMilter(Milter.Milter):
self.fp.write("\n") # terminate headers
self.fp.seek(0)
# log when neither sender nor from domains matches mail from domain
- mf_domain = self.canon_from.split('@')[-1]
- msg = rfc822.Message(self.fp)
- for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
- t = parse_addr(hf)
- if len(t) == 2 and t[1].lower() == mf_domain:
- break
- else:
- self.log("NOTE: MFROM domain doesn't match From or Sender");
- for f in msg.getallmatchingheaders('from') \
- + msg.getallmatchingheaders('sender'):
- self.log(f)
- del msg
+ if self.mailfrom != '<>':
+ mf_domain = self.canon_from.split('@')[-1]
+ msg = rfc822.Message(self.fp)
+ for rn,hf in msg.getaddrlist('from')+msg.getaddrlist('sender'):
+ t = parse_addr(hf)
+ if len(t) == 2 and t[1].lower() == mf_domain:
+ break
+ else:
+ for f in msg.getallmatchingheaders('from'):
+ self.log(f)
+ sender = msg.getallmatchingheaders('sender')
+ if sender:
+ for f in sender:
+ self.log(f)
+ else:
+ self.log("NOTE: Supplying MFROM as Sender");
+ self.add_header('Sender',self.mailfrom)
+ del msg
# copy headers to a temp file for scanning the body
self.fp.seek(0)
headers = self.fp.getvalue()
diff --git a/milter.cfg b/milter.cfg
index 11afe08..656ff27 100644
--- a/milter.cfg
+++ b/milter.cfg
@@ -70,6 +70,10 @@ config=/etc/mail/pysrs.cfg
;fwdomain = mydomain.com
# turn this on after a grace period to reject spoofed DSNs
reject_spoofed = 0
+# Many braindead MTAs send DSNs with a non-DSN MFROM (e.g. to report that
+# some virus claiming to be sent by you). This heuristic
+# refuses mail from user names commonly abused in that way.
+;banned_users = postmaster, mailer-daemon, clamav
# See http://spf.pobox.com for more info on SPF.
[spf]
@@ -88,6 +92,8 @@ reject_spoofed = 0
# treat fail from these domains like softfail: because their SPF record
# or an important sender is screwed up. Must have valid HELO, however.
;accept_fail = custhelp.com
+# use sendmail access file or similar format for detailed spf policy
+;access_file = /etc/mail/access.db
# features intended to clean up outgoing mail
[scrub]
diff --git a/milter.html b/milter.html
index f553061..5e9f732 100644
--- a/milter.html
+++ b/milter.html
@@ -24,7 +24,7 @@ ALT="Viewable With Any Browser" BORDER="0">
Stuart D. Gathman
This web page is written by Stuart D. Gathman
and
sponsored by
Business Management Systems, Inc.
-Last updated Jul 20, 2005
+Last updated Aug 28, 2005
See the FAQ | Download now |
Subscribe to mailing list |
@@ -51,12 +51,13 @@ Python milter is being moved to
pymilter Sourceforge
project for development and release downloads.
-Release 0.8.2 has changes to SPF to bring it in line with the newly -official RFC. It adds SES support (the original SES without body hash) -for pysrs-0.30.10, and honeypot support for pydspam-1.1.9. There is -a new method in the base milter module. milter.set_exception_policy(i) -lets you choose a policy of CONTINUE, REJECT, or TEMPFAIL (default) for -untrapped exceptions encountered in a milter callback. +Release 0.8.2 has changes to SPF to bring it +in line with the newly official RFC. It adds +SES +support (the original SES without body hash) for pysrs-0.30.10, and honeypot +support for pydspam-1.1.9. There is a new method in the base milter module. +milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or +TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
Release 0.8.0 is the first Sourceforge release. It supports Python-2.4, and provides an option to accept mail