Compare commits

..

62 Commits

Author SHA1 Message Date
cvs2svn e66a56154d This commit was manufactured by cvs2svn to create tag 'pymilter-0_8_9'.
Sprout from master 2007-09-25 17:07:32 UTC Stuart Gathman <stuart@gathman.org> 'Test on Centos5'
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
2007-09-25 17:07:33 +00:00
Stuart Gathman 670e97cb79 Test on Centos5 2007-09-25 17:07:32 +00:00
Stuart Gathman 6397b7027f Tested on RH7 2007-09-25 16:37:26 +00:00
Stuart Gathman 94ce032559 Update license. 2007-09-25 02:26:29 +00:00
Stuart Gathman 91230381cb Test dns.py 2007-09-25 02:15:35 +00:00
Stuart Gathman 46ed3ddbcb Allow arbitrary object, not just spf.query like, to provide data for create_msg 2007-09-25 01:24:59 +00:00
Stuart Gathman 6048fe6e8c Remove explicit spf dependency. 2007-09-24 20:13:26 +00:00
Stuart Gathman d225384829 Create milter and milter-spf as noarch packages. 2007-09-24 18:00:58 +00:00
Stuart Gathman a84f6aa574 Specify library_dirs for Debian. 2007-09-24 17:44:51 +00:00
Stuart Gathman 344e8f0a0a Report domain on reputation reject. 2007-09-13 14:51:03 +00:00
Stuart Gathman 1fa4b72c84 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.
2007-09-03 16:18:45 +00:00
Stuart Gathman 021ea96748 Fixes from test on EL5. 2007-07-25 19:04:44 +00:00
Stuart Gathman a490e79564 Build on EL5 2007-07-25 17:43:34 +00:00
Stuart Gathman 33e8f7c4cc Multi-package build fixes. 2007-07-25 17:30:30 +00:00
Stuart Gathman 6bbb6b3f02 Move milter apps to /usr/lib/pymilter 2007-07-25 17:14:59 +00:00
Stuart Gathman 6577e40bfb Build pymilter as separate package. 2007-07-25 15:32:09 +00:00
Stuart Gathman 04eeeab2e1 Clarify docs. 2007-07-25 15:20:41 +00:00
Stuart Gathman cdfeb2d792 Ban ips on bad mailfrom offenses as well as bad rcpts. 2007-07-02 03:06:10 +00:00
Stuart Gathman 46545cab94 Fix missed comcast dynip. 2007-06-28 20:33:25 +00:00
Stuart Gathman 9a8fdcb120 Ban IPs based on too many invalid recipients in a connection. Requires
configuring check_user.  Tighten HELO best_guess policy.
2007-06-23 20:53:05 +00:00
Stuart Gathman 218f5168bc Do not process valid SRS recipients as delayed_failure. 2007-04-19 16:02:43 +00:00
Stuart Gathman ddbb8ac3ea Ban ips with too many bad rcpts on a connection. 2007-04-15 01:01:13 +00:00
Stuart Gathman a2215124bb Ban ips with too many bad rcpts on a connection. 2007-04-15 00:54:30 +00:00
Stuart Gathman e505d2bb28 Check access_file at startup. Compress rcpt to log. 2007-04-13 17:20:09 +00:00
Stuart Gathman 9f40f265cd Stop querying gossip server twice. 2007-04-05 17:59:07 +00:00
Stuart Gathman 20a875b84d Don't disable gossip for temporary error. 2007-04-02 18:37:25 +00:00
Stuart Gathman 1da5ca54b5 Report bestguess and helo-spf as key-value pairs in Received-SPF
instead of in their own headers.
2007-03-30 18:13:41 +00:00
Stuart Gathman bac593f05d Don't count DSN and unqualified MAIL FROM as internal_domain. 2007-03-29 03:06:10 +00:00
Stuart Gathman dbba488d58 Do not CBV for internal domains. 2007-03-24 00:30:24 +00:00
Stuart Gathman 6936b599fe Get SMTP-Auth policy from access_file. 2007-03-23 22:39:10 +00:00
Stuart Gathman cee38f8149 Properly log From: and Sender: 2007-03-21 04:02:13 +00:00
Stuart Gathman 188e8256f3 Gossip configuration options: client or standalone with optional peers. 2007-03-18 02:32:21 +00:00
Stuart Gathman 4013365a3d New delayed DSN pattern. Retab (expandtab). 2007-03-17 21:22:48 +00:00
Stuart Gathman e571ccc5a5 Fix missing HELO log. 2007-03-13 21:18:28 +00:00
Stuart Gathman f65294b470 Include Received-SPF in permerror DSN. 2007-03-13 18:45:09 +00:00
Stuart Gathman b2d8e838a2 Fix continuing findsrs when srs.reverse fails. 2007-03-03 19:18:57 +00:00
Stuart Gathman f136e973dc Improve delayed failure detection. 2007-03-03 18:46:26 +00:00
Stuart Gathman d289822f42 Handle DNS error sending DSN. 2007-03-03 18:19:40 +00:00
Stuart Gathman 806aa5a6de Updated 2007-03-02 14:18:21 +00:00
Stuart Gathman e84a803cc1 Handle missing HELO. 2007-02-21 22:14:41 +00:00
Stuart Gathman 20612240f3 Use re for auto-reply recognition. 2007-02-07 23:21:26 +00:00
Stuart Gathman c9e6bb68d9 Newbie friendly default for internal_connect 2007-02-07 23:20:28 +00:00
Stuart Gathman 4d69b8fbfe Handle null in header value. 2007-01-26 03:47:23 +00:00
Stuart Gathman 21e3c6f489 Persist blacklisting from delayed DSNs. 2007-01-25 22:47:26 +00:00
Stuart Gathman 83529320ae Add private relay. 2007-01-23 19:46:20 +00:00
Stuart Gathman e5685c6035 Convert tabs to spaces. 2007-01-22 02:46:01 +00:00
Stuart Gathman 4c72135b0e Move parse_header to Milter.utils.
Test case for delayed DSN parsing.
Fix plock when source missing or cannot set owner/group.
2007-01-19 23:31:38 +00:00
Stuart Gathman 393aa6140a Doc update.
Parse From header for delayed failure detection.
Don't check reputation of trusted host.
Track IP reputation only when missing PTR.
2007-01-18 16:48:44 +00:00
Stuart Gathman 2a6a68230b REJECT after data for blacklisted emails - so in case of mistakes, a
legitimate sender will know what happened.
2007-01-16 05:17:29 +00:00
Stuart Gathman 279c831a8e Purge old entries in auto_whitelist and send_dsn logs. 2007-01-11 19:59:40 +00:00
Stuart Gathman c0aa632e16 Negative feedback for bad headers. Purge cache logs on startup. 2007-01-11 04:31:26 +00:00
Stuart Gathman a875ac7834 Documentation updates. 2007-01-10 04:44:25 +00:00
Stuart Gathman 9f8cef5ee2 Get user feedback. 2007-01-08 23:20:54 +00:00
Stuart Gathman 4b0e7b22da Tested on spidey2 2007-01-06 04:32:57 +00:00
Stuart Gathman 40fb05b0e3 Forgot import 2007-01-06 04:25:12 +00:00
Stuart Gathman 8ae7bd4217 Add config file to spfmilter 2007-01-06 04:21:30 +00:00
Stuart Gathman 139e141e1e Make blacklist an AddrCache 2007-01-05 23:33:55 +00:00
Stuart Gathman 8932dc36db Move parse_addr, iniplist, ip4re to Milter.utils 2007-01-05 23:12:13 +00:00
Stuart Gathman bda654b7a0 Added sample spfmilter.py application. 2007-01-05 22:48:48 +00:00
Stuart Gathman 09b671f47b Test AddrCache. 2007-01-05 21:26:03 +00:00
Stuart Gathman 732e7317f1 Move AddrCache to Milter package. 2007-01-05 21:25:40 +00:00
Stuart Gathman 702ec2d4ca Link to pyspf. 2007-01-05 21:24:29 +00:00
37 changed files with 2477 additions and 968 deletions
+12 -13
View File
@@ -1,8 +1,8 @@
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 2, June 1991 Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc. Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. of this license document, but changing it is not allowed.
@@ -15,7 +15,7 @@ software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to the GNU Lesser General Public License instead.) You can apply it to
your programs, too. your programs, too.
When we speak of free software, we are referring to freedom, not When we speak of free software, we are referring to freedom, not
@@ -55,7 +55,7 @@ patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions:
License. (Exception: if the Program itself is interactive but License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on does not normally print such an announcement, your work based on
the Program is not required to print an announcement.) the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program, identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in and can be reasonably considered independent and separate works in
@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not distribution of the source code, even though third parties are not
compelled to copy the source along with the object code. compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program 4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is otherwise to copy, modify, sublicense or distribute the Program is
@@ -225,7 +225,7 @@ impose that choice.
This section is intended to make thoroughly clear what is believed to This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License. be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in 8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License original copyright holder who places the Program under this License
@@ -278,7 +278,7 @@ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES. POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest If you develop a new program, and you want it to be of the greatest
@@ -303,10 +303,9 @@ the "copyright" line and a pointer to where the full notice is found.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License along
along with this program; if not, write to the Free Software with this program; if not, write to the Free Software Foundation, Inc.,
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
@@ -336,5 +335,5 @@ necessary. Here is a sample; alter the names:
This General Public License does not permit incorporating your program into This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. Public License instead of this License.
+2
View File
@@ -7,6 +7,8 @@ real, usable Python extension.
Other contributors (in random order): Other contributors (in random order):
Dwayne Litzenberger, B.A.Sc.
for library_dirs patch to compile on Debian
Dave MacQuigg Dave MacQuigg
for noticing that smfi_insheader wasn't supported, and creating for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter. a template to help first time pymilter users create their own milter.
+28 -10
View File
@@ -1,3 +1,10 @@
On Sun, 11 Feb 2007, Rick Saul wrote:
> Stuart I was planning to move to centos4.4 in a couple of weeks anyway...
> Your advice of where to go from here.
Oh - you are asking for a howto.
Step one. Which DSPAM is right for you? Step one. Which DSPAM is right for you?
The DSPAM project makes dspam part of the LDA (Local Delivery Agent). The DSPAM project makes dspam part of the LDA (Local Delivery Agent).
@@ -28,39 +35,42 @@ 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.2 (the RH9 rpm should work on Fedora Core - let me know) milter-0.8.7
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.0-2.4
pyspf-2.0.3-2.py24
and for SRS you'll need: and for SRS you'll need:
pysrs-0.30.9-1.py24 pysrs-0.30.11-1.py24
I'm pretty sure you will want to have SPF and SRS available. I'm pretty sure you will want to have SPF and SRS available.
Step three. Activate basic milter. Step three. Activate basic milter.
Activate the basic milter by editing /etc/mail/sendmail.mc and adding: Activate the basic milter and pysrs by editing /etc/mail/sendmail.mc and adding:
define(`NO_SRS_FILE',`/etc/mail/no-srs-mailers')dnl
dnl define(`NO_SRS_FROM_LOCAL')dnl
HACK(`pysrs',`/var/run/milter/pysrs')dnl
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m') 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. You can then "make sendmail.cf" and restart sendmail.
Start milter and pysrs with "service milter start", "service pysrs start".
Tail /var/log/milter/milter.log while SMTP clients connect to your 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.2 rejects on SPF fail, except for listed domains By default, milter-0.8.7 rejects on SPF fail.
(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. Step four. Tweaking the basic config.
Most pymilter configuration is in /etc/mail/pymilter.cfg. Most pymilter configuration is in /etc/mail/pymilter.cfg. To activate
changes, "service milter restart".
By default, milter scans attachments for executable extensions. You can By default, milter scans attachments for executable extensions. You can
turn this off by setting banned_exts to the empty list. There are options turn this off by setting banned_exts to the empty list. There are options
@@ -76,7 +86,9 @@ should also run pymilter with similar policies. (But this isn't
needed for initial testing.) needed for initial testing.)
Configure internal_connect with subnets of your internal SMTP clients. Configure internal_connect with subnets of your internal SMTP clients.
Internal connections skip SPF testing and other policies. Internal connections skip SPF testing and other policies. You will
likely need to set this to allow outgoing mail if you have
an SPF policy already.
Configure internal_domains with domains used by your internal SMTP clients. 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 If they attempt to use any other domain, the attempt is blocked and the
@@ -134,3 +146,9 @@ SRS config
pydspam config pydspam config
wiretap config wiretap config
--
Stuart D. Gathman <stuart@bmsi.com>
Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flammis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.
+5 -1
View File
@@ -8,8 +8,8 @@ include ChangeLog
include MANIFEST.in include MANIFEST.in
include testsample.py include testsample.py
include testmime.py include testmime.py
include testutils.py
include testbms.py include testbms.py
include testdspam.py
include rejects.py include rejects.py
include report.py include report.py
include bms.py include bms.py
@@ -18,6 +18,10 @@ include cid2spf.py
include spfquery.py include spfquery.py
include test.py include test.py
include sample.py include sample.py
include milter-template.py
include spfmilter.py
include spfmilter.rc
include spfmilter.cfg
include test/* include test/*
include doc/* include doc/*
include Milter/*.py include Milter/*.py
+1 -1
View File
@@ -9,7 +9,7 @@ import milter
import thread import thread
from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \ from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
set_flags, setdbg, setbacklog, settimeout, \ set_flags, setdbg, setbacklog, settimeout, error, \
ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \ ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \
V1_ACTS, V2_ACTS, CURR_ACTS V1_ACTS, V2_ACTS, CURR_ACTS
+150
View File
@@ -0,0 +1,150 @@
# Email address list with expiration
#
# This class acts like a map. Entries with a value of None are persistent,
# but disappear after a time limit. This is useful for automatic whitelists
# and blacklists with expiration. The persistent store is a simple ascii
# file with sender and timestamp on each line. Entries can be appended
# to the store, and will be picked up the next time it is loaded.
#
# Entries with other values are not persistent. This is used to hold failed
# CBV results.
#
# $Log$
# Revision 1.7 2007/01/25 22:47:26 customdesigned
# Persist blacklisting from delayed DSNs.
#
# Revision 1.6 2007/01/19 23:31:38 customdesigned
# Move parse_header to Milter.utils.
# Test case for delayed DSN parsing.
# Fix plock when source missing or cannot set owner/group.
#
# Revision 1.5 2007/01/11 19:59:40 customdesigned
# Purge old entries in auto_whitelist and send_dsn logs.
#
# Revision 1.4 2007/01/11 04:31:26 customdesigned
# Negative feedback for bad headers. Purge cache logs on startup.
#
# Revision 1.3 2007/01/08 23:20:54 customdesigned
# Get user feedback.
#
# Revision 1.2 2007/01/05 23:33:55 customdesigned
# Make blacklist an AddrCache
#
# Revision 1.1 2007/01/05 21:25:40 customdesigned
# Move AddrCache to Milter package.
#
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
import time
from plock import PLock
class AddrCache(object):
time_format = '%Y%b%d %H:%M:%S %Z'
def __init__(self,renew=7,fname=None):
self.age = renew
self.cache = {}
self.fname = fname
def load(self,fname,age=0):
"Load address cache from persistent store."
if not age:
age = self.age
self.fname = fname
cache = {}
self.cache = cache
now = time.time()
lock = PLock(self.fname)
wfp = lock.lock()
changed = False
try:
too_old = now - age*24*60*60 # max age in days
try:
fp = open(self.fname)
except OSError:
fp = ()
for ln in fp:
try:
rcpt,ts = ln.strip().split(None,1)
try:
l = time.strptime(ts,AddrCache.time_format)
t = time.mktime(l)
if t < too_old:
changed = True
continue
cache[rcpt.lower()] = (t,None)
except: # unparsable timestamp - likely garbage
changed = True
continue
except: # manual entry (no timestamp)
cache[ln.strip().lower()] = (now,None)
wfp.write(ln)
if changed:
lock.commit(self.fname+'.old')
else:
lock.unlock()
except IOError:
lock.unlock()
def has_key(self,sender):
"True if sender is cached and has not expired."
try:
lsender = sender and sender.lower()
ts,res = self.cache[lsender]
too_old = time.time() - self.age*24*60*60 # max age in days
if not ts or ts > too_old:
return True
del self.cache[lsender]
try:
user,host = sender.split('@',1)
return self.has_key(host)
except ValueError:
pass
except KeyError:
try:
user,host = sender.split('@',1)
return self.has_key(host)
except: pass
return False
__contains__ = has_key
def __getitem__(self,sender):
try:
lsender = sender.lower()
ts,res = self.cache[lsender]
too_old = time.time() - self.age*24*60*60 # max age in days
if not ts or ts > too_old:
return res
del self.cache[lsender]
raise KeyError, sender
except KeyError,x:
try:
user,host = sender.split('@',1)
return self.__getitem__(host)
except ValueError:
raise x
def addperm(self,sender,res=None):
"Add a permanent sender."
lsender = sender.lower()
if self.has_key(lsender):
ts,res = self.cache[lsender]
if not ts: return # already permanent
self.cache[lsender] = (None,res)
if not res:
print >>open(self.fname,'a'),sender
def __setitem__(self,sender,res):
lsender = sender.lower()
now = time.time()
self.cache[lsender] = (now,res)
if not res and self.fname:
s = time.strftime(AddrCache.time_format,time.localtime(now))
print >>open(self.fname,'a'),sender,s # log refreshed senders
def __len__(self):
return len(self.cache)
+59
View File
@@ -0,0 +1,59 @@
from ConfigParser import ConfigParser
class MilterConfigParser(ConfigParser):
def __init__(self,defaults={}):
ConfigParser.__init__(self)
self.defaults = defaults
# The defaults provided by ConfigParser show up in all sections,
# which screws up iterating over all options in a section.
# Worse, passing "defaults" with vars= overrides the config file!
# So we roll our own defaults.
def get(self,sect,opt):
if not self.has_option(sect,opt) and opt in self.defaults:
return self.defaults[opt]
return ConfigParser.get(self,sect,opt)
def getlist(self,sect,opt):
if self.has_option(sect,opt):
return [q.strip() for q in self.get(sect,opt).split(',')]
return []
def getaddrset(self,sect,opt):
if not self.has_option(sect,opt):
return {}
s = self.get(sect,opt)
d = {}
for q in s.split(','):
q = q.strip()
if q.startswith('file:'):
domain = q[5:].lower()
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
else:
user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user)
return d
def getaddrdict(self,sect,opt):
if not self.has_option(sect,opt):
return {}
d = {}
for q in self.get(sect,opt).split(','):
q = q.strip()
if self.has_option(sect,q):
l = self.get(sect,q)
for addr in l.split(','):
addr = addr.strip()
if addr.startswith('file:'):
fname = addr[5:]
for a in open(fname,'r').read().split():
d[a] = q
else:
d[addr] = q
return d
def getdefault(self,sect,opt,default=None):
if self.has_option(sect,opt):
return self.get(sect,opt)
return default
+88
View File
@@ -0,0 +1,88 @@
# provide a higher level interface to pydns
import DNS
from DNS import DNSError
MAX_CNAME = 10
def DNSLookup(name, qtype):
try:
req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req()
#resp.show()
# key k: ('wayforward.net', 'A'), value v
# FIXME: pydns returns AAAA RR as 16 byte binary string, but
# A RR as dotted quad. For consistency, this driver should
# return both as binary string.
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
except IOError, x:
raise DNSError, str(x)
class Session(object):
"""A Session object has a simple cache with no TTL that is valid
for a single "session", for example an SMTP conversation."""
def __init__(self):
self.cache = {}
# We have to be careful which additional DNS RRs we cache. For
# instance, PTR records are controlled by the connecting IP, and they
# could poison our local cache with bogus A and MX records.
SAFE2CACHE = {
('MX','A'): None,
('MX','MX'): None,
('CNAME','A'): None,
('CNAME','CNAME'): None,
('A','A'): None,
('AAAA','AAAA'): None,
('PTR','PTR'): None,
('TXT','TXT'): None,
('SPF','SPF'): None
}
def dns(self, name, qtype, cnames=None):
"""DNS query.
If the result is in cache, return that. Otherwise pull the
result from DNS, and cache ALL answers, so additional info
is available for further queries later.
CNAMEs are followed.
If there is no data, [] is returned.
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
post: isinstance(__return__, types.ListType)
"""
result = self.cache.get( (name, qtype) )
cname = None
if not result:
safe2cache = Session.SAFE2CACHE
for k, v in DNSLookup(name, qtype):
if k == (name, 'CNAME'):
cname = v
if (qtype,k[1]) in safe2cache:
self.cache.setdefault(k, []).append(v)
result = self.cache.get( (name, qtype), [])
if not result and cname:
if not cnames:
cnames = {}
elif len(cnames) >= MAX_CNAME:
#return result # if too many == NX_DOMAIN
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname
if cname in cnames:
raise DNSError, 'CNAME loop'
result = self.dns(cname, qtype, cnames=cnames)
return result
DNS.DiscoverNameServers()
if __name__ == '__main__':
import sys
s = Session()
for n,t in zip(*[iter(sys.argv[1:])]*2):
print n,t
print s.dns(n,t)
+54 -23
View File
@@ -5,6 +5,15 @@
# Send DSNs, do call back verification, # Send DSNs, do call back verification,
# and generate DSN messages from a template # and generate DSN messages from a template
# $Log$ # $Log$
# Revision 1.15 2007/09/24 20:13:26 customdesigned
# Remove explicit spf dependency.
#
# Revision 1.14 2007/03/03 18:19:40 customdesigned
# Handle DNS error sending DSN.
#
# Revision 1.13 2007/01/04 18:01:11 customdesigned
# Do plain CBV when template missing.
#
# Revision 1.12 2006/07/26 16:37:35 customdesigned # Revision 1.12 2006/07/26 16:37:35 customdesigned
# Support timeout. # Support timeout.
# #
@@ -16,20 +25,23 @@
# #
import smtplib import smtplib
import spf
import socket import socket
from email.Message import Message from email.Message import Message
import Milter import Milter
import time import time
import dns
def send_dsn(mailfrom,receiver,msg=None,timeout=600): def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
"""Send DSN. If msg is None, do callback verification. """Send DSN. If msg is None, do callback verification.
Mailfrom is original sender we are sending DSN or CBV to. Mailfrom is original sender we are sending DSN or CBV to.
Receiver is the MTA sending the DSN. Receiver is the MTA sending the DSN.
Return None for success or (code,msg) for failure.""" Return None for success or (code,msg) for failure."""
user,domain = mailfrom.split('@') user,domain = mailfrom.split('@')
q = spf.query(None,None,None) if not session: session = dns.Session()
mxlist = q.dns(domain,'MX') try:
mxlist = session.dns(domain,'MX')
except dns.DNSError:
return (450,'DNS Timeout: %s MX'%domain) # temp error
if not mxlist: if not mxlist:
mxlist = (0,domain), # fallback to A record when no MX mxlist = (0,domain), # fallback to A record when no MX
else: else:
@@ -80,23 +92,41 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600):
return (450,'No MX response within %f minutes'%(timeout/60.0)) return (450,'No MX response within %f minutes'%(timeout/60.0))
return (450,'No MX servers available') # temp error return (450,'No MX servers available') # temp error
def create_msg(q,rcptlist,origmsg=None,template=None): class Vars: pass
"Create a DSN message from a template. Template must be '\n' separated."
# NOTE: Caller can pass an object to create_msg that in a typical milter
# collects things like heloname or sender anyway.
def create_msg(v,rcptlist=None,origmsg=None,template=None):
"""Create a DSN message from a template. Template must be '\n' separated.
v - an object whose attributes are used for substitutions. Must
have sender and receiver attributes at a minimum.
rcptlist - used to set v.rcpt if given
origmsg - used to set v.subject and v.spf_result if given
template - a '\n' separated string with python '%(name)s' substitutions.
"""
if not template: if not template:
return None return None
heloname = q.h if hasattr(v,'perm_error'):
sender = q.s # likely to be an spf.query, try translating for backward compatibility
connectip = q.i q = v
receiver = q.r v = Vars()
sender_domain = q.o try:
result = q.result v.heloname = q.h
perm_error = q.perm_error v.sender = q.s
rcpt = '\n\t'.join(rcptlist) v.connectip = q.i
try: subject = origmsg['Subject'] v.receiver = q.r
except: subject = '(none)' v.sender_domain = q.o
try: v.result = q.result
spf_result = origmsg['Received-SPF'] v.perm_error = q.perm_error
except: spf_result = None except: v = q
if rcptlist:
v.rcpt = '\n\t'.join(rcptlist)
if origmsg:
try: v.subject = origmsg['Subject']
except: v.subject = '(none)'
try:
v.spf_result = origmsg['Received-SPF']
except: v.spf_result = None
msg = Message() msg = Message()
@@ -106,18 +136,19 @@ def create_msg(q,rcptlist,origmsg=None,template=None):
hdrs,body = template.split('\n\n',1) hdrs,body = template.split('\n\n',1)
for ln in hdrs.splitlines(): for ln in hdrs.splitlines():
name,val = ln.split(':',1) name,val = ln.split(':',1)
msg.add_header(name,(val % locals()).strip()) msg.add_header(name,(val % v.__dict__).strip())
msg.set_payload(body % locals()) msg.set_payload(body % v.__dict__)
# add headers if missing from old template # add headers if missing from old template
if 'to' not in msg: if 'to' not in msg:
msg.add_header('To',sender) msg.add_header('To',v.sender)
if 'from' not in msg: if 'from' not in msg:
msg.add_header('From','postmaster@%s'%receiver) msg.add_header('From','postmaster@%s'%v.receiver)
if 'auto-submitted' not in msg: if 'auto-submitted' not in msg:
msg.add_header('Auto-Submitted','auto-generated') msg.add_header('Auto-Submitted','auto-generated')
return msg return msg
if __name__ == '__main__': if __name__ == '__main__':
import spf
q = spf.query('192.168.9.50', q = spf.query('192.168.9.50',
'SRS0=pmeHL=RH==stuart@example.com', 'SRS0=pmeHL=RH==stuart@example.com',
'red.example.com',receiver='mail.example.com') 'red.example.com',receiver='mail.example.com')
+3 -1
View File
@@ -44,6 +44,8 @@ def is_dynip(host,addr):
True True
>>> is_dynip('[1.2.3.4]','1.2.3.4') >>> is_dynip('[1.2.3.4]','1.2.3.4')
True True
>>> is_dynip('c-71-63-151-151.hsd1.mn.comcast.net','71.63.151.151')
True
""" """
if host.startswith('[') and host.endswith(']'): if host.startswith('[') and host.endswith(']'):
return True return True
@@ -54,7 +56,7 @@ def is_dynip(host,addr):
h = host h = host
m = ip3.findall(host) m = ip3.findall(host)
if m: if m:
g = map(int,m) g = map(int,m)[:4]
ia3 = (ia[1:],ia[:3]) ia3 = (ia[1:],ia[:3])
if g[-3:] in ia3: return True if g[-3:] in ia3: return True
if g[0] == ia[3] and g[1:3] == ia[:2]: return True if g[0] == ia[3] and g[1:3] == ia[:2]: return True
+66
View File
@@ -0,0 +1,66 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
import os
from time import sleep
class PLock(object):
"A simple /etc/passwd style lock,update,rename protocol for updating files."
def __init__(self,basename):
self.basename = basename
self.fp = None
def lock(self,lockname=None,mode=0660,strict_perms=False):
"Start an update transaction. Return FILE to write new version."
self.unlock()
if not lockname:
lockname = self.basename + '.lock'
self.lockname = lockname
try:
st = os.stat(self.basename)
mode |= st.st_mode
except OSError: pass
u = os.umask(0002)
try:
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
finally:
os.umask(u)
self.fp = os.fdopen(fd,'w')
try:
os.chown(self.lockname,-1,st.st_gid)
except:
if strict_perms:
self.unlock()
raise
return self.fp
def wlock(self,lockname=None):
"Wait until lock is free, then start an update transaction."
while True:
try:
return self.lock(lockname)
except OSError:
sleep(2)
def commit(self,backname=None):
"Commit update transaction with optional backup file."
if not self.fp:
raise IOError,"File not locked"
self.fp.close()
self.fp = None
if backname:
try:
os.remove(backname)
except OSError: pass
os.link(self.basename,backname)
os.rename(self.lockname,self.basename)
def unlock(self):
"Cancel update transaction."
if self.fp:
try:
self.fp.close()
except: pass
self.fp = None
os.remove(self.lockname)
+17
View File
@@ -0,0 +1,17 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2005 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
# The localpart of SMTP return addresses is often signed. The format
# of the signing is application specific and doesn't concern us -
# except that we wish to extract some sort of fixed string from
# the variable signature which represents the "source" of the message.
def unsign(s):
"""Attempt to unsign localpart and return original email.
No attempt is made to verify the signature.
>>> unsign('SRS0=8Y3CZ=3U=jsconnor.com=bills@bmsi.com')
'bills@jsconnor.com'
"""
# not implemented yet
return s
+85
View File
@@ -0,0 +1,85 @@
import re
import struct
import socket
import email.Errors
from fnmatch import fnmatchcase
from email.Header import decode_header
ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$')
# from spf.py
def addr2bin(str):
"Convert a string IPv4 address into an unsigned integer."
return struct.unpack("!L", socket.inet_aton(str))[0]
MASK = 0xFFFFFFFFL
def cidr(i,n):
return ~(MASK >> n) & MASK & i
def iniplist(ipaddr,iplist):
"""Return whether ip is in cidr list
>>> iniplist('66.179.26.146',['127.0.0.1','66.179.26.128/26'])
True
>>> iniplist('127.0.0.1',['127.0.0.1','66.179.26.128/26'])
True
>>> iniplist('192.168.0.45',['192.168.0.*'])
True
"""
ipnum = addr2bin(ipaddr)
for pat in iplist:
p = pat.split('/',1)
if ip4re.match(p[0]):
if len(p) > 1:
n = int(p[1])
else:
n = 32
if cidr(addr2bin(p[0]),n) == cidr(ipnum,n):
return True
elif fnmatchcase(ipaddr,pat):
return True
return False
def parse_addr(t):
"""Split email into user,domain.
>>> parse_addr('user@example.com')
['user', 'example.com']
>>> parse_addr('"user@example.com"')
['user@example.com']
>>> parse_addr('"user@bar"@example.com')
['user@bar', 'example.com']
>>> parse_addr('foo')
['foo']
"""
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
if t.startswith('"'):
if t.endswith('"'): return [t[1:-1]]
pos = t.find('"@')
if pos > 0: return [t[1:pos],t[pos+2:]]
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
u = []
for s,enc in h:
if enc:
try:
u.append(unicode(s,enc))
except LookupError:
u.append(unicode(s))
else:
u.append(unicode(s))
u = ''.join(u)
for enc in ('us-ascii','iso-8859-1','utf8'):
try:
return u.encode(enc)
except UnicodeError: continue
except UnicodeDecodeError: pass
except LookupError: pass
except email.Errors.HeaderParseError: pass
return val
+4
View File
@@ -1,4 +1,8 @@
Here is a history of user visible changes to Python milter. Here is a history of user visible changes to Python milter.
0.8.8 move AddrCache, parse_addr, iniplist, parse_header to Milter package
fix plock for missing source and can't change owner/group
add sample spfmilter.py milter
private_relay config option
0.8.7 Move spf module to pyspf 0.8.7 Move spf module to pyspf
Prevent PTR cache poisoning Prevent PTR cache poisoning
More lame bounce heuristics More lame bounce heuristics
+13 -3
View File
@@ -42,7 +42,7 @@ Quick Installation
1. Build and install Sendmail, enabling libmilter (see libmilter/README). 1. Build and install Sendmail, enabling libmilter (see libmilter/README).
2. Build and install Python, enabling threading. 2. Build and install Python, enabling threading.
3. Install this module: python setup.py --help 3. Install this module: python setup.py --help
4. Add these two lines to sendmail.cf: 4. Add these two lines to sendmail.cf[*]:
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:/home/username/pythonsock Xpythonfilter, S=local:/home/username/pythonsock
@@ -51,9 +51,17 @@ Xpythonfilter, S=local:/home/username/pythonsock
Note that milters should almost certainly not run as root. Note that milters should almost certainly not run as root.
That's it. Incoming mail will cause the milter to print some things, and That's it. Incoming mail will cause the milter to print some things, and
some email will be rejected (see the "header" method). Edit and play. See some email will be rejected (see the "header" method). Edit and play.
bms.py for an example milter used in production. See spfmilter.py for a functional SPF milter, or see bms.py for an complex
milter used in production.
[*] This is for a quick test. Your sendmail.cf in most distros will get
overwritten whenever sendmail.mc is updated. To make a milter permanent,
add something like:
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
to sendmail.mc instead.
Not-so-quick Installation Not-so-quick Installation
------------------------- -------------------------
@@ -90,8 +98,10 @@ some options associated with it. In this case, we have the "S" option, which
names the socket that sendmail will use to communicate with this particular names the socket that sendmail will use to communicate with this particular
milter. This milter's socket is a unix-domain socket in the filesystem. milter. This milter's socket is a unix-domain socket in the filesystem.
See libmilter/README for the definitive list of options. See libmilter/README for the definitive list of options.
NB: The name is specified in two places: here, in sendmail's cf file, and NB: The name is specified in two places: here, in sendmail's cf file, and
in the milter itself. Make sure the two match. in the milter itself. Make sure the two match.
NB: The above lines can be added in your .mc file with this line: NB: The above lines can be added in your .mc file with this line:
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock') INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock')
+108 -47
View File
@@ -1,66 +1,89 @@
When bms.py can't find templates, it passes None to dsn.create_msg(), Convert DSN to REJECT unless sender gets SPF pass or best guess pass. Make
which uses local variable as backup, which no longer exist. configurable by SPF result with NOTSPAM policy (reject or deliver without DSN).
Maybe policy should be NODSN - still verify sender with CBV.
Purge old GOSSiP records nightly. When content filtering is not installed, reject BLACKLISTed MFROM
immediately. There is no use waiting until EOM.
Find and use X-GOSSiP: header for SPAM: and FP: submissions. Would need to Configuration is problematic when handling incoming, but not outgoing mail.
keep tags longer. The problem comes when alice@example.com sends mail to bill@example.com,
and we are the MX for example.com, but alice is sending from some other
MTA. The mail is flagged external, so we don't list example.com in
internal_domains (or we would get "spam from self"). But, if we try to do a
CBV, we get "fraudulent MX", because the MX is ourself! So we need to
avoid doing CBV on such domains. Currently, we try to make sure the SPF
policies don't do CBV.
We now don't check internal domains for incoming mail if there is an
SPF record.
On the other hand, if alice is sending internally, or with SMTP AUTH, she
*does* need the domain to be in internal_domains. The solution to that
is to use the new SMTP AUTH access configuration to specify which domains
can be used by smtp AUTH (by user if desired).
It would be cleaner if CBV would know which domains we have agreed to
be MX for. Some ideas for external connections:
a) check access file for To:example.com RELAY
b) check mailertable
c) check mx_domains config list
d) if there is an SPF record, don't check internal_domains
(let SPF block unauthorized machines)
But that still doesn't handle the roaming user, who won't use SMTP
AUTH, but sends through some hotel MTA. Maybe we don't want to support
him?
When setting up pydspam, both sender and rcpt must resolve to dspam users
for falsepositive recognition. Usually, this means adding
honeypot@mail.example.com to alias list for honeypot in pymilter.cfg.
This needs to be documented. I was caught by it setting up a new site.
Add signature (x-sig=AB7485f=TS) to Received-SPF, so it can be used
to blacklist sources of delayed DSNs.
rcpt-addr may let us know when a recipient is unknown. That should count
against reputation.
Need to use wildcards in blacklist.log: *.madcowsrecord.net
Need to exclude emails like !*-admin@example.com in whitelist_sender.
Need to exclude robot users from autowhitelist. Don't want to have to
list all users, so implement something like !*-admin@bmsi.com,@bmsi.com.
GOSSiP feedback from user training is ignored because UMIS has already been
removed from queue. Maybe keep UMIS in queue, and add method to
alter last feedback for ID.
Generate DSNs according to RFC 3464 Generate DSNs according to RFC 3464
Parse incoming 3464 DSNs for "Action: failed" to recognize delayed
failures. This works regardless of Subject.
Get temperror policy from access file. Get temperror policy from access file.
When training with spam, REJECT after data so that mistakenly blacklisted
senders at least get an error.
Reporting explanation for failure should show source if sender Reporting explanation for failure should show source if sender
provided explanation. provided explanation.
Reports PROBATION even when rejecting message (works, but confusing in log).
Bug in Auto-whitelist. Recent Auto-whitelist doesn't override expired entry. Bug in Auto-whitelist. Recent Auto-whitelist doesn't override expired entry.
DONE Delayed_failure detection needs to handle multi-line header fields.
Also, delayed_failure should be recognized when addressed to
postmaster@helodomain
Need to use wildcards in blacklist.log: *.madcowsrecord.net
Need to exclude emails like !*-admin@example.com in whitelist_sender.
SPF permerror diagnostics should include corrected mechanism. SPF permerror diagnostics should include corrected mechanism.
Delay SPF check until RCPT TO. Cache result to avoid repeating Delay SPF check until RCPT TO. Cache result to avoid repeating
for multiple RCPT. This avoids overhead for invalid RCPT, and for multiple RCPT. This avoids overhead for invalid RCPT, and
allows for per RCPT local policy. allows for per RCPT local policy.
Add auto-blacklisted senders to blacklist.log with timestamp.
Received-SPF header field should show identity that was checked.
Check SPF for outgoing mail (including local policy for internal addresses). Check SPF for outgoing mail (including local policy for internal addresses).
This could also solve the second part of the mail from relay problem below. This could also solve the second part of the mail from relay problem below.
Whitelisted sender from trusted relay get PROBATION. Need to extracted 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.
FIXME: DSN for Permerror shows 'None' for error under some condition.
Another metaDSN format:
Subject: Delivery Report
...
Original-Envelope-ID: SRS0...@...
For selected domains, check rcpts via CBV before accepting mail. Cache For selected domains, check rcpts via CBV before accepting mail. Cache
results. This will kick out dictonary attacks against a mail domain results. This will kick out dictonary attacks against a mail domain
behind a gateway sooner. behind a gateway sooner.
Allow blacklisted emails as well as domains in blacklist.log. Use same Add auto-blacklisted senders to blacklist.log with timestamp.
data structure as autowhitelist.log. Add emails blacklisted via CBV Add emails blacklisted via CBV so that they are remembered across milter
so that they are remembered across milter restarts. restarts.
Make all dictionaries work like honeypot. Do not train as ham unless Make all dictionaries work like honeypot. Do not train as ham unless
whitelisted. Train on blacklisted messages, or spam feedback. This whitelisted. Train on blacklisted messages, or spam feedback. This
@@ -71,9 +94,6 @@ to train on error to minimize labor.
Allow unsigned DSNs from selected domains (that don't accept signed MFROM, Allow unsigned DSNs from selected domains (that don't accept signed MFROM,
e.g. verizon.net). e.g. verizon.net).
Added Message-ID header to DSN with SRS signed sender. When seen on incoming
rfc ignorant failure message, blacklist sender.
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.
@@ -98,11 +118,9 @@ wildcard (e.g. empty localpart).
Quarantined mail is missing headers modified/added by milter after Quarantined mail is missing headers modified/added by milter after
checking dspam. checking dspam.
Require signed MFROM for all incoming bounces when signing all outgoing mail -
except from trusted relays.
Send DSN for permerror before processing extended result. An additional Send DSN for permerror before processing extended result. An additional
DSN may be sent based on extended result. DSN may be sent based on extended result. Send permerror DSN to
postmaster@sending_domain.
Rescind whitelist for banned extensions, in case sender is infected. Rescind whitelist for banned extensions, in case sender is infected.
@@ -116,9 +134,6 @@ SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
Defer TEMPERROR in SPF evaluation - give precedence to security Defer TEMPERROR in SPF evaluation - give precedence to security
(only defer for PASS mechanisms). (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 Create null config that does nothing - except maybe add Received-SPF
headers. Many admins would like to turn features on one at a time. headers. Many admins would like to turn features on one at a time.
@@ -147,8 +162,6 @@ a separate process. However, a significant amount of memory is wasted
for each additional Python VM, and communication between milters for each additional Python VM, and communication between milters
is cumbersome (e.g., adding mail headers, writing external files). is cumbersome (e.g., adding mail headers, writing external files).
Backup copies for outgoing/incoming mail.
Copy incoming wiretap mail, even though sendmail alias works perfectly Copy incoming wiretap mail, even though sendmail alias works perfectly
for the purpose, to avoid having to change two configs for a wiretap. for the purpose, to avoid having to change two configs for a wiretap.
@@ -166,3 +179,51 @@ embarrass yourself), and also removing Received headers with hidepath.
Need a test module to feed sample messages to a milter though a live 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 Require signed MFROM for all incoming bounces when signing all outgoing
mail - except from trusted relays.
DONE Added Message-ID header to DSN with SRS signed sender. When seen on
incoming rfc ignorant failure message, blacklist sender.
DONE Option to add Received-SPF header, but never reject on SPF.
I think the above will handle this.
DONE Received-SPF header field should show identity that was checked.
DONE When training with spam, REJECT after data so that mistakenly blacklisted
senders at least get an error.
DONE Milter won't start when it can't change permissions on *.lock to match
*.log. Should maybe ignore that error - the effect will be to set
the permissions to default.
DONE Milter won't start when a whitelist/blacklist file is missing.
DONE Delayed failure detection should parse From header to find email address.
DONE When bms.py can't find templates, it passes None to dsn.create_msg(),
which uses local variable as backup, which no longer exist. Do plain
CBV in that case instead.
DONE Find and use X-GOSSiP: header for SPAM: and FP: submissions. Would need
to keep tags longer.
DONE Parse incoming 3464 DSNs for "Action: failed" to recognize delayed
failures. This works regardless of Subject.
DONE Reports PROBATION even when rejecting message (works, but confusing in
log).
DONE Delayed_failure detection needs to handle multi-line header fields.
Also, delayed_failure should be recognized when addressed to
postmaster@helodomain
DONE DSN for Permerror shows 'None' for error under some condition.
DONE Allow blacklisted emails as well as domains in blacklist.log. Use same
data structure as autowhitelist.log.
DONE Backup copies for outgoing/incoming mail.
DONE Don't match dynamic ptr in bestguess.
+870 -786
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -2,6 +2,14 @@ Title: Recent Changes
<h2> Recent Changes </h2> <h2> Recent Changes </h2>
<h3> 0.8.7 </h3>
The spf module has been moved to the
<a href="http://cheeseshop.python.org/pypi/pyspf">pyspf</a> package.
Download <a href="http://sourceforge.net/project/showfiles.php?group_id=139894&package_id=191419">here</a>.
<h3> 0.8.6 </h3>
Python milter has been moved to Python milter has been moved to
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge <a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
project</a> for development and release downloads. project</a> for development and release downloads.
+84 -4
View File
@@ -2,8 +2,24 @@ Title: Python Milter FAQ
<h1> Python Milter <a name=faq>FAQ</a> </h1> <h1> Python Milter <a name=faq>FAQ</a> </h1>
<menu>
<li> <a href="#compiling">Compiling Python Milter</a>
<li> <a href="#running">Running Python Milter</a>
<li> <a href="#spf">Using SPF</a>
<li> <a href="#srs">Using SRS</a>
</menu>
<ol> <ol>
<h3> Compiling Python Milter </h3>
<h3> <a name="compiling">Compiling Python Milter </a> </h3>
<li> Q. I have tried to download the current milter code and my virus scan
traps several viruses in the download.
<p> A. The milter source includes a number of deactivated viruses in
the test directory. All but the first and last lines of the base64
encoded virus data has been removed. I suppose I should randomize
the first and last lines as well, since pymilter just deletes executables,
and doesn't look for signatures.
<li> Q. I have installed sendmail from source, but Python milter won't <li> Q. I have installed sendmail from source, but Python milter won't
compile. compile.
<p> A. Even though libmilter is officially supported in sendmail-8.12, <p> A. Even though libmilter is officially supported in sendmail-8.12,
@@ -36,7 +52,7 @@ in setup.py to
define_macros = [ ('MAX_ML_REPLY',1) ] define_macros = [ ('MAX_ML_REPLY',1) ]
</pre> </pre>
<h3> Running Python Milter </h3> <h3> <a name="running">Running Python Milter </a></h3>
<li> Q. The sample.py milter prints a message, then just sits there. <li> Q. The sample.py milter prints a message, then just sits there.
<pre> <pre>
@@ -186,10 +202,16 @@ The <code>internal_domains</code> option is simplistic, it assumes all
valid senders of the domains are internal. SPF provides a much more general valid senders of the domains are internal. SPF provides a much more general
check of IP and MAIL FROM for external email. Pymilter should soon check of IP and MAIL FROM for external email. Pymilter should soon
have a local policy feature for more general checking of internal mail. have a local policy feature for more general checking of internal mail.
<li> Q. <code>mail_archive</code> isn't working. Or I don't understand how
it's suppose to work. I have
<code>mail_archive = /var/mail/mail_archive</code>
in <code>pymilter.cfg</code> but nothing ever gets dumped into
<code>/var/mail/mail_archive</code>.
<p> A. The 'mail' user needs to have write access. Permission failures
should be logged as a traceback in milter.log if it doesn't.
<h3> Using SPF </h3> <h3> <a name="spf">Using SPF </a></h3>
<a name="spf">
<li> Q. So how do I use the SPF support? The sample.py milter doesn't seem <li> Q. So how do I use the SPF support? The sample.py milter doesn't seem
to use it. to use it.
<p> A. The bms.py milter supports spf. The RedHat RPMs will set almost <p> A. The bms.py milter supports spf. The RedHat RPMs will set almost
@@ -209,5 +231,63 @@ everything up for you. For other systems:
logfiles and a simple cron script using <code>find</code> to clean logfiles and a simple cron script using <code>find</code> to clean
<code>tempdir</code>. <code>tempdir</code>.
</ol> </ol>
In CVS, there is <code>spfmilter.py</code>. Run that as a service,
and it does just SPF. It uses the sendmail <code>access</code>
file to configure SPF responses just like <code>bms.py</code>, but
supports only REJECT and OK.
<li> Q. The SPF DSN is sent at least once for domains that don't publish a SPF.
How do I stop this behavior?
<p> A. The SPF response is controlled by <code>/etc/mail/access</code>
(actually the file you specify with <code>access_file</code> in
the <code>[spf]</code> section of <code>pymilter.cfg</code>).
Responses are OK, CBV, and REJECT. CBV sends the DSN.
<p>
You can change the defaults. For instance, I have:
<pre>
SPF-None: REJECT
SPF-Neutral: CBV
SPF-Softfail: CBV
SPF-Permerror: CBV
</pre>
I have best_guess = 1, so SPF none is converted to PASS/NEUTRAL for policy
lookup, and 3 strikes (no PTR, no HELO, no SPF) becomes "SPF NONE" for local
policy purposes (the Received-SPF header always shows the official SPF
result.)
<p>
You can change the default for specific domains:
<pre>
# these guys aren't going to pay attention to CBVs anyway...
SPF-None:cia.gov REJECT
SPF-None:fbi.gov REJECT
SPF-Neutral:aol.com REJECT
SPF-Softfail:ebay.com REJECT
</pre>
<h3> <a name="srs">Using SRS </a></h3>
<li> Q. The SRS part doesn't seem to work as whenever I try to start
<code>/etc/init.d/pysrs</code>, I get this in
<code>/var/log/milter/pysrs.log</code>:
<pre>
ConfigParser.NoOptionError: No option 'fwdomain' in section: 'srs'
</pre>
<p> A. You need to specify the forward domain - i.e. the domain you want
SRS to rewrite stuff too.
<p>
For instance, I have:
<pre>
# sample SRS configuration
[srs]
secret = don't you wish
maxage = 8
hashlength = 5
;database=/var/log/milter/srs.db
fwdomain = bmsi.com
sign=bmsi.com,mail.bmsi.com,gathman.org
srs=bmsaix.bmsi.com,bmsred.bmsi.com,stl.gathman.org,bampa.gathman.org
</pre>
The <code>sign</code> is for local domains which are signed.
The <code>srs</code> list is for other domains which you are relaying,
and which need to have SRS checked/undone for bounces.
</ol> </ol>
+2
View File
@@ -9,6 +9,7 @@
<li><a href="logmsgs.html">Log&nbsp;Messages</a> <li><a href="logmsgs.html">Log&nbsp;Messages</a>
<li><a href="http://bmsi.com/mailman/listinfo/pymilter">Mailing&nbsp;List</a> <li><a href="http://bmsi.com/mailman/listinfo/pymilter">Mailing&nbsp;List</a>
<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>
<h3>Links</h3> <h3>Links</h3>
<li><a href="http://www.milter.org/milter_api/api.html">C&nbsp;API</a> <li><a href="http://www.milter.org/milter_api/api.html">C&nbsp;API</a>
<li><a href="http://www.milter.org/">Milter.Org</a> <li><a href="http://www.milter.org/">Milter.Org</a>
@@ -17,5 +18,6 @@
<li><a href="http://www.openspf.org/">SPF</a> <li><a href="http://www.openspf.org/">SPF</a>
<li><a href="pysrs.html">pysrs</a> <li><a href="pysrs.html">pysrs</a>
<li><a href="http://cheeseshop.python.org/pypi/pyspf">pyspf</a> <li><a href="http://cheeseshop.python.org/pypi/pyspf">pyspf</a>
<li><a href="http://bmsi.com/python/pygossip.html">pygossip</a>
<li><a href="http://bmsi.com/python/dspam.html">pydspam</a> <li><a href="http://bmsi.com/python/dspam.html">pydspam</a>
<li><a href="http://bmsi.com/libdspam/dspam.html">libdspam</a> <li><a href="http://bmsi.com/libdspam/dspam.html">libdspam</a>
+1 -1
View File
@@ -20,7 +20,7 @@ ALT="Viewable With Any Browser" BORDER="0"></A>
Stuart D. Gathman</a><br> Stuart D. Gathman</a><br>
This web page is written by Stuart D. Gathman<br>and<br>sponsored by This web page is written by Stuart D. Gathman<br>and<br>sponsored by
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br> <a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
Last updated Dec 29, 2005</h4> Last updated Mar 30, 2007</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> |
+155
View File
@@ -0,0 +1,155 @@
## To roll your own milter, create a class that extends Milter.
# See the pymilter project at http://bmsi.com/python/milter.html
# based on Sendmail's milter API http://www.milter.org/milter_api/api.html
# This code is open-source on the same terms as Python.
## Milter calls methods of your class at milter events.
## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
## You can also add/del recipients, replacebody, add/del headers, etc.
import Milter
import StringIO
import time
import email
from socket import AF_INET, AF_INET6
def parse_addr(t):
"""Split email into user,domain.
>>> parse_addr('user@example.com')
['user', 'example.com']
>>> parse_addr('"user@example.com"')
['user@example.com']
>>> parse_addr('"user@bar"@example.com')
['user@bar', 'example.com']
>>> parse_addr('foo')
['foo']
"""
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
if t.startswith('"'):
if t.endswith('"'): return [t[1:-1]]
pos = t.find('"@')
if pos > 0: return [t[1:pos],t[pos+2:]]
return t.split('@')
class myMilter(Milter.Milter):
def __init__(self): # A new instance with each new connection.
self.id = Milter.uniqueID() # Integer incremented with each call.
# each connection runs in its own thread and has its own myMilter
# instance. Python code must be thread safe. This is trivial if only stuff
# in myMilter instances is referenced.
def connect(self, IPname, family, hostaddr):
# (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
# (self, 'ip6.mxout.example.com', AF_INET6,
# ('3ffe:80e8:d8::1', 4720, 1, 0) )
self.IP = hostaddr[0]
self.port = hostaddr[1]
if family == AF_INET6:
self.flow = hostaddr[2]
self.scope = hostaddr[3]
else:
self.flow = None
self.scope = None
self.IPname = IPname # Name from a reverse IP lookup
self.H = None
self.fp = None
self.receiver = self.getsymval('j')
self.log("connect from %s at %s" % (IPname, hostaddr) )
return Milter.CONTINUE
## def hello(self,hostname):
def hello(self, heloname):
# (self, 'mailout17.dallas.texas.example.com')
self.H = heloname
self.log("HELO %s" % heloname)
if heloname.find('.') < 0: # illegal helo name
# NOTE: example only - too many real braindead clients to reject on this
self.setreply('550','5.7.1','Sheesh people! Use a proper helo name!')
return Milter.REJECT
return Milter.CONTINUE
## def envfrom(self,f,*str):
def envfrom(self, mailfrom, *str):
self.F = mailfrom
self.R = [] # list of recipients
self.fromparms = Milter.dictfromlist(str) # ESMTP parms
self.user = self.getsymval('{auth_authen}') # authenticated user
self.log("mail from:", mailfrom, *str)
self.fp = StringIO.StringIO()
self.canon_from = '@'.join(parse_addr(mailfrom))
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
return Milter.CONTINUE
## def envrcpt(self, to, *str):
def envrcpt(self, recipient, *str):
rcptinfo = to,Milter.dictfromlist(str)
self.R.append(rcptinfo)
return Milter.CONTINUE
def header(self, name, hval):
self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
return Milter.CONTINUE
def eoh(self):
self.fp.write("\n") # terminate headers
return Milter.CONTINUE
def body(self, chunk):
self.fp.write(chunk)
return Milter.CONTINUE
def eom(self):
self.fp.seek(0)
msg = email.message_from_file(self.fp)
self.setreply('250','2.5.1','Grokked by pymilter')
# many milter functions can only be called from eom()
# example of adding a Bcc:
self.addrcpt('<%s>' % 'spy@example.com')
return Milter.ACCEPT
def close(self):
# always called, even when abort is called. Clean up
# any external resources here.
return Milter.CONTINUE
def abort(self):
# client disconnected prematurely
return Milter.CONTINUE
## === Support Functions ===
def log(self,*msg):
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print i,
print
## ===
def main():
# Register to have the Milter factory create instances of your class:
Milter.factory = myMilter
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
flags += Milter.ADDRCPT
flags += Milter.DELRCPT
Milter.set_flags(flags) # tell Sendmail which features we use
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,timeout)
print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__":
main()
+23 -4
View File
@@ -6,9 +6,10 @@ tempdir = /var/log/milter/save
# how long to wait for a response from sendmail before giving up # how long to wait for a response from sendmail before giving up
;timeout=600 ;timeout=600
log_headers = 0 log_headers = 0
# connection ips and hostnames are matched against this glob style list # Connection ips and hostnames are matched against this glob style list
# to recognize internal senders. # to recognize internal senders. You probably need to change this.
;internal_connect = 192.168.*.*,127.* # The default is a good guess to try and prevent newbie frustration.
internal_connect = 192.168.0.0/16,127.*
# mail that is not an internal_connect and claims to be from an # mail that is not an internal_connect and claims to be from an
# internal domain is rejected. Furthermore, internal mail that # internal domain is rejected. Furthermore, internal mail that
@@ -23,6 +24,9 @@ log_headers = 0
# SPF checks are bypassed for internal connections and trusted relays. # SPF checks are bypassed for internal connections and trusted relays.
;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.
;private_relay = mycorp.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
# will save some DNS lookups when rejecting certain viruses. # will save some DNS lookups when rejecting certain viruses.
@@ -78,7 +82,7 @@ reject_spoofed = 0
# refuses mail from user names commonly abused in that way. # refuses mail from user names commonly abused in that way.
;banned_users = postmaster, mailer-daemon, clamav ;banned_users = postmaster, mailer-daemon, clamav
# See http://spf.pobox.com for more info on SPF. # See http://www.openspf.com for more info on SPF.
[spf] [spf]
# namespace where SPF records can be supplied for domains without one # namespace where SPF records can be supplied for domains without one
# records are searched for under _spf.domain.com # records are searched for under _spf.domain.com
@@ -196,3 +200,18 @@ blind = 1
# delivered. # delivered.
;dspam_screener=david,goliath ;dspam_screener=david,goliath
# The dspam CGI can also be used: logins must match dspam users # The dspam CGI can also be used: logins must match dspam users
# Optional pygossip interface
#
# GOSSiP tracks reputation of domain:qualifier pairs. For instance,
# the reputation of example.com:SPF is tracked separately from
# example.com:neutral. Currently qualifiers are
# SPF,neutral,softfail,fail,permerror,GUESS,HELO
[gossip]
# Use a dedicated GOSSiP server. If not specified, a local database
# will be used.
;server=host:11900
# If a local database is used, also consult these GOSSiP servers about
# domains. Peer reputation is also tracked as to how often they
# agree with us, and weighted accordingly.
;peers=host1:port,host2
+7 -3
View File
@@ -8,7 +8,7 @@
# config: /etc/mail/pymilter.cfg # config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid # pidfile: /var/run/milter/milter.pid
python="python2.3" python="python2.4"
pidof() { pidof() {
set - "" set - ""
@@ -23,7 +23,7 @@ pidof() {
# Source function library. # Source function library.
. /etc/rc.d/init.d/functions . /etc/rc.d/init.d/functions
[ -x /var/log/milter/start.sh ] || exit 0 [ -x /usr/lib/pymilter/start.sh ] || exit 0
RETVAL=0 RETVAL=0
prog="milter" prog="milter"
@@ -32,7 +32,11 @@ start() {
# Start daemons. # Start daemons.
echo -n "Starting $prog: " echo -n "Starting $prog: "
daemon --check milter --user mail /var/log/milter/start.sh if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /usr/lib/pymilter/start.sh milter bms
RETVAL=$? RETVAL=$?
echo echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
+7 -3
View File
@@ -8,7 +8,7 @@
# config: /etc/mail/pymilter.cfg # config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid # pidfile: /var/run/milter/milter.pid
python="python2.3" python="python2.4"
pidof() { pidof() {
set - "" set - ""
@@ -23,7 +23,7 @@ pidof() {
# Source function library. # Source function library.
. /etc/rc.d/init.d/functions . /etc/rc.d/init.d/functions
[ -x /var/log/milter/start.sh ] || exit 0 [ -x /usr/lib/pymilter/start.sh ] || exit 0
RETVAL=0 RETVAL=0
prog="milter" prog="milter"
@@ -32,7 +32,11 @@ start() {
# Start daemons. # Start daemons.
echo -n "Starting $prog: " echo -n "Starting $prog: "
daemon --check milter --user mail /var/log/milter/start.sh if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /usr/lib/pymilter/start.sh milter bms
RETVAL=$? RETVAL=$?
echo echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
+19 -15
View File
@@ -1,19 +1,20 @@
/* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org) /* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org)
* Portions Copyright (C) 2001,2002,2003,2004 Stuart Gathman (stuart@bmsi.com) * Portions Copyright (C) 2001,2002,2003,2004,2005,2006,2007
* * Stuart Gathman (stuart@bmsi.com)
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License * This program is free software: you can redistribute it and/or modify it
* as published by the Free Software Foundation; either version 2 * under the terms of the GNU General Public License as published by the
* of the License, or (at your option) any later version. * Free Software Foundation, either version 2 of the License, or (at your
* * option) any later version.
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of * This program is distributed in the hope that it will be useful, but
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * WITHOUT ANY WARRANTY; without even the implied warranty of
* GNU General Public License for more details. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* * General Public License for more details.
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software * You should have received a copy of the GNU General Public License along
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
* *
* milterContext object and thread interface contributed by * milterContext object and thread interface contributed by
* Stuart D. Gathman <stuart@bmsi.com> * Stuart D. Gathman <stuart@bmsi.com>
@@ -34,6 +35,9 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* Revision 1.10 2006/02/12 02:00:42 customdesigned
* Resolve FIXME for wrap_close.
*
* Revision 1.9 2005/12/23 21:46:36 customdesigned * Revision 1.9 2005/12/23 21:46:36 customdesigned
* Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER) * Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER)
* *
+4 -2
View File
@@ -24,11 +24,13 @@ exists by sending you this DSN. We will remember this sender and not
bother you again for a while. You can avoid this message entirely for bother you again for a while. You can avoid this message entirely for
legitimate mail by using an authorized SMTP server. Contact your mail legitimate mail by using an authorized SMTP server. Contact your mail
administrator and ask how to configure your email client to use an administrator and ask how to configure your email client to use an
authorized server. authorized server.
If you never sent the above message, then your domain has been forged. If you never sent the above message, then your domain has been forged.
Your mail admin needs to publish a strict SPF record so that I can reject Your mail admin needs to publish a strict SPF record so that I can reject
those forgeries instead of bugging you about them. those forgeries instead of bugging you about them.
See http://openspf.org for details.
If you need further assistance, please do not hesitate to contact me. If you need further assistance, please do not hesitate to contact me.
+1
View File
@@ -14,6 +14,7 @@ Delivery to the following recipients has been delayed.
%(rcpt)s %(rcpt)s
Subject: %(subject)s Subject: %(subject)s
Received-SPF: %(spf_result)s
Your spf record has a permanent error. The error was: Your spf record has a permanent error. The error was:
+157 -50
View File
@@ -1,5 +1,4 @@
%define name milter %define version 0.8.9
%define version 0.8.7
%define release 1 %define release 1
# what version of RH are we building for? # what version of RH are we building for?
%define redhat7 0 %define redhat7 0
@@ -11,63 +10,77 @@
# some systems dont have initrddir defined # some systems dont have initrddir defined
%{?_initrddir:%define _initrddir /etc/rc.d/init.d} %{?_initrddir:%define _initrddir /etc/rc.d/init.d}
%if %{redhat7} # Redhat 7.x and earlier (multiple ps lines per thread) %if %{redhat7}
# Redhat 7.x and earlier (multiple ps lines per thread)
%define sysvinit milter.rc7 %define sysvinit milter.rc7
%else %else
%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 %ifos Linux # whether to use system default python?
%define python python2.4 %define python python
#define python python2.4
%else %else
%define python python %define python python
%endif %endif
%ifos aix4.1
%define libdir /var/log/milter
%else
%define libdir /usr/lib/pymilter
%endif
Summary: Python interface to sendmail milter API # This spec file contains 2 noarch packages in addition to the pymilter
Name: %{name} # module. To compile all three, use:
# rpmbuild -ba --target=i386,noarch pymilter.spec
%ifarch noarch
Name: milter
Group: Applications/System
Summary: BMS spam and reputation milter
Version: %{version} Version: %{version}
Release: %{release} Release: %{release}
Source: %{name}-%{version}.tar.gz Source: pymilter-%{version}.tar.gz
#Patch: %{name}-%{version}.patch #Patch: %{name}-%{version}.patch
Copyright: GPL License: GPL
Group: Development/Libraries Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot BuildRoot: %{_tmppath}/%{name}-buildroot
Prefix: %{_prefix} Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com> Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com> Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, sendmail >= 8.13 Requires: %{python} >= 2.4, pyspf >= 2.0.4, pymilter
%ifos Linux %ifos Linux
Requires: chkconfig Requires: chkconfig
%endif %endif
BuildRequires: %{python}-devel >= 2.4, sendmail-devel >= 8.13
%description %description -n milter
This is a python extension module to enable python scripts to A complex but effective spam filtering, SPF checking, and reputation tracking
attach to sendmail's libmilter functionality. Additional python mail application. It uses pydspam if installed for bayesian filtering.
modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV. %package spf
Group: Applications/System
Summary: BMS spam and reputation milter
Requires: pyspf >= 2.0.4, pymilter
Obsoletes: pymilter-spf
%description spf
A simple mail filter to add Received-SPF headers and reject forged mail.
Rejection policy is configured via sendmail access file.
%prep %prep
%setup %setup -n pymilter-%{version}
#patch -p0 -b .bms #patch -p0 -b .bms
%build
%if %{redhat7}
LDFLAGS="-s"
%else # Redhat builds debug packages after 7.3
LDFLAGS="-g"
%endif
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
mkdir -p $RPM_BUILD_ROOT/var/log/milter mkdir -p $RPM_BUILD_ROOT/var/log/milter
mkdir -p $RPM_BUILD_ROOT/etc/mail mkdir -p $RPM_BUILD_ROOT/etc/mail
mkdir $RPM_BUILD_ROOT/var/log/milter/save mkdir $RPM_BUILD_ROOT/var/log/milter/save
cp bms.py *.txt $RPM_BUILD_ROOT/var/log/milter mkdir -p $RPM_BUILD_ROOT%{libdir}
cp *.txt $RPM_BUILD_ROOT/var/log/milter
cp bms.py spfmilter.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
# logfile rotation # logfile rotation
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
@@ -76,6 +89,11 @@ cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF'
copytruncate copytruncate
compress compress
} }
/var/log/milter/banned_ips {
rotate 3
daily
copytruncate
}
EOF EOF
# purge saved defanged message copies # purge saved defanged message copies
@@ -95,23 +113,18 @@ EOF
chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
%ifos aix4.1 %ifos aix4.1
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF' cat >$RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
#!/bin/sh #!/bin/sh
cd /var/log/milter cd /var/log/milter
# uncomment to enable sgmlop if installed # uncomment to enable sgmlop if installed
#export PYTHONPATH=/usr/local/lib/python2.1/site-packages #export PYTHONPATH=/usr/local/lib/python2.1/site-packages
exec /usr/local/bin/python bms.py >>milter.log 2>&1 exec /usr/local/bin/python bms.py >>milter.log 2>&1
EOF EOF
%else %else # not aix4.1
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF' cp start.sh $RPM_BUILD_ROOT%{libdir}
#!/bin/sh
cd /var/log/milter
exec >>milter.log 2>&1
%{python} bms.py &
echo $! >/var/run/milter/milter.pid
EOF
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
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
@@ -120,8 +133,24 @@ python="%{python}"
w w
q q
EOF EOF
%endif ed $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter <<'EOF'
chmod a+x $RPM_BUILD_ROOT/var/log/milter/start.sh /^python=/
c
python="%{python}"
.
w
q
EOF
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^python=/
c
python="%{python}"
.
w
q
EOF
%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/var/run/milter
mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
@@ -129,31 +158,37 @@ cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
%ifos aix4.1 %ifos aix4.1
%post %post
mkssys -s milter -p /var/log/milter/start.sh -u 25 -S -n 15 -f 9 -G mail || : mkssys -s milter -p %{libdir}/start.sh -u 25 -S -n 15 -f 9 -G mail || :
%preun %preun
if [ $1 = 0 ]; then if [ $1 = 0 ]; then
rmssys -s milter || : rmssys -s milter || :
fi fi
%else %else # not aix4.1
%post %post -n milter
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf" #echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
/sbin/chkconfig --add milter /sbin/chkconfig --add milter
%preun %preun -n milter
if [ $1 = 0 ]; then if [ $1 = 0 ]; then
/sbin/chkconfig --del milter /sbin/chkconfig --del milter
fi fi
%endif %post spf
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
/sbin/chkconfig --add spfmilter
%clean %preun spf
rm -rf $RPM_BUILD_ROOT if [ $1 = 0 ]; then
/sbin/chkconfig --del spfmilter
fi
%endif # aix4.1
%files -f INSTALLED_FILES %files
%defattr(-,root,root) %defattr(-,root,root)
%doc README HOWTO ChangeLog NEWS TODO CREDITS sample.py %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
@@ -161,19 +196,91 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,mail,mail) %defattr(-,mail,mail)
%endif %endif
%dir /var/log/milter %dir /var/log/milter
%dir /var/run/milter
%dir /var/log/milter/save %dir /var/log/milter/save
%config /var/log/milter/start.sh %config %{libdir}/bms.py
%config /var/log/milter/bms.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/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) /etc/mail/pymilter.cfg %config(noreplace) /etc/mail/pymilter.cfg
/usr/share/sendmail-cf/hack/rhsbl.m4 /usr/share/sendmail-cf/hack/rhsbl.m4
%files spf
%defattr(-,root,root)
%dir /var/log/milter
%{libdir}/spfmilter.py*
%config(noreplace) /etc/mail/spfmilter.cfg
/etc/rc.d/init.d/spfmilter
%else # not noarch
%define name pymilter
Summary: Python interface to sendmail milter API
Name: %{name}
Version: %{version}
Release: %{release}
Source: %{name}-%{version}.tar.gz
#Patch: %{name}-%{version}.patch
License: GPL
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot
Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, sendmail >= 8.13
BuildRequires: %{python}-devel >= 2.4, sendmail-devel >= 8.13
%description
This is a python extension module to enable python scripts to
attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV.
%prep
%setup
#patch -p0 -b .bms
%build
%if %{redhat7}
LDFLAGS="-s"
%else # Redhat builds debug packages after 7.3
LDFLAGS="-g"
%endif
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
%if !%{redhat7}
grep '.pyc$' INSTALLED_FILES | sed -e 's/c$/o/' >>INSTALLED_FILES
%endif
%files -f INSTALLED_FILES
%defattr(-,root,root)
%doc README HOWTO ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%endif # noarch
%clean
rm -rf $RPM_BUILD_ROOT
%changelog %changelog
* 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
- Remove spf dependency from dsn.py, add dns.py
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
- move AddrCache, parse_addr, iniplist to Milter package
- move parse_header to Milter.utils
- fix plock for missing source and can't change owner/group
- add sample spfmilter.py milter
- private_relay config option
- persist delayed DSN blacklisting
- handle gossip server restart without disabling gossip
- split out pymilter and pymilter-spf packages
- move milter apps to /usr/lib/pymilter
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1 * Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
- More lame bounce heuristics - More lame bounce heuristics
- SPF moved to pyspf RPM - SPF moved to pyspf RPM
+3 -1
View File
@@ -6,6 +6,7 @@ from distutils.core import setup, Extension
# on slackware and debian, leave it out entirely. It depends # on slackware and debian, leave it out entirely. It depends
# on how libmilter was built by the sendmail package. # on how libmilter was built by the sendmail package.
libs = ["milter", "smutil"] libs = ["milter", "smutil"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian
# patch distutils if it can't cope with the "classifiers" or # patch distutils if it can't cope with the "classifiers" or
# "download_url" keywords # "download_url" keywords
@@ -15,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 = "milter", version = '0.8.7', setup(name = "pymilter", version = '0.8.9',
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,6 +34,7 @@ sending DSNs or doing CBVs.
packages = ['Milter'], packages = ['Milter'],
ext_modules=[ ext_modules=[
Extension("milter", ["miltermodule.c"], Extension("milter", ["miltermodule.c"],
library_dirs=libdirs,
libraries=libs, libraries=libs,
# set MAX_ML_REPLY to 1 for sendmail < 8.13 # set MAX_ML_REPLY to 1 for sendmail < 8.13
define_macros = [ ('MAX_ML_REPLY',32) ] define_macros = [ ('MAX_ML_REPLY',32) ]
+20
View File
@@ -0,0 +1,20 @@
[milter]
# The socket used to communicate with sendmail
socketname = /var/run/milter/spfmiltersock
# Name of the milter given to sendmail
name = pyspffilter
# Trusted relays such as secondary MXes that should not have SPF checked.
;trusted_relay =
# Internal networks that should not have SPF checked.
internal_connect = 127.0.0.1,192.168.0.0/16,10.0.0.0/8
# See http://www.openspf.com for more info on SPF.
[spf]
# Use sendmail access map or similar format for detailed spf policy.
# SPF entries in the access map will override defaults.
access_file = /etc/mail/access.db
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
+253
View File
@@ -0,0 +1,253 @@
# A simple SPF milter.
# You must install pyspf for this to work.
# http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2007 Business Management Systems, Inc.
# This code is under GPL. See COPYING for details.
import sys
import Milter
import spf
import syslog
import anydbm
from Milter.config import MilterConfigParser
from Milter.utils import iniplist,parse_addr
syslog.openlog('spfmilter',0,syslog.LOG_MAIL)
class Config(object):
"Hold configuration options."
pass
def read_config(list):
"Return new config object."
cp = MilterConfigParser()
cp.read(list)
if cp.has_option('milter','datadir'):
os.chdir(cp.get('milter','datadir'))
conf = Config()
conf.socketname = cp.getdefault('milter','socketname', '/tmp/spfmiltersock')
conf.miltername = cp.getdefault('milter','name','pyspffilter')
conf.trusted_relay = cp.getlist('milter','trusted_relay')
conf.internal_connect = cp.getlist('milter','internal_connect')
conf.trusted_forwarder = cp.getlist('spf','trusted_relay')
conf.access_file = cp.getdefault('spf','access_file',None)
return conf
class SPFPolicy(object):
"Get SPF policy by result from sendmail style access file."
def __init__(self,sender,access_file=None):
self.sender = sender
self.domain = sender.split('@')[-1].lower()
if access_file:
try: acf = anydbm.open(access_file,'r')
except: acf = None
else: acf = None
self.acf = acf
def getPolicy(self,pfx):
acf = self.acf
if not acf: return None
try:
return acf[pfx + self.sender]
except KeyError:
try:
return acf[pfx + self.domain]
except KeyError:
try:
return acf[pfx]
except KeyError:
return None
class spfMilter(Milter.Milter):
"Milter to check SPF. Each connection gets its own instance."
def log(self,*msg):
syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg])))
def __init__(self):
self.mailfrom = None
self.id = Milter.uniqueID()
# we don't want config used to change during a connection
self.conf = config
# addheader can only be called from eom(). This accumulates added headers
# which can then be applied by alter_headers()
def add_header(self,name,val,idx=-1):
self.new_headers.append((name,val,idx))
self.log('%s: %s' % (name,val))
def connect(self,hostname,unused,hostaddr):
self.internal_connection = False
self.trusted_relay = False
self.hello_name = None
# sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j').strip()
if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0]
if iniplist(ipaddr,self.conf.internal_connect):
self.internal_connection = True
if iniplist(ipaddr,self.conf.trusted_relay):
self.trusted_relay = True
else: ipaddr = ''
self.connectip = ipaddr
if self.internal_connection:
connecttype = 'INTERNAL'
else:
connecttype = 'EXTERNAL'
if self.trusted_relay:
connecttype += ' TRUSTED'
self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype))
return Milter.CONTINUE
def hello(self,hostname):
self.hello_name = hostname
self.log("hello from %s" % hostname)
return Milter.CONTINUE
# multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message.
def envfrom(self,f,*str):
self.log("mail from",f,str)
if not self.hello_name:
self.log('REJECT: missing HELO')
self.setreply('550','5.7.1',"It's polite to say helo first.")
return Milter.REJECT
self.mailfrom = f
self.new_headers = []
t = parse_addr(f)
if len(t) == 2: t[1] = t[1].lower()
self.canon_from = '@'.join(t)
if not (self.internal_connection or self.trusted_relay) and self.connectip:
rc = self.check_spf()
if rc != Milter.CONTINUE: return rc
return Milter.CONTINUE
def envrcpt(self,f,*str):
return Milter.CONTINUE
def header(self,name,hval):
return Milter.CONTINUE
def eoh(self):
return Milter.CONTINUE
def eom(self):
for name,val,idx in self.new_headers:
try:
self.addheader(name,val,idx)
except:
self.addheader(name,val) # older sendmail can't insheader
return Milter.CONTINUE
def close(self):
return Milter.CONTINUE
def check_spf(self):
receiver = self.receiver
for tf in self.conf.trusted_forwarder:
q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False)
res,code,txt = q.check()
if res == 'pass':
self.log("TRUSTED_FORWARDER:",tf)
break
else:
q = spf.query(self.connectip,self.canon_from,self.hello_name,
receiver=receiver,strict=False)
q.set_default_explanation(
'SPF fail: see http://openspf.org/why.html?sender=%s&ip=%s' % (q.s,q.i))
res,code,txt = q.check()
if res not in ('pass','temperror'):
if self.mailfrom != '<>':
# check hello name via spf unless spf pass
h = spf.query(self.connectip,'',self.hello_name,receiver=receiver)
hres,hcode,htxt = h.check()
if hres in ('deny','fail','neutral','softfail'):
self.log('REJECT: hello SPF: %s 550 %s' % (hres,htxt))
self.setreply('550','5.7.1',htxt,
"The hostname given in your MTA's HELO response is not listed",
"as a legitimate MTA in the SPF records for your domain. If you",
"get this bounce, the message was not in fact a forgery, and you",
"should IMMEDIATELY notify your email administrator of the problem."
)
return Milter.REJECT
else:
hres,hcode,htxt = res,code,txt
else: hres = None
p = SPFPolicy(q.s,self.conf.access_file)
if res == 'fail':
policy = p.getPolicy('spf-fail:')
if not policy or policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT
if res == 'softfail':
policy = p.getPolicy('spf-softfail:')
if policy and policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT
elif res == 'permerror':
policy = p.getPolicy('spf-permerror:')
if not policy or policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
# latest SPF draft recommends 5.5.2 instead of 5.7.1
self.setreply(str(code),'5.5.2',txt,
'There is a fatal syntax error in the SPF record for %s' % q.o,
'We cannot accept mail from %s until this is corrected.' % q.o
)
return Milter.REJECT
elif res == 'temperror':
policy = p.getPolicy('spf-temperror:')
if not policy or policy == 'REJECT':
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'4.3.0',txt)
return Milter.TEMPFAIL
elif res == 'neutral' or res == 'none':
policy = p.getPolicy('spf-neutral:')
if policy and policy == 'REJECT':
self.log('REJECT NEUTRAL:',q.s)
self.setreply('550','5.7.1',
"%s requires and SPF PASS to accept mail from %s. [http://openspf.org]"
% (receiver,q.s))
return Milter.REJECT
elif res == 'pass':
policy = p.getPolicy('spf-pass:')
if policy and policy == 'REJECT':
self.log('REJECT PASS:',q.s)
self.setreply('550','5.7.1',
"%s has been blacklisted by %s." % (q.s,receiver))
return Milter.REJECT
self.add_header('Received-SPF',q.get_header(res,receiver),0)
if hres and q.h != q.o:
self.add_header('X-Hello-SPF',hres,0)
return Milter.CONTINUE
if __name__ == "__main__":
Milter.factory = spfMilter
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
global config
config = read_config(['spfmilter.cfg','/etc/mail/spfmilter.cfg'])
miltername = config.miltername
socketname = config.socketname
print """To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=%s
X%s, S=local:%s
See the sendmail README for libmilter.
sample spfmilter startup""" % (miltername,miltername,socketname)
sys.stdout.flush()
Milter.runmilter("pyspffilter",socketname,240)
print "sample spfmilter shutdown"
Executable
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
#
# spfmilter This shell script takes care of starting and stopping spfmilter.
#
# chkconfig: 2345 80 30
# description: a process that checks SPF for messages sent through sendmail.
# processname: spfmilter
# config: /etc/mail/spfmilter.cfg
# pidfile: /var/run/milter/spfmilter.pid
python="python2.4"
pidof() {
set - ""
if set - `ps -e -o pid,cmd | grep "${python} spfmilter.py"` &&
[ "$2" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
[ -x /usr/lib/pymilter/start.sh ] || exit 0
RETVAL=0
prog="spfmilter"
start() {
# Start daemons.
echo -n "Starting $prog: "
if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /usr/lib/pymilter/start.sh spfmilter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/spfmilter
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc milter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/spfmilter
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/spfmilter ]; then
stop
start
RETVAL=$?
fi
;;
status)
status spfmilter
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
appname="$1"
script="${2:-${appname}}"
datadir=/var/log/milter
python="python2.4"
exec >>${datadir}/${appname}.log 2>&1
if test -s ${datadir}/${script}.py; then
cd ${datadir} # use version in log dir if it exists for debugging
else
cd /usr/lib/pymilter
fi
${python} ${script}.py &
echo $! >/var/run/milter/${appname}.pid
+2
View File
@@ -2,6 +2,7 @@ import unittest
import testbms import testbms
import testmime import testmime
import testsample import testsample
import testutils
import os import os
def suite(): def suite():
@@ -9,6 +10,7 @@ def suite():
s.addTest(testbms.suite()) s.addTest(testbms.suite())
s.addTest(testmime.suite()) s.addTest(testmime.suite())
s.addTest(testsample.suite()) s.addTest(testsample.suite())
s.addTest(testutils.suite())
return s return s
if __name__ == '__main__': if __name__ == '__main__':
+19
View File
@@ -277,6 +277,25 @@ class BMSMilterTestCase(unittest.TestCase):
fp = milter._body fp = milter._body
open("test/test1.tstout","w").write(fp.getvalue()) open("test/test1.tstout","w").write(fp.getvalue())
def testFindsrs(self):
if not bms.srs:
import SRS
bms.srs = SRS.new(secret='test')
sender = bms.srs.forward('foo@bar.com','mail.example.com')
sndr = bms.findsrs(StringIO.StringIO(
"""Received: from [1.16.33.86] (helo=mail.example.com)
by bastion4.mail.zen.co.uk with smtp (Exim 4.50) id 1H3IBC-00013b-O9
for foo@bar.com; Sat, 06 Jan 2007 20:30:17 +0000
X-Mailer: "PyMilter-0.8.5"
<%s> foo
MIME-Version: 1.0
Content-Type: text/plain
To: foo@bar.com
From: postmaster@mail.example.com
""" % sender
))
self.assertEqual(sndr,'foo@bar.com')
# def testReject(self): # def testReject(self):
# "Test content based spam rejection." # "Test content based spam rejection."
# milter = TestMilter() # milter = TestMilter()
+48
View File
@@ -0,0 +1,48 @@
import unittest
import doctest
import os
import Milter.utils
from Milter.cache import AddrCache
from Milter.dynip import is_dynip
class AddrCacheTestCase(unittest.TestCase):
def setUp(self):
self.fname = 'test.dat'
def tearDown(self):
os.remove(self.fname)
def testAdd(self):
cache = AddrCache(fname=self.fname)
cache['foo@bar.com'] = None
cache.addperm('baz@bar.com')
cache['temp@bar.com'] = 'testing'
self.failUnless(cache.has_key('foo@bar.com'))
self.failUnless(not cache.has_key('hello@bar.com'))
self.failUnless('baz@bar.com' in cache)
self.assertEquals(cache['temp@bar.com'],'testing')
s = open(self.fname).readlines()
self.failUnless(len(s) == 2)
self.failUnless(s[0].startswith('foo@bar.com '))
self.assertEquals(s[1].strip(),'baz@bar.com')
# check that new result overrides old
cache['temp@bar.com'] = None
self.failUnless(not cache['temp@bar.com'])
def testDomain(self):
fp = open(self.fname,'w')
print >>fp,'spammer.com'
fp.close()
cache = AddrCache(fname=self.fname)
cache.load(self.fname,30)
self.failUnless('spammer.com' in cache)
def suite():
s = unittest.makeSuite(AddrCacheTestCase,'test')
s.addTest(doctest.DocTestSuite(Milter.utils))
s.addTest(doctest.DocTestSuite(Milter.dynip))
return s
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())