Compare commits

..

1 Commits

Author SHA1 Message Date
cvs2svn 4501107768 This commit was manufactured by cvs2svn to create tag 'milter-0_8_0a1'.
Sprout from bmsi 2005-05-31 18:23:49 UTC Stuart Gathman <stuart@gathman.org> 'Development changes since 0.7.2'
Cherrypick from master 2005-05-31 20:39:16 UTC Stuart Gathman <stuart@gathman.org> 'Create Milter package.':
    MANIFEST.in
    milter.html
    setup.py
    spf.py
2005-05-31 20:39:17 +00:00
43 changed files with 1069 additions and 3548 deletions
-340
View File
@@ -1,340 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the 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
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
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
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
Public License instead of this License.
+1 -10
View File
@@ -5,16 +5,10 @@ wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a it with distutils, and generally transformed it from a quick hack to a
real, usable Python extension. real, usable Python extension.
Other contributors (in random order): Other contributors:
Dave MacQuigg
for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter.
Terence Way Terence Way
for providing a Python port of SPF for providing a Python port of SPF
Scott Kitterman
for doing lots of testing and debugging of SPF against draft standard,
and for putting up a web page that validates SPF records using spf.py
Alexander Kourakos Alexander Kourakos
for plugging several memory leaks for plugging several memory leaks
George Graf at Vienna University of Economics and Business Administration George Graf at Vienna University of Economics and Business Administration
@@ -28,9 +22,6 @@ John Draper
then pointing out that it would be easier to just write the MTA in Python. then pointing out that it would be easier to just write the MTA in Python.
Eric S. Johansson Eric S. Johansson
for helpful design discussions while working on camram for helpful design discussions while working on camram
Alex Savguira
for finding bugs with international headers and
suggesting the scan_zip option.
Business Management Systems - http://www.bmsi.com Business Management Systems - http://www.bmsi.com
for hosting the website, and providing paying clients who need milter service for hosting the website, and providing paying clients who need milter service
so I can work on it as part of my day job. so I can work on it as part of my day job.
-136
View File
@@ -1,136 +0,0 @@
Step one. Which DSPAM is right for you?
The DSPAM project makes dspam part of the LDA (Local Delivery Agent).
Pydspam puts dspam into the MTA (Mail Transfer Agent - sendmail with pymilter).
The advantage of doing dspam in the LDA is that any aliasing has already been
resolved. You need only configure mailboxes.
The advantage of doing dspam in the MTA is it can screen an entire
company as a gateway with multiple domains. Unfortunately, this
means you have to tell it about all the aliases that comprise each
account. (Also, pydspam is still uses dspam-2.6.5.2 - the Dspam API
has changed for newer versions.)
If the LDA is right for you, you'll want to use the official Dspam
package. http://www.nuclearelephant.com/projects/dspam/
If the MTA approach is what you want, then pydspam is what you want.
In either case, you will still want pymilter to block forgeries, Windows
executables, etc.
So, lets assume you want to install pymilter, and may or may not
wish to install pydspam.
Step two. Obtaining RPMS.
For basic pymilter you'll need:
python-2.4
milter-0.8.2 (the RH9 rpm should work on Fedora Core - let me know)
sendmail-8.13.x (with milter support enabled)
and for SPF you'll need:
pydns-2.3.0-2.4
and for SRS you'll need:
pysrs-0.30.9-1.py24
I'm pretty sure you will want to have SPF and SRS available.
Step three. Activate basic milter.
Activate the basic milter by editing /etc/mail/sendmail.mc and adding:
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
You can then "make sendmail.cf" and restart sendmail.
Tail /var/log/milter/milter.log while SMTP clients connect to your
sendmail instance. This should show you what the milter is doing.
By default, milter-0.8.2 rejects on SPF fail, except for listed domains
(that are known to be broken). Some admins don't like that, and 0.8.3 will use
the /etc/mail/access database to configure SPF responses. For now,
if you don't like SPF, you can disable spf by replacing "import spf"
with "spf = None" around line 285 in /var/log/milter/bms.py.
Step four. Tweaking the basic config.
Most pymilter configuration is in /etc/mail/pymilter.cfg.
By default, milter scans attachments for executable extensions. You can
turn this off by setting banned_exts to the empty list. There are options
to scan ZIP attachments and rfc822 attachments. When it finds a banned
file type, milter saves the original message in /var/log/milter/save,
and replaces the attachment with a plain text warning message.
Configure hello_blacklist with your own helo name and domains - which
you know cannot legitimately be used by external MTAs.
Configure trusted_relay with your secondary MX servers, if any. These
should also run pymilter with similar policies. (But this isn't
needed for initial testing.)
Configure internal_connect with subnets of your internal SMTP clients.
Internal connections skip SPF testing and other policies.
Configure internal_domains with domains used by your internal SMTP clients.
If they attempt to use any other domain, the attempt is blocked and the
client is logged as a "zombie". Conversely, any attempt by an external
MTA to use one of your internal domains is treated as a forgery and
blocked (a simplified form of local SPF).
Adjust porn_words and spam_words - these block emails with a Subject
containing the listed strings. They can be empty to disable Subject
string blocking.
Advanced SPF configuration.
The sendmail access file, or another readonly database with that
format, can be used for detail spf policy. SPF access policy
record are tagged with "SPF-{Result}:". Results are
Pass, Neutral, Softfail, Fail, PermError. Currently supported
policy keywords are OK, CBV, REJECT. Currently, TempError always
results in TEMPFAIL.
The default policies are set in pymilter.cfg. The defaults
if none of the config options are set are as follows:
SPF-Fail: REJECT
SPF-Softfail: CBV
SPF-Neutral: OK
SPF-PermError: REJECT
SPF-Pass: OK
The tag may be followed by a specific domain. For instance, to
require a Pass from aol.com:
SPF-Neutral:aol.com REJECT
SPF-Softfail:aol.com REJECT
The CBV policy requires a valid HELO name. If the EHLO name is
RFC2822 compliant, then a DSN is sent to the alleged sender. The
template for the DSN is selected according to the SPF result:
Fail: softfail.txt
SoftFail: softfail.txt
Neutral: neutral.txt
PermError: permerror.txt
None: strike3.txt
An SPF-Pass is always accepted by the milter. Domains can be blacklisted
via sendmail in the access file or via a RHS DNS blacklist.
To be continued.
Forthcoming topics:
SRS config
pydspam config
wiretap config
-6
View File
@@ -1,7 +1,6 @@
include COPYING include COPYING
include TODO include TODO
include NEWS include NEWS
include HOWTO
include CREDITS include CREDITS
include README include README
include MANIFEST.in include MANIFEST.in
@@ -10,15 +9,12 @@ include testmime.py
include testbms.py include testbms.py
include testdspam.py include testdspam.py
include rejects.py include rejects.py
include report.py
include bms.py include bms.py
include spf.py include spf.py
include cid2spf.py
include spfquery.py include spfquery.py
include test.py include test.py
include sample.py include sample.py
include test/* include test/*
include doc/*
include Milter/*.py include Milter/*.py
include *.spec include *.spec
include start.sh include start.sh
@@ -26,5 +22,3 @@ include milter.rc
include milter.rc7 include milter.rc7
include milter.cfg include milter.cfg
include rhsbl.m4 include rhsbl.m4
include *.txt
include *.html
Executable
+203
View File
@@ -0,0 +1,203 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc.
# This code is under GPL. See COPYING for details.
import os
import milter
import thread
from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
set_flags, setdbg, setbacklog, settimeout, \
ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \
V1_ACTS, V2_ACTS, CURR_ACTS
try: from milter import QUARANTINE
except: pass
_seq_lock = thread.allocate_lock()
_seq = 0
def uniqueID():
"""Return a sequence number unique to this process.
"""
global _seq
_seq_lock.acquire()
seqno = _seq = _seq + 1
_seq_lock.release()
return seqno
class Milter:
"""A simple class interface to the milter module.
"""
def _setctx(self,ctx):
self.__ctx = ctx
if ctx:
ctx.setpriv(self)
# user replaceable callbacks
def log(self,*msg):
print 'Milter:',
for i in msg: print i,
print
def connect(self,hostname,unused,hostaddr):
"Called for each connection to sendmail."
self.log("connect from %s at %s" % (hostname,hostaddr))
return CONTINUE
def hello(self,hostname):
"Called after the HELO command."
self.log("hello from %s" % hostname)
return CONTINUE
def envfrom(self,f,*str):
"""Called to begin each message.
f -> string message sender
str -> tuple additional ESMTP parameters
"""
self.log("mail from",f,str)
return CONTINUE
def envrcpt(self,to,*str):
"Called for each message recipient."
self.log("rcpt to",to,str)
return CONTINUE
def header(self,field,value):
"Called for each message header."
self.log("%s: %s" % (field,value))
return CONTINUE
def eoh(self):
"Called after all headers are processed."
self.log("eoh")
return CONTINUE
def body(self,unused):
"Called to transfer the message body."
return CONTINUE
def eom(self):
"Called at the end of message."
self.log("eom")
return CONTINUE
def abort(self):
"Called if the connection is terminated abnormally."
self.log("abort")
return CONTINUE
def close(self):
"Called at the end of connection, even if aborted."
self.log("close")
return CONTINUE
# Milter methods which can be invoked from callbacks
def getsymval(self,sym):
return self.__ctx.getsymval(sym)
# If sendmail does not support setmlreply, then only the
# first msg line is used.
def setreply(self,rcode,xcode=None,msg=None,*ml):
return self.__ctx.setreply(rcode,xcode,msg,*ml)
# Milter methods which can only be called from eom callback.
def addheader(self,field,value):
return self.__ctx.addheader(field,value)
def chgheader(self,field,idx,value):
return self.__ctx.chgheader(field,idx,value)
def addrcpt(self,rcpt):
return self.__ctx.addrcpt(rcpt)
def delrcpt(self,rcpt):
return self.__ctx.delrcpt(rcpt)
def replacebody(self,body):
return self.__ctx.replacebody(body)
# When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined.
def quarantine(self,reason):
return self.__ctx.quarantine(reason)
def progress(self):
return self.__ctx.progress()
factory = Milter
def connectcallback(ctx,hostname,family,hostaddr):
m = factory()
m._setctx(ctx)
return m.connect(hostname,family,hostaddr)
def closecallback(ctx):
m = ctx.getpriv()
if not m: return CONTINUE
rc = m.close()
m._setctx(None) # release milterContext
return rc
def envcallback(c,args):
"""Convert ESMTP parms to keyword parameters.
Can be used in the envfrom and/or envrcpt callbacks to process
ESMTP parameters as python keyword parameters."""
kw = {}
for s in args[1:]:
pos = s.find('=')
if pos > 0:
kw[s[:pos]] = s[pos+1:]
return apply(c,args,kw)
def runmilter(name,socketname,timeout = 0):
# This bit is here on the assumption that you will be starting this filter
# before sendmail. If sendmail is not running and the socket already exists,
# libmilter will throw a warning. If sendmail is running, this is still
# safe if there are no messages currently being processed. It's safer to
# shutdown sendmail, kill the filter process, restart the filter, and then
# restart sendmail.
pos = socketname.find(':')
if pos > 1:
s = socketname[:pos]
fname = socketname[pos+1:]
else:
s = "unix"
fname = socketname
if s == "unix" or s == "local":
print "Removing %s" % fname
try:
os.unlink(fname)
except:
pass
# The default flags set include everything
# milter.set_flags(milter.ADDHDRS)
milter.set_connect_callback(connectcallback)
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
milter.set_envfrom_callback(lambda ctx,*str:
ctx.getpriv().envfrom(*str))
# envcallback(ctx.getpriv().envfrom,str))
milter.set_envrcpt_callback(lambda ctx,*str:
ctx.getpriv().envrcpt(*str))
# envcallback(ctx.getpriv().envrcpt,str))
milter.set_header_callback(lambda ctx,fld,val:
ctx.getpriv().header(fld,val))
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
milter.set_abort_callback(lambda ctx: ctx.getpriv().abort())
milter.set_close_callback(closecallback)
milter.setconn(socketname)
if timeout > 0: milter.settimeout(timeout)
# The name *must* match the X line in sendmail.cf (supposedly)
milter.register(name)
start_seq = _seq
try:
milter.main()
except milter.error:
if start_seq == _seq: raise # couldn't start
# milter has been running for a while, but now it can't start new threads
raise milter.error("out of thread resources")
+17 -31
View File
@@ -1,8 +1,7 @@
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc. # Copyright 2001 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details. # This code is under GPL. See COPYING for details.
# A thin OO wrapper for the milter module
import os import os
import milter import milter
@@ -16,8 +15,6 @@ from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
try: from milter import QUARANTINE try: from milter import QUARANTINE
except: pass except: pass
__version__ = '0.8.5'
_seq_lock = thread.allocate_lock() _seq_lock = thread.allocate_lock()
_seq = 0 _seq = 0
@@ -44,7 +41,7 @@ class Milter:
for i in msg: print i, for i in msg: print i,
print print
def connect(self,hostname,family,hostaddr): def connect(self,hostname,unused,hostaddr):
"Called for each connection to sendmail." "Called for each connection to sendmail."
self.log("connect from %s at %s" % (hostname,hostaddr)) self.log("connect from %s at %s" % (hostname,hostaddr))
return CONTINUE return CONTINUE
@@ -106,8 +103,8 @@ class Milter:
return self.__ctx.setreply(rcode,xcode,msg,*ml) return self.__ctx.setreply(rcode,xcode,msg,*ml)
# Milter methods which can only be called from eom callback. # Milter methods which can only be called from eom callback.
def addheader(self,field,value,idx=-1): def addheader(self,field,value):
return self.__ctx.addheader(field,value,idx) return self.__ctx.addheader(field,value)
def chgheader(self,field,idx,value): def chgheader(self,field,idx,value):
return self.__ctx.chgheader(field,idx,value) return self.__ctx.chgheader(field,idx,value)
@@ -143,28 +140,16 @@ def closecallback(ctx):
m._setctx(None) # release milterContext m._setctx(None) # release milterContext
return rc return rc
def dictfromlist(args):
"Convert ESMTP parm list to keyword dictionary."
kw = {}
for s in args:
pos = s.find('=')
if pos > 0:
kw[s[:pos].upper()] = s[pos+1:]
return kw
def envcallback(c,args): def envcallback(c,args):
"""Call function c with ESMTP parms converted to keyword parameters. """Convert ESMTP parms to keyword parameters.
Can be used in the envfrom and/or envrcpt callbacks to process Can be used in the envfrom and/or envrcpt callbacks to process
ESMTP parameters as python keyword parameters.""" ESMTP parameters as python keyword parameters."""
kw = {} kw = {}
pargs = [args[0]]
for s in args[1:]: for s in args[1:]:
pos = s.find('=') pos = s.find('=')
if pos > 0: if pos > 0:
kw[s[:pos].upper()] = s[pos+1:] kw[s[:pos]] = s[pos+1:]
else: return apply(c,args,kw)
pargs.append(s)
return c(*pargs,**kw)
def runmilter(name,socketname,timeout = 0): def runmilter(name,socketname,timeout = 0):
# This bit is here on the assumption that you will be starting this filter # This bit is here on the assumption that you will be starting this filter
@@ -191,13 +176,14 @@ def runmilter(name,socketname,timeout = 0):
# milter.set_flags(milter.ADDHDRS) # milter.set_flags(milter.ADDHDRS)
milter.set_connect_callback(connectcallback) milter.set_connect_callback(connectcallback)
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host)) milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
# For envfrom and envrcpt, we would like to convert ESMTP parms to keyword milter.set_envfrom_callback(lambda ctx,*str:
# parms, but then all existing users would have to include **kw to accept ctx.getpriv().envfrom(*str))
# arbitrary keywords without crashing. We do provide envcallback and # envcallback(ctx.getpriv().envfrom,str))
# dictfromlist to make parsing the ESMTP args convenient. milter.set_envrcpt_callback(lambda ctx,*str:
milter.set_envfrom_callback(lambda ctx,*str: ctx.getpriv().envfrom(*str)) ctx.getpriv().envrcpt(*str))
milter.set_envrcpt_callback(lambda ctx,*str: ctx.getpriv().envrcpt(*str)) # envcallback(ctx.getpriv().envrcpt,str))
milter.set_header_callback(lambda ctx,fld,val: ctx.getpriv().header(fld,val)) milter.set_header_callback(lambda ctx,fld,val:
ctx.getpriv().header(fld,val))
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh()) milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk)) milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom()) milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
@@ -217,6 +203,6 @@ def runmilter(name,socketname,timeout = 0):
raise milter.error("out of thread resources") raise milter.error("out of thread resources")
__all__ = globals().copy() __all__ = globals().copy()
for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'): for priv in ('os','milter','thread','factory','_seq','_seq_lock'):
del __all__[priv] del __all__[priv]
__all__ = __all__.keys() __all__ = __all__.keys()
+21 -48
View File
@@ -1,19 +1,9 @@
# 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.
# Send DSNs, do call back verification,
# and generate DSN messages from a template
import smtplib import smtplib
import spf import spf
import socket import socket
from email.Message import Message from email.Message import Message
import Milter
nospf_msg = """Subject: Critical mail server configuration error nospf_msg = """This is an automatically generated Delivery Status Notification.
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY. THIS IS A WARNING MESSAGE ONLY.
@@ -75,12 +65,11 @@ If you need further assistance, please do not hesitate to
contact me again. contact me again.
Kind regards, Kind regards,
Stuart D. Gathman
postmaster@%(receiver)s postmaster@%(receiver)s
""" """
softfail_msg = """Subject: SPF softfail (POSSIBLE FORGERY) softfail_msg = """
This is an automatically generated Delivery Status Notification. This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY. THIS IS A WARNING MESSAGE ONLY.
@@ -96,15 +85,12 @@ Received-SPF: %(spf_result)s
""" """
def send_dsn(mailfrom,receiver,msg=None): def send_dsn(mailfrom,receiver,msg=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.
Receiver is the MTA sending the DSN.
Return None for success or (code,msg) for failure."""
user,domain = mailfrom.split('@') user,domain = mailfrom.split('@')
q = spf.query(None,None,None) q = spf.query(None,None,None)
mxlist = q.dns(domain,'MX') mxlist = q.dns(domain,'MX')
if not mxlist: if not mxlist:
mxlist = (0,domain), # fallback to A record when no MX mxlist = (0,domain),
else: else:
mxlist.sort() mxlist.sort()
smtp = smtplib.SMTP() smtp = smtplib.SMTP()
@@ -113,13 +99,10 @@ def send_dsn(mailfrom,receiver,msg=None):
smtp.connect(host) smtp.connect(host)
code,resp = smtp.helo(receiver) code,resp = smtp.helo(receiver)
# some wiley spammers have MX records that resolve to 127.0.0.1 # some wiley spammers have MX records that resolve to 127.0.0.1
a = resp.split() if resp.split()[0] == receiver:
if not a: return (553,'Fraudulent MX for %s' % domain)
return (553,'MX for %s has no hostname in banner: %s' % (domain,host))
if a[0] == receiver:
return (553,'Fraudulent MX for %s: %s' % (domain,host))
if not (200 <= code <= 299): if not (200 <= code <= 299):
raise smtplib.SMTPHeloError(code, resp) raise SMTPHeloError(code, resp)
if msg: if msg:
try: try:
smtp.sendmail('<>',mailfrom,msg) smtp.sendmail('<>',mailfrom,msg)
@@ -129,7 +112,7 @@ def send_dsn(mailfrom,receiver,msg=None):
else: # CBV else: # CBV
code,resp = smtp.docmd('MAIL FROM: <>') code,resp = smtp.docmd('MAIL FROM: <>')
if code != 250: if code != 250:
raise smtplib.SMTPSenderRefused(code, resp, '<>') raise SMTPSenderRefused(code, resp, '<>')
code,resp = smtp.rcpt(mailfrom) code,resp = smtp.rcpt(mailfrom)
if code not in (250,251): if code not in (250,251):
return (code,resp) # permanent error return (code,resp) # permanent error
@@ -138,9 +121,9 @@ def send_dsn(mailfrom,receiver,msg=None):
except smtplib.SMTPRecipientsRefused,x: except smtplib.SMTPRecipientsRefused,x:
return x.recipients[mailfrom] # permanent error return x.recipients[mailfrom] # permanent error
except smtplib.SMTPSenderRefused,x: except smtplib.SMTPSenderRefused,x:
return x.args[:2] # does not accept DSN return x # does not accept DSN
except smtplib.SMTPDataError,x: except smtplib.SMTPDataError,x:
return x.args # permanent error return x # permanent error
except smtplib.SMTPException: except smtplib.SMTPException:
pass # any other error, try next MX pass # any other error, try next MX
except socket.error: except socket.error:
@@ -148,48 +131,38 @@ def send_dsn(mailfrom,receiver,msg=None):
smtp.close() smtp.close()
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): def create_msg(q,rcptlist,origmsg):
"Create a DSN message from a template. Template must be '\n' separated."
heloname = q.h heloname = q.h
sender = q.s sender = q.s
connectip = q.i connectip = q.i
receiver = q.r receiver = q.r
sender_domain = q.o sender_domain = q.o
result = q.result
perm_error = q.perm_error
rcpt = '\n\t'.join(rcptlist) rcpt = '\n\t'.join(rcptlist)
try: subject = origmsg['Subject'] try: subject = origmsg['Subject']
except: subject = '(none)' except: subject = '(none)'
try: try:
spf_result = origmsg['Received-SPF'] spf_result = origmsg['Received-SPF']
if not spf_result.startswith('softfail'):
spf_result = None
except: spf_result = None except: spf_result = None
msg = Message() msg = Message()
msg.add_header('To',sender) msg.add_header('To',sender)
msg.add_header('From','postmaster@%s'%receiver) msg.add_header('From','postmaster@%s'%receiver)
msg.add_header('Auto-Submitted','auto-generated (configuration error)') msg.add_header('Auto-Submitted','auto-generated (configuration error)')
msg.add_header('X-Mailer','PyMilter-'+Milter.__version__)
msg.set_type('text/plain') msg.set_type('text/plain')
if spf_result:
if not template: msg.add_header('Subject','SPF softfail (POSSIBLE FORGERY)')
if spf_result and spf_result.startswith('softfail'): msg.set_payload(softfail_msg % locals())
template = softfail_msg
else: else:
template = nospf_msg msg.add_header('Subject','Critical mail server configuration error')
hdrs,body = template.split('\n',1) msg.set_payload(nospf_msg % locals())
for ln in hdrs.splitlines():
name,val = ln.split(':',1)
msg.add_header(name,(val % locals()).strip())
msg.set_payload(body % locals())
return msg return msg
if __name__ == '__main__': if __name__ == '__main__':
q = spf.query('192.168.9.50', q = spf.query('192.168.9.50',
'SRS0=pmeHL=RH=bmsi.com=stuart@bmsi.com', 'SRS0=pmeHL=RH=bmsi.com=stuart@bmsi.com',
'bmsred.bmsi.com',receiver='mail.bmsi.com') 'bmsred.bmsi.com',receiver='mail.bmsi.com')
msg = create_msg(q,['charlie@jsconnor.com'],None,None) msg = create_msg(q,'charlie@jsconnor.com')
print msg.as_string() #print msg.as_string()
# print send_dsn(f,msg.as_string()) # print send_dsn(f,msg.as_string())
print send_dsn(q.s,'mail.bmsi.com',msg.as_string()) print send_dsn(q.s,'mail.bmsi.com',msg.as_string())
-6
View File
@@ -1,9 +1,3 @@
# 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.
# Heuristically determine whether a domain name is for a dynamic IP.
# examples we don't yet recognize: # examples we don't yet recognize:
# #
# wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810) # wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810)
-43
View File
@@ -1,48 +1,5 @@
Here is a history of user visible changes to Python milter. Here is a history of user visible changes to Python milter.
0.8.5 Simple trusted_forwarder implementation.
Fix access_file neutral policy
Move Received-SPF header to beginning of headers
Supply keyword info for all results in Received-SPF header.
Move guessed SPF result to separate header
Activate smfi_insheader only when SMFIR_INSHEADER defined
Handle NULL MX in spf.py
in-process GOSSiP server support (to be extended later)
Expire CBV cache and renew auto-whitelist entries
0.8.4 Auto-whitelist recipients of outgoing email.
Fix SPF policy via sendmail access map (case insensitive keys).
Train screener on whitelisted messages
Optional idx parameter to addheader to invoke smfi_insheader
Activate progress API when SMFIR_PROGRESS defined
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
spf_accept_fail option for braindead SPF senders
(treats fail like softfail)
Option to set SPF policy via sendmail access map.
Option to supply Sender header from MAIL FROM when missing.
Consider SMTP AUTH connections internal.
Send DSN for SPF errors corrected by extended processing.
Send DSN before SCREENED mail is quarantined
Use logging package to keep log lines atomic.
0.8.2 Strict processing limits per SPF RFC
Fixed several parsing bugs under RFC
Support official IANA SPF record (type99)
Honeypot support (requires pydspam-1.1.9)
Extended SPF processing results beyond strict RFC limits
Support original SES for bounce protection (requires pysrs-0.30.10)
Callback exception processing option in milter module
Handle corrupt ZIP attachments
0.8.1 Fix zip in zip loop in mime.py
Fix HeaderParseError in bms.py header callback
Check internal_domains for outgoing mail
Fix inconsistent results from send_dsn
0.8.0 Move Milter module to subpackage.
DSN support for Three strikes rule and SPF SOFTFAIL
Move /*mime*/ and dynip to Milter subpackage
Fix SPF unknown mechanism list not cleared
Make banned extensions configurable.
Option to scan zipfiles for bad extensions.
Properly log pydspam exceptions
0.7.3 Experimental release with python2.4 support
0.7.2 Return unknown for invalid ip address in mechanism 0.7.2 Return unknown for invalid ip address in mechanism
Recognize dynamic PTR names, and don't count them as authentication. Recognize dynamic PTR names, and don't count them as authentication.
Three strikes and yer out rule. Three strikes and yer out rule.
+39 -20
View File
@@ -1,46 +1,60 @@
Require signed MFROM for all incoming bounces when signing all outgoing mail -
except from trusted relays.
Send DSN for permerror before processing extended result. An additional
DSN may be sent based on extended result.
Rescind whitelist for banned extensions, in case sender is infected.
Train honeypot on error only.
Find rfc2822 policy for MFROM quoting.
Support explicit errors for SPF policy in access file:
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
Defer TEMPERROR in SPF evaluation - give precedence to security Defer TEMPERROR in SPF evaluation - give precedence to security
(only defer for PASS mechanisms). (only defer for PASS mechanisms).
Allow multiple recipients for MAIL FROM: <> by default.
Option to add Received-SPF header, but never reject on SPF. Option to add Received-SPF header, but never reject on SPF.
I think the above will handle this.
Option to configure banned extension list for mime.py. Default to empty.
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.
Checking in mime.py;
/bms/cvs/milter/mime.py,v <-- mime.py
new revision: 1.56; previous revision: 1.55
done
Checking in spf.py;
/bms/cvs/milter/spf.py,v <-- spf.py
new revision: 1.18; previous revision: 1.17
done
Checking in testmime.py;
/bms/cvs/milter/testmime.py,v <-- testmime.py
new revision: 1.19; previous revision: 1.18
Auto whitelist based on outgoing email - perhaps with magic subject
or recipient prefix.
Can't output messages with malformed rfc822 attachments. Can't output messages with malformed rfc822 attachments.
Use python exceptions in SPF to cleanly handle unknown and error results.
Example malformed SPF:
onvunvuvvx.usafisnews.org text "v=spf1 mx ptr ip4:207.44.199.970 -all"
Move milter,Milter,mime,spf modules to pymilter Move milter,Milter,mime,spf modules to pymilter
milter package will have bms.py application milter package will have bms.py application
Support SMTP AUTH and disable SPF checks when connection is authorized.
Web admin interface Web admin interface
Check valid domains allowed by internal senders to detect PCs infected
with spam trojans.
Do CBV (callback verification) for mail with no published SPF record.
message log for automated stats and blacklisting message log for automated stats and blacklisting
Skip dspam when SPF pass? NO Skip dspam when SPF pass?
Report 551 with rcpt on SPF fail? Report 551 with rcpt on SPF fail?
check spam keywords with character classes, e.g. check spam keywords with character classes, e.g.
{a}=[a@ãä], {i}=[i1í], {e}=[eë], {o}=[o0ö] {a}=[a@ãä], {i}=[i1í], {e}=[eë], {o}=[o0ö]
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
forwarder accounts, and a util provides a special local alias for the forwarder accounts, and a util provides a special local alias for the
user to give to the forwarder. (Or user just adds arbitrary alias user to give to the forwarder. Alias only works for mail from that
unique to that forwarder to a database.) Alias only works for mail from that
forwarder. Milter gets forwarder domain from alias and uses it to forwarder. Milter gets forwarder domain from alias and uses it to
SPF check forwarder. SPF check forwarder.
Another special dspam user, 'honeypot', can be listed in innoculations.
All email to those addresses is treated as known spam.
Framework for modular Python milter components within a single VM. Framework for modular Python milter components within a single VM.
Python milters can be already be composed through sendmail by running each in Python milters can be already be composed through sendmail by running each in
a separate process. However, a significant amount of memory is wasted a separate process. However, a significant amount of memory is wasted
@@ -49,7 +63,8 @@ is cumbersome (e.g., adding mail headers, writing external files).
Backup copies for outgoing/incoming mail. Backup copies for outgoing/incoming mail.
Copy incoming wiretap mail, even though sendmail alias works perfectly Allow multiple wiretap groups, each with its own destination. Perhaps
also 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.
Provide a way to reload milter.cfg without stopping/restarting milter. Provide a way to reload milter.cfg without stopping/restarting milter.
@@ -63,6 +78,10 @@ Keep separate ismodified flag for headers and body. This is important
when rejecting outgoing mail with viruses removed (so as not to when rejecting outgoing mail with viruses removed (so as not to
embarrass yourself), and also removing Received headers with hidepath. embarrass yourself), and also removing Received headers with hidepath.
Wrap smfi_setbacklog(int) - but it is only available in sendmail >= 8.12.3,
so how can we detect whether to wrap it?
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.
+138 -741
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

-173
View File
@@ -1,173 +0,0 @@
Title: Recent Changes
<h2> Recent Changes </h2>
Python milter has been moved to
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
project</a> for development and release downloads.
<h3> 0.8.5 </h3>
Release 0.8.5 fixes some build bugs reported by Stephen Figgins. It
fixes many small things, like not auto-whitelisting recipients of
outgoing mail when the subject contains "autoreply:". There is a
simple trusted forwarder implementation. If you have more than
2 or so forwarders, we will need a way to "compile" SPF records into an
IP set and TTL for it to be efficient (like libspf2 does).
<h3> GOSSiP </h3>
An alpha release of <a href="pygossip.html">pygossip</a> has been commited to
CVS, module pygossip. A version of the bms.py milter has been commited to CVS
which supports calling GOSSiP to track domain reputation in a local database.
<h3> New website design </h3>
Hey, I'm no artist, so I just used the
<a href="http://ht2html.sourceforge.net/">ht2html</a> package
by <a href="http://barry.wooz.org/">Barry Warsaw</a>. The mascot
is by <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>,
or maybe his wife. I chose Maxwell's daemon because it tirelessly
and invisibly sorts molecules, just as milters sort mail.
Christian has also provided a fun
<a href="http://alphard.ethz.ch/hafner/PPS/PPS2002/Maxwell/simulation.htm">
simulation</a> that lets you try your hand at sorting molecules.
<h3> 0.8.4 </h3>
Release 0.8.4 makes configuring SPF policy via access.db actually work.
The honeypot idea is enhanced by auto-whitelisting recipients of
email sent from selected domains. Whitelisted messages are then used
to train the honeypot. This makes the honeypot screener entirely self
training. The smfi_progress() API is now automatically supported when present.
An optional idx parameter to milter.addheader() invokes smfi_insheader().
<h3> 0.8.3 </h3>
Release 0.8.3 uses the standard logging module, and supports configuring
more detailed SPF policy via the sendmail access map. SMTP AUTH connections
are considered INTERNAL. Preventing forgery between internal domains is
just a matter of specifying the user-domain map - I'll define something
for the next version. We now send DSNs when mail is quarantined (rejecting
if DSN fails) and for SPF syntax errors (PermError). There is an
experimental option to add a Sender header when it is missing and the From
domain doesn't match the MAIL FROM domain. Next release, we may start
renaming and replacing an existing Sender header when neither it nor the
From domain matches MAIL FROM. Since bogus MAIL FROMs are rejected
(to varying degrees depending on the configured SPF policy), and
both Sender and From and displayed by default in many email clients,
this provides some phishing protection without rejecting mail based
on headers.
<h3> 0.8.2 </h3>
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
in line with the newly official RFC. It adds
<a href="http://ses.codeshare.ca/">SES</a>
support (the original SES without body hash) for pysrs-0.30.10, and honeypot
support for pydspam-1.1.9. There is a new method in the base milter module.
milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or
TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
<h3> 0.8.0 </h3>
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
release. It supports Python-2.4, and provides an option to accept mail
that gets an SPF softfail or fails the 3 strikes rule, provided the
alleged sender accepts a DSN explaining the problem. Python-2.3 is
no longer supported by the reworked mime.py module, although API changes
could be backported. There are too many incompatible changes to the
python email package.
<h3> Older Releases </h3>
Release 0.7.2 tightens the authentication screws with a "3 strikes and
you're out" policy. A sender must have a valid PTR, HELO, or SPF record
to send email. Specific senders can be whitelisted using the
"delegate" option in the spf configuration section by adding a
default SPF record for them. The PTR and HELO are required
by RFC anyway, so this is not an unreasonable requirement.
There is now a coherent policy for an SPF softfail result. A softfail
is accepted if there is a valid PTR or HELO, or if the domain
is listed in the "accept_softfail" option of the spf configuration section.
A neutral result is accepted by default if there is a valid PTR or
HELO, (and the SPF record was not guessed), unless the domain is listed in the
"reject_neutral" option. Common forms of PTR records for dynamic IPs are
recognized, and do not count as a valid PTR. This does not prevent anyone
from sending mail from a dynamic IP - they just need to configure a
valid HELO name or publish an SPF record.
<p>
As SPF adoption continues to rise, forged spam is not getting through. So
spammers are publishing their SPF records as predicted. The 0.7.2 RPM
now provides the <code>rhsbl</code> sendmail hack so that spammer domains
can be blacklisted. With the RPM installed, add a line like the following
to your <code>sendmail.mc</code>.
<pre>
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
</pre>
<p>
Of course, spammers are now starting to register
throwaway domains. The next thing we need is a custom DNS server,
in Python, that
can recognize patterns. For instance, one spammer registers ded304.com,
ded305.com, ded306.com, etc. We also need the custom DNS server to
let SPF classic clients check SES (which will be part of pysrs).
The <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
framework provides a custom DNS server - but I
would like a smaller implementation for our use.
<p>
The RPM for release 0.7.0 moves the config file and socket locations to
/etc/mail and /var/run/milter respectively. We now parse Microsoft CID records
- but only hotmail.com uses them. They seem to have applied for a patent on
the brilliant idea of examining the mail headers to see who the message is
from. We aren't doing that here, so not to worry - but I am not a lawyer, so
if you are worried, change spf.py around line 626 to return None instead of
calling CIDParser(). There is a new option to reject mail with no PTR
and no SPF.
<p>
Microsoft is pushing an anti-opensource license for their pending patent
along with their sender-ID proposal before the IETF.
It is royalty free - but requires anyone distributing a binary they've
compiled from source to sign a license agreement. The Apache Software
Foundation <a
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
the problem with sender-ID</a>, and Debian <a
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
software in general</a> and the <a
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
particular</a>, Python milter will not be able to implement sender-ID in its
current form. This was, no doubt, Microsoft's intent all along.
<p>
Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I
recommend ignoring it and continuing to implement and improve SPF until a
working and unencumbered proposal for RFC2822 headers surfaces.
<p>
<a href="http://openspf.com">
<img src="SPF.gif" align=left alt="SPF logo"></a>
Release 0.6.6 adds support for <a href="http://openspf.com/">SPF</a>,
a protocol to prevent forging of the envelope from address.
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
The included spf.py module is an updated version of the original 1.6
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
The updated version tracks the draft RFC and test suite.
<p>
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
dspam bayes filter project</a> and <a href="dspam.html">
packaged it for python</a>.
Release 0.6.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers.
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
-55
View File
@@ -1,55 +0,0 @@
Title: Credits
<h1> CREDITS </h1>
<a href="mailto:Jim Niemira <urmane@urmane.org>">Jim Niemira</a>
wrote the original C module and some quick
and dirty python to use it.
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">Stuart D. Gathman</a>
took that kludge and added threading and context objects to it, wrote a proper
OO wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a
real, usable Python extension.
<h2>Other contributors (in random order):</h2>
<dl>
<dt> <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>
<dd>for the pymilter mascot image of
<a href="http://maxwelld.netfirms.com/">
Maxwell's daemon</a>
<dt>Stephen Figgins
<dd>for reporting problems building with sendmail-8.12, and when
building milter.so for the first time.
<dt>Dave MacQuigg
<dd>for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter.
<dt>Terence Way
<dd>for providing a Python port of SPF
<dt>Scott Kitterman
<dd>for doing lots of testing and debugging of SPF against draft standard,
and for putting up a <a href="http://www.kitterman.com/spf/validate.html">
web page that validates SPF</a> records using spf.py
<dt>Alexander Kourakos
<dd>for plugging several memory leaks
<dt>George Graf at Vienna University of Economics and Business Administration
<dd>for handling None passed to setreply and chgheader.
<dt>Deron Meranda
<dd>for IPv6 patches
<dt>Jason Erikson
<dd>for handling NULL hostaddr in connect callback.
<dt>John Draper
<dd>for porting Python milter to OpenBSD, and starting to work on tutorials
then pointing out that it would be easier to just write the MTA in Python.
<dt>Eric S. Johansson
<dd>for helpful design discussions while working on camram
<dt>Alex Savguira
<dd>for finding bugs with international headers and
suggesting the scan_zip option.
<dt><a href="http://www.bmsi.com">Business Management Systems</a>
<dd>for hosting the website, and providing paying clients who need milter
service so I can work on it as part of my day job.
</dl>
If I have left anybody out, send me a reminder:
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">stuart@bmsi.com</a>
-71
View File
@@ -1,71 +0,0 @@
Title: Python Milter Log Documentation
<style>
DT { font-weight: bolder; padding-top: 1em }
</style>
<h1> Milter Log Documentation </h1>
The milter log from the bms.py application has a variety of "tags" in it that
indicate what it did.
<dl>
<dt> DSPAM: honeypot SCREENED
<dd> message was quarantined to the honeypot quarantine
<dt> REJECT: hello SPF: fail 550 access denied
<dt> REJECT: hello SPF: softfail 550 domain in transition
<dt> REJECT: hello SPF: neutral 550 access neither permitted nor denied
<dd> message was rejected because there was an SPF policy for the
HELO name, and it did not pass.
<dt> CBV: sender-17-44662668-643@bluepenmagic.com
<dd> we performed a call back verification
<dt> dspam
<dd> dspam identifier was added to the message
<dt> REJECT: spam from self: jsconnor.com
<dd> message was reject because HELO was us (jsconnor.com)
<dt> INNOC: richh
<dd> message was used to update richh's dspam dictionary
<dt> HONEYPOT: michaelb@jsconnor.com
<dd> message was sent to a honeypot address (michaelb@jsconnor.com), the
message was added to the honeypot dspam dictionary as spam
<dt> REJECT: numeric hello name: 63.217.19.146
<dd> message was rejected because helo name was invalid (numeric)
<dt> eom
<dd> message was successfully received
<dt> TEMPFAIL: CBV: 450 No MX servers available
<dd> we tried to do a call back verification but could not look up
MX record, we told the sender to try again later
<dt> CBV: info@emailpizzahut.com (cached)
<dd> call back verification was needed, we had already done it recently
<dt> abort after 0 body chars
<dd> sender hung up on us
<dt> REJECT: SPF fail 550 SPF fail: see
http://openspf.com/why.html?sender=m.hendersonxk@163.net&ip=213.47.161.100
<dd> message was reject because its sender's spf policy said to
<dt> REJECT: Subject: Cialis - No prescription needed!
<dd> message was rejected because its subject contained a bad expression
<dt> DSPAM: tonyc tonyc@jsconnor.com
<dd> message was sent to tonyc@jsconnor.com and it was identified as spam
and placed in the tonyc dspam quarantine
<dt> REJECT: CBV: 550 calvinalstonis@ix.netcom.com...User unknown
<dt> REJECT: CBV: 553 sorry, that domain isn't in my list
<dt> REJECT: CBV: 554 delivery error: dd This user doesn't have an account
<dd> message was rejected because call back verification gave us a fatal
error
</dl>
Please add more tags to this list if you know of any. Thanks.
-237
View File
@@ -1,237 +0,0 @@
Title: Python Milter Mail Policy
<h1> Python Milter Mail Policy </h1>
These are the policies implemented by the <code>bms.py</code> milter
application. The milter and Milter modules do not implement any policies
by themselves. Eventually, I'll get the bms.py milter moved to its
own package.
<h3> Classify connection </h3>
When the SMTP client connects, the connection IP address is
saved for later verification, and the connection
is classified as INTERNAL or EXTERNAL by matching the ip
address against the <code>internal_connect</code> configuration.
IP addresses with no PTR, and PTR names that look like
the kind assigned to dynamic IPs (as determined by a heuristic
algorithm) are flagged as DYNAMIC. IPs that match the
<code>trusted_relay</code> configuration are flagged as TRUSTED.
<p>
Examples from the log file (<i>not</i> the SMTP error message returned):
<pre>
2005Jul29 13:56:53 [71207] connect from p50863492.dip0.t-ipconnect.de at ('80.134.52.146', 1858) EXTERNAL DYN
2005Jul29 18:10:15 [74511] connect from foopub at ('1.2.3.4', 46513) EXTERNAL TRUSTED
2005Jul29 14:41:00 [71805] connect from foobar at ('192.168.0.1', 41205) INTERNAL
2005Jul29 14:41:15 [71806] connect from cncln.online.ln.cn at ('218.25.240.137', 35992) EXTERNAL
</pre>
<p>
Certain obviously evil PTR names are blocked at this point:
"localhost" (when IP is not 127.*) and ".".
<pre>
2005Jul29 14:49:50 [71918] connect from localhost at ('221.132.0.6', 50507) EXTERNAL
2005Jul29 14:49:50 [71918] REJECT: PTR is localhost
</pre>
<h3> HELO Check </h3>
The HELO name provided by the client is saved for later verification
(for example by SPF). We could validate the HELO at this point
by verifying that an A record for the HELO name matches the connect ip.
However, currently we only block certain obvious problems.
HELO names that look like an IP4 address
and ones that match the <code>hello_blacklist</code> configuration
are immediately rejected. The hello_blacklist typically contains
the current MTAs own HELO name or email domains.
Clients that attempt to skip HELO are immediately rejected.
<pre>
2005Jul29 18:10:15 [74512] hello from example.com
2005Jul29 18:10:15 [74512] REJECT: spam from self: example.com
2005Jul29 18:17:09 [74581] hello from 80.191.244.69
2005Jul29 18:17:09 [74581] REJECT: numeric hello name: 80.191.244.69
</pre>
<h3> MAIL FROM Check </h3>
Before calling our milter, sendmail checks a DNS blacklist to
block banned sender domains. We never see a blocked domain.
<p>
The MAIL FROM address is saved for possible use by the smart-alias
feature. First, the <code>internal_domains</code> is used for
a simple screening if defined. If the MAIL FROM for an INTERNAL connection
is NOT in <code>internal_domains</code>, then it is rejected (the
PC is most likely infected and attempting to send out spam).
If the MAIL FROM for an EXTERNAL connection IS in
<code>internal_domains</code>, then the message is immediately rejected.
This is quick and effective for most small company MTAs. For more
complex mail networks, it is too simplistic, and should not be defined.
SPF will handle the complex cases.
<h4> wiretap </h4>
The wiretap feature can screen and/or monitor mail to/from certain
users. If the MAIL FROM is being wiretapped, the recipients are
altered accordingly.
<!--table-stop-->
<h2> SPF check </h2>
Finally, the MAIL FROM, connect IP, and HELO name are checked against
any SPF records published via DNS for the alleged sender (MAIL FROM).
If there is no SPF record, we check for a local substitute under the
domain defined in the <code>[spf]delegate</code> configuration.
Further checks depend on the result.
<table border=1>
<tr><th>NONE</th><td>
If there is no SPF record (official or delegated), then we
initiate a "three strikes and your out" regime, which looks for
<b>some</b> form of validated identification.
<ol>
<li>We try a "best guess" SPF record of "v=spf1 a/24 mx/24 ptr". If this
passes, good.
<li> We try to validate the HELO name. First check for an SPF record.
Otherwise, check whether the connect IP matches any A record for
the HELO name, or any A record for any MX name for the HELO name,
or is at least in the same /24 subnet as any of the above.
(In other words, a HELO SPF "best guess" of "v=spf1 a/24 mx/24".)
If so, good. We consider the HELO validated. If the HELO SPF
check fails, we reject the email.
</ol>
<pre>
2005Jul30 19:45:16 [93991] connect from [221.200.41.54] at ('221.200.41.54', 3581) EXTERNAL DYN
2005Jul30 19:45:18 [93991] hello from adelphia.net
2005Jul30 19:45:19 [93991] mail from <wendy.stubbsua@link-it.com> ()
2005Jul30 19:45:19 [93991] REJECT: hello SPF: fail 550 access denied
</pre>
<ol>
<li> If there is a validated PTR name, and it doesn't look
like a dynamic name, good. We consider the connection validated.
</ol>
If any of the above can be validated, we continue on.
If none of the above can be validated, and the <code>[SPF]reject_noptr</code>
option is true, we reject the message immediately with the explanation
that we need some form of valid identification before we accept an email.
If <code>[SPF]reject_noptr</code> is false, we flag the message as
needing Call Back Validation.
The Call Back Valildation sends a DSN to the purported sender informing
them of the lack of identification. If the message is legitimate, the
sender needs to know that their email setup is broken and should be corrected.
If the message is forged, the sender is informed of the forgery,
and their need to publish an SPF record or at least use a valid HELO name.
If the purported sender does not accept the DSN,
then the message is rejected. The CBV status is cached to avoid
annoying the purported sender with too many DSNs. Currently, the DSN
is repeated to the same sender once per month.
<p>
In this example, although 3com.com has no SPF record, we assume that
any legitimate mail from them will at least have a valid HELO or PTR.
<pre>
2005Jul30 23:52:03 [96777] connect from [222.252.233.200] at ('222.252.233.200', 29934) EXTERNAL DYN
2005Jul30 23:52:03 [96777] hello from 3mail.3com.com
2005Jul30 23:52:04 [96777] mail from <etec_nic_family@3mail.3com.com> ()
2005Jul30 23:52:04 [96777] REJECT: no PTR, HELO or SPF
</pre>
</td></tr>
<tr><th>PASS</th><td>
A pass result normally lets the email continue on, but the domain is
tracked for reputation (and may be blocked), and may skip content scanning if
it matches a whitelist.
<pre>
2005Jul24 17:44:26 [2104] mail from <gnucash-devel-bounces@gnucash.org> ('SIZE=4410',)
2005Jul24 17:44:26 [2104] Received-SPF: pass (mail.bmsi.com: domain of gnucash.org
designates 204.107.200.65 as permitted sender)
client-ip=204.107.200.65; envelope-from=gnucash-devel-bounces@gnucash.org; helo=cvs.gnucash.org;
</pre>
</td></tr>
<tr><th>NEUTRAL</th><td>
A neutral result normally lets the email continue on, but the domain is not
tracked for reputation or matched against any whitelists.
Highly forged domains listed in <code>[SPF]reject_neutral</code> are
rejected.
<pre>
2005Jul24 17:41:37 [2070] connect from cp500627-a.dbsch1.nb.home.nl at ('84.27.225.3', 3465) EXTERNAL
2005Jul24 17:41:37 [2070] hello from cp500627-a.dbsch1.nb.home.nl
2005Jul24 17:41:38 [2070] mail from <nwarjejkw@yahoo.com> ()
2005Jul24 17:41:38 [2070] REJECT: SPF neutral for nwarjejkw@yahoo.com
</pre>
</td></tr>
<tr><th>SOFTFAIL</th><td>
A softfail result normally lets the email continue on, but the domain is not
tracked for reputation or matched against any whitelists. Furthermore,
the message is flagged as needing Call Back Validation,
and the highly forged domains listed in <code>[SPF]reject_neutral</code> are
rejected as well.
<p>
At present, we also require a valid HELO or PTR to avoid rejecting
a softfail. But this should probably change to only require a
successful CBV.
<p>
The Call Back Valildation sends a DSN to the purported sender informing
them of the softfail. If the message is legitimate, the sender needs
to know about the softfail so that their email setup can be corrected.
If the message is forged, the sender is informed of the forgery, confirming
that SPF is protecting their reputation and encouraging a rapid transition
to a strict policy. If the purported sender does not accept the DSN,
then the message is rejected. The CBV status is cached to avoid
annoying the purported sender with too many DSNs. Currently, the DSN
is repeated to the same sender once per month.
<pre>
2005Jul24 15:41:33 [801] mail from <Aitp@horafeliz.com> ()
2005Jul24 15:41:33 [801] Received-SPF: softfail (mail.bmsi.com: transitioning domain of horafeliz.com
does not designate 221.184.83.185 as permitted sender)
client-ip=221.184.83.185; envelope-from=Aitp@horafeliz.com;
helo=p8185-ipad30funabasi.chiba.ocn.ne.jp;
2005Jul24 15:41:33 [801] rcpt to <david@example.com> ()
2005Jul24 15:41:35 [801] Subject: Microsoft, Adobe, Macromedia, Corel software. Up to 80% discount.
2005Jul24 15:41:35 [801] X-Mailer: Microsoft Outlook, Build 10.0.2605
2005Jul24 15:41:35 [801] CBV: Aitp@horafeliz.com
2005Jul24 15:41:38 [801] REJECT: CBV: 550 <Aitp@horafeliz.com>: User unknown
</pre>
</td></tr>
<tr><th>FAIL</th><td>
The message is rejected with a reference the SPF why page.
<pre>
2005Jul30 19:53:27 [94070] connect from [212.70.52.16] at ('212.70.52.16', 3192) EXTERNAL DYN
2005Jul30 19:53:27 [94070] hello from winzip.com
2005Jul30 19:53:27 [94070] mail from <dan@winzip.com> ()
2005Jul30 19:53:27 [94070] REJECT: SPF fail 550 SPF fail:
see http://openspf.com/why.html?sender=dan@winzip.com&ip=212.70.52.16
</pre>
</td></tr>
<tr><th>PERMERROR</th><td>
Permanent errors were called "unknown", and are still show that way
in the log. The message is rejected. Previously, we enabled "lax" parsing
of the SPF record, but rejecting is better because it informs the
sender about their problem. The next milter version will
look for a local substitute SPF record (as for a missing SPF record)
before rejecting. This will inform the sender of their problem, but
also let the receiver install a temporary workaround.
<pre>
2005Jul24 18:05:37 [2312] mail from <b-mihdbcgaacaa-becibijh-000-@msg.euxiphipops.com> ()
2005Jul24 18:05:37 [2312] REJECT: SPF unknown 550 SPF Permanent Error:
include mechanism missing domain: include
</pre>
The SPF record for msg.euxiphipops.com looked like this at the time of the
above error:
<pre>
msg.euxiphipops.com TXT "v=spf1 mx ptr a include"
</pre>
</td></tr>
<tr><th>TEMPERROR</th><td>
Temporary errors result in a 451 "Try again later" response. The sender
should retry the message at a later time.
<pre>
2005Jul24 07:33:13 [29846] mail from <quickenloans@rate.quicken.com> ('SIZE=73775', 'BODY=8BITMIME')
2005Jul24 07:33:43 [29846] TEMPFAIL: SPF error 450 SPF Temporary Error: DNS Timeout
</pre>
</td></tr>
</table>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

-99
View File
@@ -1,99 +0,0 @@
Title: Requirements
<h2> Requirements </h2>
<menu>
<li> While the miltermodule will work with python 1.5, you probably
want to use python 2.0 or better. The python code uses a number of
python 2 features. The email support requires python 2.4.
<li> Python must be configured with thread support. This is because
pymilter uses sendmail's libmilter which requires thread support.
<li> You must compile sendmail with libmilter enabled. In versions of
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
is not installed by default.
Sendmail 8.12 still does not enable libmilter by default. You must
explicitly select the "MILTER" option when compiling.
<li> When compiling Python milter against sendmail versions earlier than
8.13, you must set MAX_ML_REPLY to 1 in setup.py. There is no way to tell from
the libmilter includes that smfi_setmlreply is not supported.
<li> You probably want to use sendmail-8.13, since that supports multi-line
SMTP error descriptions and SOCKETMAP. You want SOCKETMAP for use with
pysrs.
<li> Python milter has been tested against sendmail-8.11 through sendmail-8.13.
<li> Python milter must be compiled for the specific version of sendmail
it will run with. (Since the result is dynamically loaded, there could
conceivably be multiple versions available and selected at startup - but
that will have to wait.) This situation may only exist for sendmail
versions prior to 8.12. The protocol seems designed for backward
compatibility - and 8.12 is the first official milter release.
<li> Mea Culpa! After reading the Python Style guide, I realize that
my Python code is not up to snuff. Apparently mixed tabs and spaces
are anathema to those using Windows editors, where tabs can be expanded using
any arbitrary algorithm. Other than that, my
intuition matched Guido's pretty well - although I like to indent by 2
rather than 4. I will arrange to have tabs expanded to spaces when
exporting new versions. Until then, beware!
</menu>
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
to represent the stuff that comes with AIX. You might also want
my <a href="/aix/python.spec">python-2.1.1</a> spec file for AIX. It
does not include Tk or curses modules, sorry. If y'all trust me, you can
download rpms for AIX 4.x from my <a href="/aix">AIX RPM directory</a>.
<p>
Sendmail-8.12 renames
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system
shared library. Therefore, I rename libsm.a back to libsmutil.a for
AIX. This presents a problem for setup.py.
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
If you are running Redhat 7.2, the distributed version of sendmail
now enables libmilter by default. RedHat 7.2 bundles
the development libraries with the main sendmail package, so
there is no sendmail-devel package. However, they forgot to include the
headers! So you'll have to get the SRPM and modify it. I suggest
moving the static libs to a devel package and adding the headers. If
this is too much trouble, you can get the <a href="mfapi.h">mfapi.h</a>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
<p>
If you do modify the SRPM, I suggest renaming libsmutil.a
to libsm.a - just like sendmail-8.12 will. If you manually install
mfapi.h or don't rename libsmutil.a, you'll
need to force <code>libs = ["milter", "smutil"]</code> in setup.py.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
If you are running Redhat 6.2, the distributed version of sendmail
does not enable libmilter. You can download the Redhat 7.2 sendmail.spec
modified to compile on RedHat 6.2:
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
sendmail-rhmilter.spec</a>. The <a
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
SRPM for sendmail-8.11.6</a> is available from
<a href="http://www.redhat.com">Redhat</a> under
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
Errata for RH6.2</a>. But that doesn't include the latest security
patches since RH6.2 is no longer supported.
<p>
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> directory.
The lastest RPMs were built by taking a RH7.2 SRPMS and removing some
RPM features from the spec file that RH6.2 doesn't support, then
recompiling on RH6.2. You can check this by installing the RH7.2 SRPM,
then diffing my sendmail.spec with theirs. Then run
"rpm -bb sendmail-rhmilter.spec" when you are satisfied.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
You'll need to install the sendmail-devel package to compile milter.
+11 -50
View File
@@ -1,4 +1,8 @@
Title: Python Milter FAQ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Python Milter FAQ</title>
</head><body>
<h1> Python Milter <a name=faq>FAQ</a> </h1> <h1> Python Milter <a name=faq>FAQ</a> </h1>
@@ -18,7 +22,7 @@ shows you how to install libmilter with a separate invocation of make.
<li> Q. Why is mfapi.h not found when I try to compile Python milter on <li> Q. Why is mfapi.h not found when I try to compile Python milter on
RedHat 7.2? RedHat 7.2?
<p> A. RedHat forgot to include the header in the RPM. See the <p> A. RedHat forgot to include the header in the RPM. See the
<a href="requirements.html#rh72">RedHat 7.2 requirements</a>. <a href="milter.html#rh72">RedHat 7.2 requirements</a>.
<p> <p>
<h3> Running Python Milter </h3> <h3> Running Python Milter </h3>
@@ -68,9 +72,6 @@ milter-0.4.5 or later to remove this dependency.
<code>set_flags()</code> before calling <code>runmilter()</code>. For <code>set_flags()</code> before calling <code>runmilter()</code>. For
instance, <code>Milter.set_flags(Milter.ADDRCPT)</code>. You must add together instance, <code>Milter.set_flags(Milter.ADDRCPT)</code>. You must add together
all of <code>ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS</code> that apply. all of <code>ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS</code> that apply.
<p> NOTE - recent versions default flags to enabling all features. You
must now call <code>set_flags()</code> if you wish to disable features for
efficiency.
<p> <p>
<li> Q. Why does sendmail sometimes print something like: <li> Q. Why does sendmail sometimes print something like:
@@ -93,16 +94,10 @@ for your specific needs. We will of course continue to move generic
code out of the sample as the project evolves. Think of sample.py as code out of the sample as the project evolves. Think of sample.py as
an active config file. an active config file.
<p> <p>
If you are running bms.py, then the block_chinese option in
<code>/etc/mail/pymilter.cfg</code> controls this feature.
<p>
<li> Q. Why does sendmail coredump with milters on OpenBSD? <li> Q. Why does sendmail coredump with milters on OpenBSD?
<p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD. <p> A. Sendmail has a problem with unix sockets on OpenBSD. Use
OpenBSD users report that this problem has been fixed, so upgrading an internet domain socket instead. For example, in <code>sendmail.cf</code> use
OpenBSD will fix this. Otherwise, you can
use an internet domain socket instead. For example, in
<code>sendmail.cf</code> use
<pre> <pre>
Xpythonfilter, S=inet:1234@localhost Xpythonfilter, S=inet:1234@localhost
</pre> </pre>
@@ -138,41 +133,6 @@ is a milter declaration for sendmail.cf with all timeouts specified:
<pre> <pre>
Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
</pre> </pre>
<li> Q. There is a Python traceback in the log file! What happened to
my email?
<p> A. By default, when the milter fails with an untrapped exception, a
TEMPFAIL result (451) is returned to the sender. The sender will then retry
every hour or so for several days. Hopefully, someone will notice the
traceback, and workaround or fix the problem. Beginning with milter-0.8.2,
you can call <code>milter.set_exception_policy(milter.CONTINUE)</code>
to cause an untrapped exception to continue processing with the
next callback or milter instead. For
completeness, you can also set the exception policy to
<code>milter.REJECT</code>.
<li> Q. I read some notes such as "Check valid domains allowed by internal
senders to detect PCs infected with spam trojans." but could not
understand the idea. Could you clarify the content ?
<p> A. The <code>internal_domains</code> configuration specifies which
MAIL FROM domains are used by internal connections. If an internal
PC tries to use some other domain, it is assumed to be a "Zombie".
<p>
Here is a sample log line:
<pre>
2005Jun22 12:01:04 [12430] REJECT: zombie PC at 192.168.100.171 sending MAIL FROM debby@fedex.com
</pre>
No, fedex.com does not use pymilter, and there is no one named debby at my
client. But the idiot using the PC at 192.168.100.171 has downloaded and
installed some stupid weatherbar/hotbar/aquariumscreensaver that is actually a
spam bot.
<p>
The <code>internal_domains</code> option is simplistic, it assumes all
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
have a local policy feature for more general checking of internal mail.
<h3> Using SPF </h3>
<a name="spf"> <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
@@ -183,8 +143,8 @@ everything up for you. For other systems:
<li> Arrange to run bms.py in the background (as a service perhaps) and <li> Arrange to run bms.py in the background (as a service perhaps) and
redirect output and errors to a logfile. For instance, on AIX you'll want redirect output and errors to a logfile. For instance, on AIX you'll want
to use SRC (System Resource Controller). to use SRC (System Resource Controller).
<li> Copy pymilter.cfg to the /etc/mail or the directory you run bms.py in, <li> Copy milter.cfg to the directory you run bms.py in, and edit it. The
and edit it. The comments should explain the options. comments should explain the options.
<li> Start bms.py in the background as arranged. <li> Start bms.py in the background as arranged.
<li> Add Xpythonfilter to sendmail.cf or add an INPUT_MAIL_FILTER to <li> Add Xpythonfilter to sendmail.cf or add an INPUT_MAIL_FILTER to
sendmail.mc. Regen sendmail.cf if you use sendmail.mc and restart sendmail.mc. Regen sendmail.cf if you use sendmail.mc and restart
@@ -196,3 +156,4 @@ everything up for you. For other systems:
</ol> </ol>
</ol> </ol>
</html>
+15 -62
View File
@@ -1,63 +1,50 @@
[milter] [milter]
# the socket used to communicate with sendmail. Must match sendmail.cf # the socket used to communicate with sendmail. Must match sendmail.cf
socket=/var/run/milter/pythonsock ;socket=/var/run/milter/pythonsock
# where to save original copies of defanged and failed messages # where to save original copies of defanged and failed messages
tempdir = /var/log/milter/save 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
;internal_connect = 192.168.*.*,127.* ;internal_connect = 192.168.*.*
# mail that is not an internal_connect and claims to be from an # mail that is not an internal_connect and claims to be from an
# internal domain is rejected. Furthermore, internal mail that # internal domain is rejected. You should enable SPF instead if you can.
# does not claim to be from an internal domain is rejected. # SPF is much more comprehensive and flexible.
# You should enable SPF instead if you can. SPF is much more comprehensive and ;internal_domains = mycorp.com
# flexible. However, SPF is not currently checked for outgoing
# (internal_connect) mail because it doesn't yet handle authorizing
# internal IPs locally.
;internal_domains = mycorp.com,localhost.localdomain
# connections from a trusted relay can trust the first Received header # connections from a trusted relay can trust the first Received header
# SPF checks are bypassed for internal connections and trusted relays. # SPF checks are bypassed for internal connections and trusted relays.
;trusted_relay = 1.2.3.4, 66.12.34.56 ;trusted_relay = 1.2.3.4, 66.12.34.56
# 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.
;hello_blacklist = mycorp.com, 66.12.34.56 ;hello_blacklist = mycorp.com, 66.12.34.56
# Reject mail for domains mentioned unless user is mentioned here also
;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
# features intended to filter or block incoming mail # features intended to filter or block incoming mail
[defang] ;[defang]
# do virus scanning on attached messages also # do virus scanning on attached messages also
scan_rfc822 = 1 scan_rfc822 = 1
# do virus scanning on attached zipfiles also
scan_zip = 0
# Comment out scripts in HTML attachments. Can be CPU intensive. # Comment out scripts in HTML attachments. Can be CPU intensive.
scan_html = 0 scan_html = 0
# reject messages with asian fonts because we can't read them # reject messages with asian fonts because we can't read them
block_chinese = 1 block_chinese = 1
# list users who hate forwarded mail # list users who hate forwarded mail
;block_forward = egghead@mycorp.com, busybee@mycorp.com ;block_forward = egghead@mycorp.com, busybee@mycorp.com
# Reject mail for domains mentioned unless user is mentioned here also
;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
# reject mail with these case insensitive strings in the subject # reject mail with these case insensitive strings in the subject
porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck, porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck, xanaax, vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck, xanaax,
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam, p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra, v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra,
x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin, x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin,
valium, rolex, sexual, fuck, adv1t valium, rolex, sexual
# reject mail with these case sensitive strings in the subject # reject mail with these case sensitive strings in the subject
spam_words = $$$, !!!, XXX, FREE, HGH spam_words = $$$, !!!, XXX, FREE, HGH
# attachments with these extensions will be replaced with a warning
# message. A copy of the original will be saved.
banned_exts = ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,
inf,ins,isp,js,jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,
shs,url,vb,vbe,vbs,wsc,wsf,wsh
# See http://bmsi.com/python/pysrs.html for details # See http://bmsi.com/python/pysrs.html for details
[srs] [srs]
@@ -70,10 +57,6 @@ config=/etc/mail/pysrs.cfg
;fwdomain = mydomain.com ;fwdomain = mydomain.com
# turn this on after a grace period to reject spoofed DSNs # turn this on after a grace period to reject spoofed DSNs
reject_spoofed = 0 reject_spoofed = 0
# Many braindead MTAs send DSNs with a non-DSN MFROM (e.g. to report that
# some virus claiming to be sent by you). This heuristic
# refuses mail from user names commonly abused in that way.
;banned_users = postmaster, mailer-daemon, clamav
# See http://spf.pobox.com for more info on SPF. # See http://spf.pobox.com for more info on SPF.
[spf] [spf]
@@ -84,26 +67,10 @@ reject_spoofed = 0
;reject_neutral = aol.com ;reject_neutral = aol.com
# use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published # use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published
;best_guess = 0 ;best_guess = 0
# Reject senders that have neither PTR nor valid HELO nor SPF records, or send # reject senders that have neither PTR nor SPF records
# DSN otherwise
;reject_noptr = 0 ;reject_noptr = 0
# always accept softfail from these domains, or send DSN otherwise # always accept softfail from these domains
;accept_softfail = bounces.amazon.com ;accept_softfail = bounces.amazon.com
# Treat fail from these domains like softfail: because their SPF record
# or an important sender is screwed up. Must have valid HELO, however.
;accept_fail = custhelp.com
# Use sendmail access map or similar format for detailed spf policy.
# SPF entries in the access map will override any defaults set above.
;access_file = /etc/mail/access.db
# Add MAIL FROM as Sender when Sender is missing and From domain
# doesn't match MAIL FROM. Outlook and other email clients will then display
# something like: "Sent by sender@domain.com on behalf of from@example.com"
;supply_sender = 0
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
# features intended to clean up outgoing mail # features intended to clean up outgoing mail
[scrub] [scrub]
@@ -120,7 +87,6 @@ blind = 1
# (sendmail aliases let you monitor incoming mail) # (sendmail aliases let you monitor incoming mail)
# #
;users = disloyal@bigcorp.com, bigmouth@bigcorp.com ;users = disloyal@bigcorp.com, bigmouth@bigcorp.com
# multiple destinations can use smart_alias
;dest = spy@bigcorp.com ;dest = spy@bigcorp.com
# discard outgoing mail without alerting sender # discard outgoing mail without alerting sender
# can be used in conjunction with wiretap to censor outgoing mail # can be used in conjunction with wiretap to censor outgoing mail
@@ -128,10 +94,7 @@ blind = 1
# #
# smart aliases trigger on both sender and recipient # smart aliases trigger on both sender and recipient
# #
;smart_alias = copycust,walter,spy1,spy2 ;smart_alias = copycust,walter
# multiple wiretap monitors
;spy1 = disloyal@bigcorp.com,spy@bigcorp.com
;spy2 = bigmouth@bigcorp.com,spy@bigcorp.com
# mail from client@clientcorp.com to sue@bigcorp.com is redirected to # mail from client@clientcorp.com to sue@bigcorp.com is redirected to
# local alias copycust # local alias copycust
;copycust = client@clientcorp.com,sue@bigcorp.com ;copycust = client@clientcorp.com,sue@bigcorp.com
@@ -141,8 +104,6 @@ blind = 1
# additional copies can be added # additional copies can be added
;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com, ;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com,
; walter@bigcorp.com ; walter@bigcorp.com
;bulk = soruce@telex.com,bob@jsconnor.com
;bulk = soruce@telex.com,larry@jsconnor.com
# See http://bmsi.com/python/dspam.html # See http://bmsi.com/python/dspam.html
[dspam] [dspam]
@@ -151,13 +112,7 @@ blind = 1
# only EXTERNAL messages are dspam filtered # only EXTERNAL messages are dspam filtered
;dspam_dict=/var/lib/dspam/moderator.dict ;dspam_dict=/var/lib/dspam/moderator.dict
# Recipients of mail sent from these senders are added to the auto_whitelist. # Opt-opt recipients from dspam screening and header triage
# Auto_whitelisted senders with an SPF PASS are never rejected by dspam, and
# messages from auto_whitelisted senders will be used to train screener
# dictionaries as innocent mail.
;whitelist_senders = @mycorp.com
# Opt-out recipients entirely from dspam screening and header triage
;dspam_exempt=getitall@mycorp.com ;dspam_exempt=getitall@mycorp.com
# Do not scan mail (ostensibly) from these senders # Do not scan mail (ostensibly) from these senders
;dspam_whitelist=getitall@sender.com ;dspam_whitelist=getitall@sender.com
@@ -181,8 +136,6 @@ blind = 1
;spam=spam@foocorp.com ;spam=spam@foocorp.com
# address to forward false positives to. milter will process and not deliver # address to forward false positives to. milter will process and not deliver
;falsepositive=ham@foocorp.com ;falsepositive=ham@foocorp.com
# account which receives only spam: all received messages are marked as spam.
;honeypot=spam-me@example.com
# the dspam_screener is a list of dspam users who screen mail for all # the dspam_screener is a list of dspam users who screen mail for all
# recipients who are not dspam_users. Spam goes to the screeners quarantine, # recipients who are not dspam_users. Spam goes to the screeners quarantine,
# and the original recipients are saved so that false positives can be properly # and the original recipients are saved so that false positives can be properly
+249 -65
View File
@@ -1,32 +1,34 @@
Title: Python Milters <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Python Milters</title>
</head><body>
<P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/"> <P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
<IMG SRC="http://bmsi.com/art/brain1.gif" <IMG SRC="/art/brain1.gif"
ALT="Viewable With Any Browser" BORDER="0"></A> ALT="Viewable With Any Browser" BORDER="0"></A>
<img src="http://bmsi.com/art/banner_4.gif" width="468" height="60" border="0" <img src="/art/banner_4.gif" width="468" height="60" border="0"
usemap="#banner_4" alt="Your vote?"> usemap="#banner_4" alt="Your vote?">
<map name="banner_4"> <map name="banner_4">
<area shape="rect" coords="330,25,426,59" <area shape="rect" coords="330,25,426,59"
href="http://education-survey.org/" alt="I Disagree"> href="http://education-survey.org/" alt="I Disagree">
<area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree"> <area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
</map> </map>
</P>
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align=left> </P>
<h1 align=center>Sendmail Milters in Python</h1> <h1 align=center>Sendmail Milters in Python</h1>
<h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a> <h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a>
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D"> and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
Stuart D. Gathman</a><br> Stuart D. Gathman</a><br>
This web page is written by Stuart D. Gathman<br>and<br>sponsored by This web page is written by Stuart D. Gathman<br>and<br>sponsored by
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br> <a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
Last updated Dec 29, 2005</h4> Last updated May 31, 2005</h4>
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> | See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
<a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> | <a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
<a href="#overview">Overview</a> | <a href="#overview">Overview</a>
<a href="/python/dspam.html">pydspam</a> |
<a href="/libdspam/dspam.html">libdspam</a>
<p> <p>
<a href="//www.python.org"> <a href="//www.python.org">
<img src="python55.gif" align=left alt="A Python"></a> <img src="python55.gif" align=left alt="A Python"></a>
@@ -37,9 +39,145 @@ provides a python interface to libmilter that exploits all its features.
<p> <p>
Sendmail 8.12 officially releases libmilter. Sendmail 8.12 officially releases libmilter.
Version 8.12 seems to be more robust, and includes new privilege Version 8.12 seems to be more robust, and includes new privilege
separation features to enhance security. Even better, sendmail 8.13 separation features to enhance security.
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more I recommend upgrading.
efficient and secure. I recommend upgrading.
<h2> Recent Changes </h2>
Python milter is being moved to
<a href="http://sourceforge.net/projects/pymilter/">Sourceforge</a> for
development.
<p>
Release 0.7.2 tightens the authentication screws with a "3 strikes and
your out" policy. A sender must have a valid PTR, HELO, or SPF record
to send email. Specific senders can be whitelisted using the
"delegate" option in the spf configuration section by adding a
default SPF record for them. The PTR and HELO are required
by RFC anyway, so this is not an unreasonable requirement.
There is now a coherent policy for an SPF softfail result. A softfail
is accepted if there is a valid PTR or HELO, or if the domain
is listed in the "accept_softfail" option of the spf configuration section.
A neutral result is accepted by default if there is a valid PTR or
HELO, (and the SPF record was not guessed), unless the domain is listed in the
"reject_neutral" option. Common forms of PTR records for dynamic IPs are
recognized, and do not count as a valid PTR. This does not prevent anyone
from sending mail from a dynamic IP - they just need to configure a
valid HELO name or publish an SPF record.
<p>
As SPF adoption continues to rise, forged spam is not getting through. So
spammers are publishing their SPF records as predicted. The 0.7.2 RPM
now provides the <code>rhsbl</code> sendmail hack so that spammer domains
can be blacklisted. With the RPM installed, add a line like the following
to your <code>sendmail.mc</code>.
<pre>
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
</pre>
<p>
Of course, spammers are now starting to register
throwaway domains. The next thing we need is a custom DNS server,
in Python, that
can recognize patterns. For instance, one spammer registers ded304.com,
ded305.com, ded306.com, etc. We also need the custom DNS server to
let SPF classic clients check SES (which will be part of pysrs).
The <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
framework provides a custom DNS server - but I
would like a smaller implementation for our use.
<p>
The RPM for release 0.7.0 moves the config file and socket locations to
/etc/mail and /var/run/milter respectively. We now parse Microsoft CID records
- but only hotmail.com uses them. They seem to have applied for a patent on
the brilliant idea of examining the mail headers to see who the message is
from. We aren't doing that here, so not to worry - but I am not a lawyer, so
if you are worried, change spf.py around line 626 to return None instead of
calling CIDParser(). There is a new option to reject mail with no PTR
and no SPF.
<p>
Microsoft is pushing an anti-opensource license for their pending patent
along with their sender-ID proposal before the IETF.
It is royalty free - but requires anyone distributing a binary they've
compiled from source to sign a license agreement. The Apache Software
Foundation <a
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
the problem with sender-ID</a>, and Debian <a
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
software in general</a> and the <a
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
particular</a>, Python milter will not be able to implement sender-ID in its
current form. This was, no doubt, Microsoft's intent all along.
<p>
Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I
recommend ignoring it and continuing to implement and improve SPF until a
working and unencumbered proposal for RFC2822 headers surfaces.
<p>
<a href="http://spf.pobox.com">
<img src="SPF.gif" align=left alt="SPF logo"></a>
Release 0.6.6 adds support for <a href="http://spf.pobox.com/">SPF</a>,
a protocol to prevent forging of the envelope from address.
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
The included spf.py module is an updated version of the original 1.6
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
The updated version tracks the draft RFC and test suite.
<p>
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
dspam bayes filter project</a> and <a href="dspam.html">
packaged it for python</a>.
Release 0.6.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers.
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
<h2> Enough Already! </h2>
Nearly a dozen people have emailed me begging for a feature to copy
outgoing and/or incoming mail to a backup directory by user. Ok, it
looks like this is a most requested feature for 0.5.6. In the meantime,
here are some things to consider:
<ul>
<li> If you want to equivalent of a Bcc added to each message, this
is very easy to do in the python code for bms.py. See below.
<li> If you want to copy to a file in a directory (thus avoiding having to
set up aliases), this is slightly more involved. The bms.py milter already
copies the message to a temporary file for use in replacing the message body
when banned attachments are found. You have to open a file, and copy the
Mesage object to it in eom().
<li> Finally, you are probably aware that most email clients already
keep a copy of outgoing mail? Presumably there is a good reason for
keeping another copy on the server.
</ul>
<p>
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
determining whether you want to copy (e.g. whether the sender is local). For
example,
<pre>
def envfrom(...
...
if len(t) == 2:
self.rejectvirus = t[1] in reject_virus_from
if t[0] in wiretap_users.get(t[1],()):
self.add_recipient(wiretap_dest)
if t[1] == 'mydomain.com':
self.add_recipient('&lt;copy-%s&gt;' % t[0])
...
</pre>
<p>
To make this a generic feature requires thinking about how the configuration
would look. Feel free to make specific suggestions about config file
entries. Be sure to handle both Bcc and file copies, and designating what
mail should be copied. How should "outgoing" be defined? Implementing it is
easy once the configuration is designed.
<h3><a name=overview>Overview</a></h3> <h3><a name=overview>Overview</a></h3>
@@ -73,7 +211,7 @@ methods that
do nothing, and also provides wrappers for the libmilter methods to mutate do nothing, and also provides wrappers for the libmilter methods to mutate
the message. the message.
<p> <p>
The 'spf' module provides an implementation of <a href="http://openspf.com"> The 'spf' module provides an implementation of <a href="http://spf.pobox.com">
SPF</a> useful for detecting email forgery. SPF</a> useful for detecting email forgery.
<p> <p>
The 'mime' module provides a wrapper for the Python email package that The 'mime' module provides a wrapper for the Python email package that
@@ -88,8 +226,6 @@ content filtering. SPF checking
requires <a href="http://pydns.sourceforge.net/"> requires <a href="http://pydns.sourceforge.net/">
pydns</a>. Configuration documentation is currently included as comments pydns</a>. Configuration documentation is currently included as comments
in the <a href="milter.cfg">sample config file</a> for the bms.py milter. in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
See also the <a href="HOWTO">HOWTO</a> and <a href="logmsgs.html">
Milter Log Message Tags</a>.
<p> <p>
Python milter is under GPL. The authors can probably be convinced to Python milter is under GPL. The authors can probably be convinced to
change this to LGPL if needed. change this to LGPL if needed.
@@ -221,14 +357,8 @@ me if you successfully install milter on a system not mentioned below.
<td>0.5.5</td><tr> <td>0.5.5</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.13.1</td> <td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.13.1</td>
<td>0.7.2</td><tr> <td>0.7.2</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.4.1</td><td>8.13.5</td>
<td>0.8.4</td><tr>
<td>RedHat 8.0</td><td>gcc-3.2</td><td>2.2.1</td><td>8.12.6</td> <td>RedHat 8.0</td><td>gcc-3.2</td><td>2.2.1</td><td>8.12.6</td>
<td>0.5.2</td><tr> <td>0.5.2</td><tr>
<td>RedHat 9.0</td><td>gcc-3.2.2</td><td>2.4.1</td><td>8.13.1</td>
<td>0.8.2</td><tr>
<td>RedHat EL3</td><td>gcc-3.2.3</td><td>2.4.1</td><td>8.13.5</td>
<td>0.8.4</td><tr>
<td>Debian Linux</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.0</td> <td>Debian Linux</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.0</td>
<td>0.3.7</td><tr> <td>0.3.7</td><tr>
<td>Debian Linux</td><td>gcc-3.2.2</td><td>2.2.2</td><td>8.12.7</td> <td>Debian Linux</td><td>gcc-3.2.2</td><td>2.2.2</td><td>8.12.7</td>
@@ -239,8 +369,8 @@ me if you successfully install milter on a system not mentioned below.
<td>0.3.4</td><tr> <td>0.3.4</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.3</td><td>8.12.3</td> <td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.3</td><td>8.12.3</td>
<td>0.4.2</td><tr> <td>0.4.2</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.4.1</td><td>8.13.1</td> <td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.2.3</td><td>8.13.1</td>
<td>0.8.4</td><tr> <td>0.7.1</td><tr>
<td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td> <td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td>
<td>0.3.8</td><tr> <td>0.3.8</td><tr>
<td>Slackware 9.0</td><td>gcc-3.2.2</td><td>2.2.3</td><td>8.12.9</td> <td>Slackware 9.0</td><td>gcc-3.2.2</td><td>2.2.3</td><td>8.12.9</td>
@@ -254,54 +384,108 @@ me if you successfully install milter on a system not mentioned below.
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td> <td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td>
<td>0.5.5</td><tr> <td>0.5.5</td><tr>
<td>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td> <td>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td>
<td>0.6.6</td> <td>0.6.6</td><tr>
</table> </table>
<h2> Enough Already! </h2> <h3> Requirements </h3>
Nearly a dozen people have emailed me begging for a feature to copy <menu>
outgoing and/or incoming mail to a backup directory by user. Ok, it <li> While the miltermodule will work with python 1.5, you probably
looks like this is a most requested feature for 0.5.6. In the meantime, want to use python 2.0 or better. The python code uses a number of
here are some things to consider: python 2 features.
<ul> <li> Python must be configured with thread support. This is because
<li> If you want to equivalent of a Bcc added to each message, this sendmail's libmilter requires thread support.
is very easy to do in the python code for bms.py. See below. <li> You must compile sendmail with libmilter enabled. In versions of
<li> If you want to copy to a file in a directory (thus avoiding having to sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
set up aliases), this is slightly more involved. The bms.py milter already is not installed by default.
copies the message to a temporary file for use in replacing the message body Sendmail 8.12 still does not enable libmilter by default. You must
when banned attachments are found. You have to open a file, and copy the explicitly select the "MILTER" option when compiling.
Mesage object to it in eom(). <li> Python milter has been tested against sendmail-8.11 and sendmail-8.12.
<li> Finally, you are probably aware that most email clients already <li> Python milter must be compiled for the specific version of sendmail
keep a copy of outgoing mail? Presumably there is a good reason for it will run with. (Since the result is dynamically loaded, there could
keeping another copy on the server. conceivably be multiple versions available and selected at startup - but
</ul> that will have to wait.) This situation may only exist for sendmail
<p> versions prior to 8.12. The protocol seems designed for backward
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after compatibility - and 8.12 is the first official milter release.
determining whether you want to copy (e.g. whether the sender is local). For <li> Mea Culpa! After reading the Python Style guide, I realize that
example, my Python code is not up to snuff. Apparently mixed tabs and spaces
<pre> are anathema to those using Windows editors, where tabs can be expanded using
def envfrom(... any arbitrary algorithm. Other than that, my
... intuition matched Guido's pretty well - although I like to indent by 2
if len(t) == 2: rather than 4. I will arrange to have tabs expanded to spaces when
self.rejectvirus = t[1] in reject_virus_from exporting new versions. Until then, beware!
if t[0] in wiretap_users.get(t[1],()): </menu>
self.add_recipient(wiretap_dest)
if t[1] == 'mydomain.com':
self.add_recipient('&lt;copy-%s&gt;' % t[0])
...
</pre>
<p>
To make this a generic feature requires thinking about how the configuration
would look. Feel free to make specific suggestions about config file
entries. Be sure to handle both Bcc and file copies, and designating what
mail should be copied. How should "outgoing" be defined? Implementing it is
easy once the configuration is designed.
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
to represent the stuff that comes with AIX. You might also want
my <a href="/aix/python.spec">python-2.1.1</a> spec file for AIX. It
does not include Tk or curses modules, sorry. If y'all trust me, you can
download rpms for AIX 4.x from my <a href="/aix">AIX RPM directory</a>.
<p>
Sendmail-8.12 renames
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system
shared library. Therefore, I rename libsm.a back to libsmutil.a for
AIX. This presents a problem for setup.py.
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
If you are running Redhat 7.2, the distributed version of sendmail
now enables libmilter by default. RedHat 7.2 bundles
the development libraries with the main sendmail package, so
there is no sendmail-devel package. However, they forgot to include the
headers! So you'll have to get the SRPM and modify it. I suggest
moving the static libs to a devel package and adding the headers. If
this is too much trouble, you can get the <a href="mfapi.h">mfapi.h</a>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
<p>
If you do modify the SRPM, I suggest renaming libsmutil.a
to libsm.a - just like sendmail-8.12 will. If you manually install
mfapi.h or don't rename libsmutil.a, you'll
need to force <code>libs = ["milter", "smutil"]</code> in setup.py.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
If you are running Redhat 6.2, the distributed version of sendmail
does not enable libmilter. You can download the Redhat 7.2 sendmail.spec
modified to compile on RedHat 6.2:
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
sendmail-rhmilter.spec</a>. The <a
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
SRPM for sendmail-8.11.6</a> is available from
<a href="http://www.redhat.com">Redhat</a> under
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
Errata for RH6.2</a>. But that doesn't include the latest security
patches since RH6.2 is no longer supported.
<p>
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> directory.
The lastest RPMs were built by taking a RH7.2 SRPMS and removing some
RPM features from the spec file that RH6.2 doesn't support, then
recompiling on RH6.2. You can check this by installing the RH7.2 SRPM,
then diffing my sendmail.spec with theirs. Then run
"rpm -bb sendmail-rhmilter.spec" when you are satisfied.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
You'll need to install the sendmail-devel package to compile milter.
<hr> <hr>
<p> <p>
<a href="http://validator.w3.org/check/referer"> <a href="http://validator.w3.org/check/referer">
<img border=0 src="http://bmsi.com/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a> <img border=0 src="/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
<a href="http://www.redhat.com"> <a href="http://www.redhat.com">
<img src="http://bmsi.com/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a> <img src="/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
</p> </p>
</body></html>
+1 -1
View File
@@ -5,7 +5,7 @@
# chkconfig: 2345 80 30 # chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail. # description: Milter is a process that filters messages sent through sendmail.
# processname: milter # processname: milter
# config: /etc/mail/pymilter.cfg # config: /var/log/milter/bms.py
# pidfile: /var/run/milter/milter.pid # pidfile: /var/run/milter/milter.pid
python="python2.3" python="python2.3"
+1 -1
View File
@@ -5,7 +5,7 @@
# chkconfig: 2345 80 30 # chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail. # description: Milter is a process that filters messages sent through sendmail.
# processname: milter # processname: milter
# config: /etc/mail/pymilter.cfg # config: /var/log/milter/bms.py
# pidfile: /var/run/milter/milter.pid # pidfile: /var/run/milter/milter.pid
python="python2.3" python="python2.3"
+13 -96
View File
@@ -1,25 +1,10 @@
%define name milter %define name milter
%define version 0.8.5 %define version 0.8.0
%define release 1.RH7 %define release 2.EL3
# what version of RH are we building for? # Redhat 7.x and earlier (multiple ps lines per thread)
%define redhat9 0 #define sysvinit milter.rc7
%define redhat7 1
%define redhat6 0
# Options for Redhat version 6.x:
# rpm -ba|--rebuild --define "rh6 1"
%{?rh6:%define redhat7 0}
%{?rh6:%define redhat6 1}
# some systems dont have initrddir defined
%{?_initrddir:%define _initrddir /etc/rc.d/init.d}
%if %{redhat9}
%define sysvinit milter.rc
%else # Redhat 7.x and earlier (multiple ps lines per thread)
%define sysvinit milter.rc7
%endif
# RH9, other systems (single ps line per process) # RH9, other systems (single ps line per process)
%define sysvinit milter.rc
%ifos Linux %ifos Linux
%define python python2.4 %define python python2.4
%else %else
@@ -39,11 +24,11 @@ Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com> Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com> Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, sendmail >= 8.13 Requires: %{python} >= 2.4, sendmail >= 8.12.10
%ifos Linux %ifnos aix4.1
Requires: chkconfig Requires: chkconfig
%endif %endif
BuildRequires: %{python}-devel , sendmail-devel >= 8.13 BuildRequires: %{python}-devel >= 2.2.2, sendmail-devel >= 8.12.10
%description %description
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
@@ -52,15 +37,10 @@ modules provide for navigating and modifying MIME parts.
%prep %prep
%setup %setup
#%patch -p0 -b .bms #%patch -p1
%build %build
if %{redhat9}; then env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
LDFLAGS="-g"
else
LDFLAGS="-s"
fi
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build
%install %install
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
@@ -68,7 +48,7 @@ rm -rf $RPM_BUILD_ROOT
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 cp bms.py $RPM_BUILD_ROOT/var/log/milter
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
# logfile rotation # logfile rotation
@@ -151,7 +131,7 @@ rm -rf $RPM_BUILD_ROOT
%files -f INSTALLED_FILES %files -f INSTALLED_FILES
%defattr(-,root,root) %defattr(-,root,root)
%doc README HOWTO NEWS TODO CREDITS sample.py %doc README NEWS TODO CREDITS sample.py
/etc/logrotate.d/milter /etc/logrotate.d/milter
/etc/cron.daily/milter /etc/cron.daily/milter
%ifos aix4.1 %ifos aix4.1
@@ -165,75 +145,12 @@ rm -rf $RPM_BUILD_ROOT
%dir /var/log/milter/save %dir /var/log/milter/save
%config /var/log/milter/start.sh %config /var/log/milter/start.sh
%config /var/log/milter/bms.py %config /var/log/milter/bms.py
%config(noreplace) /var/log/milter/strike3.txt
%config(noreplace) /var/log/milter/softfail.txt
%config(noreplace) /var/log/milter/neutral.txt
%config(noreplace) /var/log/milter/quarantine.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
%changelog %changelog
* Thu Dec 29 2005 Stuart Gathman <stuart@bmsi.com> 0.8.5-1
- Simple trusted_forwarder implementation.
- Fix access_file neutral policy
- Move Received-SPF header to beginning of headers
- Supply keyword info for all results in Received-SPF header.
- Move guessed SPF result to separate header
- Activate smfi_insheader only when SMFIR_INSHEADER defined
- Handle NULL MX in spf.py
- in-process GOSSiP server support (to be extended later)
- Expire CBV cache and renew auto-whitelist entries
* Fri Oct 21 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-2
- Don't supply sender when MFROM is subdomain of header from/sender.
- Don't send quarantine DSN for DSNs
- Skip dspam for replies/DSNs to signed MFROM
* Thu Oct 20 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-1
- Fix SPF policy via sendmail access map (case insensitive keys).
- Auto whitelist senders, train screener on whitelisted messages
- Optional idx parameter to addheader to invoke smfi_insheader
- Activate progress when SMFIR_PROGRESS defined
* Wed Oct 12 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-1
- Keep screened honeypot mail, but optionally discard honeypot only mail.
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
- Consider SMTP AUTH connections internal.
- Send DSN for SPF errors corrected by extended processing.
- Send DSN before SCREENED mail is quarantined
- Option to set SPF policy via sendmail access map.
- Option to supply Sender header from MAIL FROM when missing.
- Use logging package to keep log lines atomic.
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-4
- Limit each CNAME chain independently like PTR and MX
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-3
- Limit CNAME lookups (regression)
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-2
- Handle corrupt ZIP attachments
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-1
- Strict processing limits per SPF RFC
- Fixed several parsing bugs under RFC
- Support official IANA SPF record (type99)
- Honeypot support (requires pydspam-1.1.9)
- Extended SPF processing results beyond strict RFC limits
- Support original SES for local bounce protection (requires pysrs-0.30.10)
- Callback exception processing option in milter module
* Thu Jun 16 2005 Stuart Gathman <stuart@bmsi.com> 0.8.1-1
- Fix zip in zip loop in mime.py
- Fix HeaderParseError in bms.py header callback
- Check internal_domains for outgoing mail
- Fix inconsistent results from send_dsn
* Mon Jun 06 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-3
- properly log pydspam exceptions
* Sat Jun 04 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-2
- Include default softfail, strike3 templates
* Wed May 25 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-1
- Move Milter module to subpackage.
- DSN support for Three strikes rule and SPF SOFTFAIL
- Move /*mime*/ and dynip to Milter subpackage
- Fix SPF unknown mechanism list not cleared
- Make banned extensions configurable.
- Option to scan zipfiles for bad extensions.
* Tue Feb 08 2005 Stuart Gathman <stuart@bmsi.com> 0.7.3-1.EL3 * Tue Feb 08 2005 Stuart Gathman <stuart@bmsi.com> 0.7.3-1.EL3
- Support EL3 and Python2.4 (some scanning/defang support broken) - Compile for EL3 and Python4
* Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1 * Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
- Fix various SPF bugs - Fix various SPF bugs
- Recognize dynamic PTR names, and don't count them as authentication. - Recognize dynamic PTR names, and don't count them as authentication.
+16 -100
View File
@@ -1,5 +1,4 @@
/* 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)
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@@ -34,30 +33,6 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* Revision 1.8 2005/10/20 23:23:36 customdesigned
* Include smfi_progress is SMFIR_PROGRESS defined
*
* Revision 1.7 2005/10/20 23:04:46 customdesigned
* Add optional idx for position of added header.
*
* Revision 1.6 2005/07/15 22:18:17 customdesigned
* Support callback exception policy
*
* Revision 1.5 2005/06/24 04:20:07 customdesigned
* Report context allocation error.
*
* Revision 1.4 2005/06/24 04:12:43 customdesigned
* Remove unused name argument to generic wrappers.
*
* Revision 1.3 2005/06/24 03:57:35 customdesigned
* Handle close called before connect.
*
* Revision 1.2 2005/06/02 04:18:55 customdesigned
* Update copyright notices after reading article on /.
*
* Revision 1.1.1.2 2005/05/31 18:09:06 customdesigned
* Release 0.7.1
*
* Revision 2.31 2004/08/23 02:24:36 stuart * Revision 2.31 2004/08/23 02:24:36 stuart
* Support setbacklog * Support setbacklog
* *
@@ -215,7 +190,7 @@ $ python setup.py help
/* Yes, these are static. If you need multiple different callbacks, */ /* Yes, these are static. If you need multiple different callbacks, */
/* it's cleaner to use multiple filters, or convert to OO method calls. */ /* it's cleaner to use multiple filters. */
static PyObject *connect_callback = NULL; static PyObject *connect_callback = NULL;
static PyObject *helo_callback = NULL; static PyObject *helo_callback = NULL;
static PyObject *envfrom_callback = NULL; static PyObject *envfrom_callback = NULL;
@@ -260,11 +235,8 @@ _get_context(SMFICTX *ctx) {
PyEval_AcquireThread(t); /* lock interp */ PyEval_AcquireThread(t); /* lock interp */
self = PyObject_New(milter_ContextObject,&milter_ContextType); self = PyObject_New(milter_ContextObject,&milter_ContextType);
if (!self) { if (!self) {
/* Report and clear exception since we are called from libmilter */ /* Can't pass on exception since we are called from libmilter */
if (PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear(); PyErr_Clear();
}
PyThreadState_Clear(t); PyThreadState_Clear(t);
PyEval_ReleaseThread(t); PyEval_ReleaseThread(t);
PyThreadState_Delete(t); PyThreadState_Delete(t);
@@ -355,8 +327,7 @@ CHGHDRS - filter may change/delete headers";
static PyObject * static PyObject *
milter_set_flags(PyObject *self, PyObject *args) { milter_set_flags(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "i:set_flags", &description.xxfi_flags)) if (!PyArg_ParseTuple(args, "i", &description.xxfi_flags)) return NULL;
return NULL;
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
} }
@@ -522,28 +493,6 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_close_callback", &close_callback); return generic_set_callback(args, "O:set_close_callback", &close_callback);
} }
static int exception_policy = SMFIS_TEMPFAIL;
static char milter_set_exception_policy__doc__[] =
"set_exception_policy(i) -> None\n\
Sets the policy for untrapped Python exceptions during a callback.\n\
Must be one of TEMPFAIL,REJECT,CONTINUE";
static PyObject *
milter_set_exception_policy(PyObject *self, PyObject *args) {
int i;
if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i))
return NULL;
switch (i) {
case SMFIS_REJECT: case SMFIS_TEMPFAIL: case SMFIS_CONTINUE:
exception_policy = i;
Py_INCREF(Py_None);
return Py_None;
}
PyErr_SetString(MilterError,"invalid exception policy");
return NULL;
}
/** Report and clear any python exception before returning to libmilter. /** Report and clear any python exception before returning to libmilter.
The interpreter is locked when we are called, and we unlock it. */ The interpreter is locked when we are called, and we unlock it. */
static int _report_exception(milter_ContextObject *self) { static int _report_exception(milter_ContextObject *self) {
@@ -551,16 +500,9 @@ static int _report_exception(milter_ContextObject *self) {
PyErr_Print(); PyErr_Print();
PyErr_Clear(); /* must clear since not returning to python */ PyErr_Clear(); /* must clear since not returning to python */
PyEval_ReleaseThread(self->t); PyEval_ReleaseThread(self->t);
switch (exception_policy) {
case SMFIS_REJECT:
smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure");
return SMFIS_REJECT;
case SMFIS_TEMPFAIL:
smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure"); smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure");
return SMFIS_TEMPFAIL; return SMFIS_TEMPFAIL;
} }
return SMFIS_CONTINUE;
}
PyEval_ReleaseThread(self->t); PyEval_ReleaseThread(self->t);
return SMFIS_CONTINUE; return SMFIS_CONTINUE;
} }
@@ -670,7 +612,7 @@ milter_wrap_helo(SMFICTX *ctx, char *helohost) {
} }
static int static int
generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) { generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv, const char *name) {
PyObject *arglist; PyObject *arglist;
milter_ContextObject *self; milter_ContextObject *self;
int count = 0; int count = 0;
@@ -707,12 +649,12 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
static int static int
milter_wrap_envfrom(SMFICTX *ctx, char **argv) { milter_wrap_envfrom(SMFICTX *ctx, char **argv) {
return generic_env_wrapper(ctx,envfrom_callback,argv); return generic_env_wrapper(ctx,envfrom_callback,argv,"milter_wrap_envfrom");
} }
static int static int
milter_wrap_envrcpt(SMFICTX *ctx, char **argv) { milter_wrap_envrcpt(SMFICTX *ctx, char **argv) {
return generic_env_wrapper(ctx,envrcpt_callback,argv); return generic_env_wrapper(ctx,envrcpt_callback,argv,"milter_wrap_envrcpt");
} }
static int static int
@@ -728,7 +670,7 @@ milter_wrap_header(SMFICTX *ctx, char *headerf, char *headerv) {
} }
static int static int
generic_noarg_wrapper(SMFICTX *ctx,PyObject *cb) { generic_noarg_wrapper(SMFICTX *ctx,PyObject *cb,const char *name) {
PyObject *arglist; PyObject *arglist;
milter_ContextObject *c; milter_ContextObject *c;
if (cb == NULL) return SMFIS_CONTINUE; if (cb == NULL) return SMFIS_CONTINUE;
@@ -740,7 +682,7 @@ generic_noarg_wrapper(SMFICTX *ctx,PyObject *cb) {
static int static int
milter_wrap_eoh(SMFICTX *ctx) { milter_wrap_eoh(SMFICTX *ctx) {
return generic_noarg_wrapper(ctx,eoh_callback); return generic_noarg_wrapper(ctx,eoh_callback,"milter_wrap_eoh");
} }
static int static int
@@ -758,31 +700,18 @@ milter_wrap_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) {
static int static int
milter_wrap_eom(SMFICTX *ctx) { milter_wrap_eom(SMFICTX *ctx) {
return generic_noarg_wrapper(ctx,eom_callback); return generic_noarg_wrapper(ctx,eom_callback,"milter_wrap_eom");
} }
static int static int
milter_wrap_abort(SMFICTX *ctx) { milter_wrap_abort(SMFICTX *ctx) {
/* libmilter still calls close after abort */ /* libmilter still calls close after abort */
return generic_noarg_wrapper(ctx,abort_callback); return generic_noarg_wrapper(ctx,abort_callback,"milter_wrap_abort");
} }
static int static int
milter_wrap_close(SMFICTX *ctx) { milter_wrap_close(SMFICTX *ctx) {
/* xxfi_close can be called out of order - even before connect. int r = generic_noarg_wrapper(ctx,close_callback,"milter_wrap_close");
* There may not yet be a private context pointer. To avoid
* creating a ThreadContext and allocating a milter context only
* to destroy them, and to avoid invoking the python close_callback when
* connect has never been called, we don't use generic_noarg_wrapper here. */
PyObject *cb = close_callback;
milter_ContextObject *self = smfi_getpriv(ctx);
int r = SMFIS_CONTINUE;
if (self != NULL && cb != NULL && self->ctx == ctx) {
PyObject *arglist;
PyEval_AcquireThread(self->t);
arglist = Py_BuildValue("(O)", self);
r = _generic_wrapper(self, cb, arglist);
}
/* FIXME: It is inefficient to have released the interp lock only to /* FIXME: It is inefficient to have released the interp lock only to
acquire it again in _clear_context. We can tell _generic_return and acquire it again in _clear_context. We can tell _generic_return and
friends not to release the lock by, for instance, setting self->t to NULL. friends not to release the lock by, for instance, setting self->t to NULL.
@@ -976,39 +905,28 @@ milter_setreply(PyObject *self, PyObject *args) {
} }
static char milter_addheader__doc__[] = static char milter_addheader__doc__[] =
"addheader(field, value, idx=-1) -> None\n\ "addheader(field, value) -> None\n\
Add a header to the message. This header is not passed to other\n\ Add a header to the message. This header is not passed to other\n\
filters. It is not checked for standards compliance;\n\ filters. It is not checked for standards compliance;\n\
the mail filter must ensure that no protocols are violated\n\ the mail filter must ensure that no protocols are violated\n\
as a result of adding this header.\n\ as a result of adding this header.\n\
field - header field name\n\ field - header field name\n\
value - header field value\n\ value - header field value\n\
idx - optional position in internal header list to insert new header\n\
Both are strings. This function can only be called from the EOM callback."; Both are strings. This function can only be called from the EOM callback.";
static PyObject * static PyObject *
milter_addheader(PyObject *self, PyObject *args) { milter_addheader(PyObject *self, PyObject *args) {
char *headerf; char *headerf;
char *headerv; char *headerv;
int idx = -1;
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
if (!PyArg_ParseTuple(args, "ss|i:addheader", &headerf, &headerv, &idx)) if (!PyArg_ParseTuple(args, "ss:addheader", &headerf, &headerv)) return NULL;
return NULL;
ctx = _find_context(self); ctx = _find_context(self);
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
t = PyEval_SaveThread(); t = PyEval_SaveThread();
#ifdef SMFIR_INSHEADER return _thread_return(t,smfi_addheader(ctx, headerf, headerv),
return _thread_return(t, (idx < 0) ? smfi_addheader(ctx, headerf, headerv) :
smfi_insheader(ctx, idx, headerf, headerv), "cannot add header");
#else
if (idx < 0)
return _thread_return(t, smfi_addheader(ctx, headerf, headerv),
"cannot add header"); "cannot add header");
PyErr_SetString(MilterError, "insheader not supported");
return NULL;
#endif
} }
static char milter_chgheader__doc__[] = static char milter_chgheader__doc__[] =
@@ -1163,7 +1081,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
} }
#endif #endif
#ifdef SMFIR_PROGRESS #if _FFR_SMFI_PROGRESS
static char milter_progress__doc__[] = static char milter_progress__doc__[] =
"progress() -> None\n\ "progress() -> None\n\
Notify the MTA that we are working on a message so it will reset timeouts."; Notify the MTA that we are working on a message so it will reset timeouts.";
@@ -1194,7 +1112,7 @@ static PyMethodDef context_methods[] = {
#ifdef SMFIF_QUARANTINE #ifdef SMFIF_QUARANTINE
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__}, { "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
#endif #endif
#ifdef SMFIR_PROGRESS #if _FFR_SMFI_PROGRESS
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__}, { "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
#endif #endif
{ NULL, NULL } { NULL, NULL }
@@ -1233,8 +1151,6 @@ static PyMethodDef milter_methods[] = {
{ "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__}, { "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__},
{ "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__}, { "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__},
{ "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__}, { "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__},
{ "set_exception_policy", milter_set_exception_policy,METH_VARARGS, milter_set_exception_policy__doc__},
{ "register", milter_register, METH_VARARGS, milter_register__doc__},
{ "register", milter_register, METH_VARARGS, milter_register__doc__}, { "register", milter_register, METH_VARARGS, milter_register__doc__},
{ "main", milter_main, METH_VARARGS, milter_main__doc__}, { "main", milter_main, METH_VARARGS, milter_main__doc__},
{ "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__}, { "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__},
+11 -51
View File
@@ -1,16 +1,4 @@
# $Log$ # $Log$
# Revision 1.4 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
# Revision 1.3 2005/06/02 15:00:17 customdesigned
# Configure banned extensions. Scan zipfile option with test case.
#
# Revision 1.2 2005/06/02 04:18:55 customdesigned
# Update copyright notices after reading article on /.
#
# Revision 1.1.1.4 2005/05/31 18:23:49 customdesigned
# Development changes since 0.7.2
#
# Revision 1.62 2005/02/14 22:31:17 stuart # Revision 1.62 2005/02/14 22:31:17 stuart
# _parseparam replacement not needed for python2.4 # _parseparam replacement not needed for python2.4
# #
@@ -74,13 +62,12 @@
# with a warning message. # with a warning message.
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. # Copyright 2001 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details. # This code is under GPL. See COPYING for details.
import StringIO import StringIO
import socket import socket
import Milter import Milter
import zipfile
import email import email
import email.Message import email.Message
@@ -93,16 +80,6 @@ from email import Errors
from types import ListType,StringType from types import ListType,StringType
def zipnames(txt):
fp = StringIO.StringIO(txt)
zipf = zipfile.ZipFile(fp,'r')
names = []
for nm in zipf.namelist():
names.append(('zipname',nm))
if nm.lower().endswith('.zip'):
names += zipnames(zipf.read(nm))
return names
class MimeGenerator(Generator): class MimeGenerator(Generator):
def _dispatch(self, msg): def _dispatch(self, msg):
# Get the Content-Type: for the message, then try to dispatch to # Get the Content-Type: for the message, then try to dispatch to
@@ -176,7 +153,7 @@ class MimeMessage(Message):
def getname(self): def getname(self):
return self.get_param('name') return self.get_param('name')
def getnames(self,scan_zip=False): def getnames(self):
"""Return a list of (attr,name) pairs of attributes that IE might """Return a list of (attr,name) pairs of attributes that IE might
interpret as a name - and hence decide to execute this message.""" interpret as a name - and hence decide to execute this message."""
names = [] names = []
@@ -191,14 +168,7 @@ class MimeMessage(Message):
else: else:
val = _unquotevalue(val.strip()) val = _unquotevalue(val.strip())
names.append((attr,val)) names.append((attr,val))
names += [("filename",self.get_filename())] return names + [("filename",self.get_filename())]
if scan_zip:
for key,name in tuple(names): # copy by converting to tuple
if name and name.lower().endswith('.zip'):
txt = self.get_payload(decode=True)
if txt.strip():
names += zipnames(txt)
return names
def ismodified(self): def ismodified(self):
"True if this message or a subpart has been modified." "True if this message or a subpart has been modified."
@@ -306,20 +276,11 @@ A copy of your original message was saved as '%s:%s'.
See your administrator. See your administrator.
""" """
def check_name(msg,savname=None,ckname=check_ext,scan_zip=False): def check_name(msg,savname=None,ckname=check_ext):
"Replace attachment with a warning if its name is suspicious." "Replace attachment with a warning if its name is suspicious."
try: for key,name in msg.getnames():
for key,name in msg.getnames(scan_zip):
badname = ckname(name) badname = ckname(name)
if badname: if badname:
if key == 'zipname':
badname = msg.get_filename()
break
else:
return Milter.CONTINUE
except zipfile.BadZipfile:
# a ZIP that is not a zip is very suspicious
badname = msg.get_filename()
hostname = socket.gethostname() hostname = socket.gethostname()
msg.set_payload(virus_msg % (badname,hostname,savname)) msg.set_payload(virus_msg % (badname,hostname,savname))
del msg["content-type"] del msg["content-type"]
@@ -327,6 +288,7 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
del msg["content-transfer-encoding"] del msg["content-transfer-encoding"]
name = "WARNING.TXT" name = "WARNING.TXT"
msg["Content-Type"] = "text/plain; name="+name msg["Content-Type"] = "text/plain; name="+name
break
return Milter.CONTINUE return Milter.CONTINUE
import email.Iterators import email.Iterators
@@ -347,11 +309,11 @@ check function(MimeMessage): int
# save call context for Python without nested_scopes # save call context for Python without nested_scopes
class _defang: class _defang:
def __init__(self,scan_html=True): def __init__(self):
self.scan_html = scan_html self.scan_html = True
def _chk_name(self,msg): def _chk_name(self,msg):
rc = check_name(msg,self._savname,self._check,self.scan_zip) rc = check_name(msg,self._savname,self._check)
if self.scan_html: if self.scan_html:
check_html(msg,self._savname) # remove scripts from HTML check_html(msg,self._savname) # remove scripts from HTML
if self.scan_rfc822: if self.scan_rfc822:
@@ -360,14 +322,12 @@ class _defang:
return check_attachments(msg,self._chk_name) return check_attachments(msg,self._chk_name)
return rc return rc
def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True, def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True):
scan_zip=False):
"""Compatible entry point. """Compatible entry point.
Replace all attachments with dangerous names.""" Replace all attachments with dangerous names."""
self._savname = savname self._savname = savname
self._check = check self._check = check
self.scan_rfc822 = scan_rfc822 self.scan_rfc822 = scan_rfc822
self.scan_zip = scan_zip
check_attachments(msg,self._chk_name) check_attachments(msg,self._chk_name)
if msg.ismodified(): if msg.ismodified():
return True return True
-34
View File
@@ -1,34 +0,0 @@
Subject: SPF %(result)s (POSSIBLE FORGERY)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy (or lack thereof) indicated that the above email was not
sent via an authorized SMTP server, but may still be legitimate. Since there
is no positive confirmation that the message is really from you, we have
to give it extra scrutiny - including verifying that the sender really
exists by sending you this DSN. We will remember this sender and not
bother you again for a while. You can avoid this message entirely for
legitimate mail by using an authorized SMTP server. Contact your mail
administrator and ask how to configure your email client to use an
authorized server.
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
those forgeries instead of bugging you about them.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
-31
View File
@@ -1,31 +0,0 @@
Subject: Critical SPF configuration error
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Your spf record has a permanent error. The error was:
%(perm_error)s
We will reinterpret your record using "lax" processing heuristics
which may result in your mail being accepted anyway. But you or your
mail administrator need to fix your SPF record as soon as possible.
We are sending you this message to alert you to the fact that
you have problems with your email configuration.
If you need further assistance, please do not hesitate to
contact me again.
Kind regards,
postmaster@%(receiver)s
-26
View File
@@ -1,26 +0,0 @@
Subject: DELIVERY STATUS (POSSIBLE SPAM)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
A statistical analysis of your message has classified it as junk mail,
and it has been quarantined. Eventually, the recipients will review
their quarantined mail and may notice your message. If your message is
important, please contact them via other means. You may also try sending
them a simple plain text message.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
-138
View File
@@ -1,138 +0,0 @@
# Analyze milter log to find abusers
import traceback
import sys
def parse_addr(a):
beg = a.find('<')
end = a.find('>')
if beg >= 0:
if end > beg: return a[beg+1:end]
return a
class Connection(object):
def __init__(self,dt,tm,id,ip=None,conn=None):
self.dt = dt
self.tm = tm
self.id = id
if ip:
_,self.host,self.ip = ip.split(None,2)
elif conn:
self.ip = conn.ip
self.host = conn.host
self.helo = conn.helo
self.subject = None
self.rcpt = []
self.mfrom = None
self.helo = None
self.innoc = []
self.whitelist = False
def connections(fp):
conndict = {}
termdict = {}
for line in fp:
if line.startswith('{'): continue
a = line.split(None,4)
if len(a) < 4: continue
dt,tm,id,op = a[:4]
if (id,op) == ('bms','milter'):
# FIXME: optionally yield all partial connections in conndict
conndict = {}
termdict = {}
continue
if id[0] == '[' and id[-1] == ']':
try:
key = int(id[1:-1])
except:
print >>sys.stderr,'bad id:',line.rstrip()
continue
else: continue
if op == 'connect':
ip = a[4].rstrip()
conn = Connection(dt,tm,id,ip=ip)
conndict[key] = conn
elif op in (
'DISCARD:','TAG:','CBV:','Large','No',
'NOTE:','From:','Sender:','TRAIN:'):
continue
else:
op = op.lower()
try:
conn = conndict[key]
except KeyError:
try:
conn = termdict[key]
del termdict[key]
conndict[key] = conn
except KeyError:
print >>sys.stderr,'key error:',line.rstrip()
continue
try:
if op == 'subject:':
if len(a) > 4:
conn.subject = a[4].rstrip()
elif op == 'innoc:':
conn.innoc.append(a[4].rstrip())
elif op == 'whitelist':
conn.whitelist = True
elif op == 'x-mailer:':
if len(a) > 4:
conn.mailer = a[4].rstrip()
elif op == 'x-guessed-spf:':
conn.spfguess = a[4]
elif op == 'received-spf:':
conn.spfres,conn.spfmsg = a[4].rstrip().split(None,1)
elif op == 'received:':
conn.received = a[4].rstrip()
elif op == 'temp':
_,conn.tempfile = a[4].rstrip().split(None,1)
elif op == 'srs':
_,conn.srsrcpt = a[4].rstrip().split(None,1)
elif op == 'mail':
_,conn.mfrom = a[4].rstrip().split(None,1)
elif op == 'rcpt':
_,rcpt = a[4].rstrip().split(None,1)
conn.rcpt.append(rcpt)
elif op == 'hello':
_,conn.helo = a[4].rstrip().split(None,1)
elif op in ('eom','dspam','abort'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('reject:','dspam:','tempfail:','reject','fail:','honeypot:'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
conn.resmsg = a[4].rstrip()
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('fp:','spam:'):
del conndict[key]
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
else:
print >>sys.stderr,'unknown op:',line.rstrip()
except Exception:
print >>sys.stderr,'error:',line.rstrip()
traceback.print_exc()
if __name__ == '__main__':
import gzip
for fn in sys.argv[1:]:
if fn.endswith('.gz'):
fp = gzip.open(fn)
else:
fp = open(fn)
for conn in connections(fp):
if conn.rcpt and conn.mfrom:
for r in conn.rcpt:
if r.lower().find('iancarter') > 0: break
else:
if conn.mfrom.lower().find('iancarter') < 0: continue
print >>sys.stderr,conn.result,conn.dt,conn.tm,conn.id,conn.subject,parse_addr(conn.mfrom),
for a in conn.rcpt:
print parse_addr(a),
print
+1 -2
View File
@@ -1,5 +1,4 @@
[bdist_rpm] [bdist_rpm]
python=python2.4 python=python2
doc_files=README NEWS TODO doc_files=README NEWS TODO
packager=Stuart D. Gathman <stuart@bmsi.com> packager=Stuart D. Gathman <stuart@bmsi.com>
release=1
+1 -3
View File
@@ -12,8 +12,7 @@ if sys.version < '2.2.3':
DistributionMetadata.classifiers = None DistributionMetadata.classifiers = None
DistributionMetadata.download_url = None DistributionMetadata.download_url = None
# NOTE: importing Milter to obtain version fails when milter.so not built setup(name = "milter", version = "0.8.0",
setup(name = "milter", version = '0.8.5',
description="Python interface to sendmail milter API", description="Python interface to sendmail milter API",
long_description="""\ long_description="""\
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
@@ -32,7 +31,6 @@ querying SPF records.
ext_modules=[ ext_modules=[
Extension("milter", ["miltermodule.c"], Extension("milter", ["miltermodule.c"],
libraries=libs, libraries=libs,
# set MAX_ML_REPLY to 1 for sendmail < 8.13
define_macros = [ ('MAX_ML_REPLY',32) ] define_macros = [ ('MAX_ML_REPLY',32) ]
), ),
], ],
-25
View File
@@ -1,25 +0,0 @@
Subject: SPF %(result)s (POSSIBLE FORGERY)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy indicated that the above email was likely forged and that
feedback was desired. If you are sending from a foreign ISP,
then you may need to follow your home ISPs instructions for configuring
your outgoing mail server.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
+232 -444
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
"""SPF (Sender Policy Framework) implementation. """SPF (Sender-Permitted From) implementation.
Copyright (c) 2003, Terence Way Copyright (c) 2003, Terence Way
Portions Copyright (c) 2004,2005 Stuart Gathman <stuart@bmsi.com>
This module is free software, and you may redistribute it and/or modify This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form. and disclaimer are retained in their original form.
@@ -19,11 +18,10 @@ AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
For more information about SPF, a tool against email forgery, see For more information about SPF, a tool against email forgery, see
http://openspf.org/ http://spf.pobox.com
For news, bugfixes, etc. visit the home page for this implementation at For news, bugfixes, etc. visit the home page for this implementation at
http://www.wayforward.net/spf/ http://www.wayforward.net/spf/
http://sourceforge.net/projects/pymilter/
""" """
# Changes: # Changes:
@@ -47,153 +45,6 @@ For news, bugfixes, etc. visit the home page for this implementation at
# Terrence is not responding to email. # Terrence is not responding to email.
# #
# $Log$ # $Log$
# Revision 1.17 2005/12/23 21:44:15 customdesigned
# Always include keyword data in Received-SPF header.
#
# Revision 1.16 2005/12/01 22:42:32 customdesigned
# improve gossip support.
# Initialize srs_domain from srs.srs config property. Should probably
# always block unsigned DSN when signing all.
#
# Revision 1.15 2005/10/30 01:08:14 customdesigned
# Ignore records missing spaces.
#
# Revision 1.14 2005/08/12 17:36:51 customdesigned
# Trean non-existant include as no match in "lax" mode.
#
# Revision 1.13 2005/07/22 16:00:23 customdesigned
# Limit CNAME chains independently of DNS lookup limit
#
# Revision 1.31 2005/07/22 02:11:50 customdesigned
# Use dictionary to check for CNAME loops. Check limit independently for
# each top level name, just like for PTR.
#
# Revision 1.30 2005/07/21 20:07:31 customdesigned
# Translate DNS error in DNSLookup. This completely isolates DNS
# dependencies to the DNSLookup method.
#
# Revision 1.29 2005/07/21 17:49:39 customdesigned
# My best guess at what RFC intended for limiting CNAME loops.
#
# Revision 1.28 2005/07/21 17:37:08 customdesigned
# Break out external DNSLookup method so that test suite can
# duplicate CNAME loop bug. Test zone data dictionary now
# mirrors structure of real DNS.
#
# Revision 1.27 2005/07/21 15:26:06 customdesigned
# First cut at updating docs. Test suite is obsolete.
#
# Revision 1.26 2005/07/20 03:12:40 customdesigned
# When not in strict mode, don't give PermErr for bad mechanism until
# encountered during evaluation.
#
# Revision 1.25 2005/07/19 23:24:42 customdesigned
# Validate all mechanisms before evaluating.
#
# Revision 1.24 2005/07/19 18:11:52 kitterma
# Fix to change that compares type TXT and type SPF records. Bug in the change
# prevented records from being returned if it was published as TXT, but not SPF.
#
# Revision 1.23 2005/07/19 15:22:50 customdesigned
# MX and PTR limits are MUST NOT check limits, and do not result in PermErr.
# Also, check belongs in mx and ptr specific methods, not in dns() method.
#
# Revision 1.22 2005/07/19 05:02:29 customdesigned
# FQDN test was broken. Added test case. Move FQDN test to after
# macro expansion.
#
# Revision 1.21 2005/07/18 20:46:27 kitterma
# Fixed reference problem in 1.20
#
# Revision 1.20 2005/07/18 20:21:47 kitterma
# Change to dns_spf to go ahead and check for a type 99 (SPF) record even if a
# TXT record is found and make sure if type SPF is present that they are
# identical when using strict processing.
#
# Revision 1.19 2005/07/18 19:36:00 kitterma
# Change to require at least one dot in a domain name. Added PermError
# description to indicate FQDN should be used. This is a common error.
#
# Revision 1.18 2005/07/18 17:13:37 kitterma
# Change macro processing to raise PermError on an unknown macro.
# schlitt-spf-classic-02 para 8.1. Change exp modifier processing to ignore
# exp strings with syntax errors. schlitt-spf-classic-02 para 6.2.
#
# Revision 1.17 2005/07/18 14:35:34 customdesigned
# Remove debugging printf
#
# Revision 1.16 2005/07/18 14:34:14 customdesigned
# Forgot to remove debugging print
#
# Revision 1.15 2005/07/15 21:17:36 customdesigned
# Recursion limit raises AssertionError in strict mode, PermError otherwise.
#
# Revision 1.14 2005/07/15 20:34:11 customdesigned
# Check whether DNS package already supports SPF before patching
#
# Revision 1.13 2005/07/15 20:01:22 customdesigned
# Allow extended results for MX limit
#
# Revision 1.12 2005/07/15 19:12:09 customdesigned
# Official IANA SPF record (type 99) support.
#
# Revision 1.11 2005/07/15 18:03:02 customdesigned
# Fix unknown Received-SPF header broken by result changes
#
# Revision 1.10 2005/07/15 16:17:05 customdesigned
# Start type99 support.
# Make Scott's "/" support in parse_mechanism more elegant as requested.
# Add test case for "/" support.
#
# Revision 1.9 2005/07/15 03:33:14 kitterma
# Fix for bug 1238403 - Crash if non-CIDR / present. Also added
# validation check for valid IPv4 CIDR range.
#
# Revision 1.8 2005/07/14 04:18:01 customdesigned
# Bring explanations and Received-SPF header into line with
# the unknown=PermErr and error=TempErr convention.
# Hope my case-sensitive mech fix doesn't clash with Scotts.
#
# Revision 1.7 2005/07/12 21:43:56 kitterma
# Added processing to clarify some cases of unknown
# qualifier errors (to distinguish between unknown qualifier and
# unknown mechanism).
# Also cleaned up comments from previous updates.
#
# Revision 1.6 2005/06/29 14:46:26 customdesigned
# Distinguish trivial recursion from missing arg for diagnostic purposes.
#
# Revision 1.5 2005/06/28 17:48:56 customdesigned
# Support extended processing results when a PermError should strictly occur.
#
# Revision 1.4 2005/06/22 15:54:54 customdesigned
# Correct spelling.
#
# Revision 1.3 2005/06/22 00:08:24 kitterma
# Changes from draft-mengwong overall DNS lookup and recursion
# depth limits to draft-schlitt-spf-classic-02 DNS lookup, MX lookup, and
# PTR lookup limits. Recursion code is still present and functioning, but
# it should be impossible to trip it.
#
# Revision 1.2 2005/06/21 16:46:09 kitterma
# Updated definition of SPF, added reference to the sourceforge project site,
# and deleted obsolete Microsoft Caller ID for Email XML translation routine.
#
# Revision 1.1.1.1 2005/06/20 19:57:32 customdesigned
# Move Python SPF to its own module.
#
# Revision 1.5 2005/06/14 20:31:26 customdesigned
# fix pychecker nits
#
# Revision 1.4 2005/06/02 04:18:55 customdesigned
# Update copyright notices after reading article on /.
#
# Revision 1.3 2005/06/02 02:08:12 customdesigned
# Reject on PermErr
#
# Revision 1.2 2005/05/31 18:57:59 customdesigned
# Clear unknown mechanism list at proper time.
#
# Revision 1.24 2005/03/16 21:58:39 stuart # Revision 1.24 2005/03/16 21:58:39 stuart
# Change Milter module to package. # Change Milter module to package.
# #
@@ -283,25 +134,135 @@ import struct # for pack() and unpack()
import time # for time() import time # for time()
import DNS # http://pydns.sourceforge.net import DNS # http://pydns.sourceforge.net
if not hasattr(DNS.Type,'SPF'): import xml.sax
# patch in type99 support
DNS.Type.SPF = 99
DNS.Type.typemap[99] = 'SPF'
DNS.Lib.RRunpacker.getSPFdata = DNS.Lib.RRunpacker.getTXTdata
def DNSLookup(name,qtype): # -------------------------------------------------------------------------
try: # Convert a MS Caller-ID entry (XML) to a SPF entry
req = DNS.DnsRequest(name, qtype=qtype) #
resp = req.req() # (c) 2004 by Ernesto Baschny
#resp.show() # (c) 2004 Python version by Stuart Gathman
# key k: ('wayforward.net', 'A'), value v #
return [((a['name'], a['typename']), a['data']) for a in resp.answers] # Date: 2004-02-25
except DNS.DNSError,x: #
raise TempError,'DNS ' + str(x) # A complete reverse translation (SPF -> CID) might be impossible, since
# there are no ways to handle:
# - PTR and EXISTS mechanism
# - MX mechanism with an different domain as argument
# - macros
#
# References:
# http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx
# http://spf.pobox.com/
#
# Known bugs:
# - Currently it won't handle the exclusions provided in the A and R
# tags (prefix '!'). They will show up "as-is" in the SPF record
# - I really haven't read the MS-CID specs in-depth, so there are probably
# other bugs too :)
#
# Ernesto Baschny <ernst@baschny.de>
#
def isSPF(txt): class CIDParser(xml.sax.ContentHandler):
"Return True if txt has SPF record signature." "Convert a MS Caller-ID entry (XML) to a SPF entry."
return txt.startswith('v=spf1 ') or txt == 'v=spf1'
def __init__(self,q=None):
self.spf = []
self.action = '-all'
self.has_servers = None
self.spf_entry = None
if q:
self.spf_query = q
else:
self.spf_query = query(i='127.0.0.1', s='localhost', h='unknown')
def startElement(self,tag,attr):
if tag == 'm':
if self.has_servers != None and not self.has_servers:
raise ValueError(
"Declared <noMailServers\> and later <m>, this CID entry is not valid."
)
self.has_servers = True
elif tag == 'noMailServers':
if self.has_servers:
raise ValueError(
"Declared <m> and later <noMailServers\>, this CID entry is not valid."
)
self.has_servers = False
elif tag == 'ep':
if attr.has_key('testing') and attr.getValue('testing') == 'true':
# A CID with 'testing' found:
# From the MS-specs:
# "Documents in which such attribute is present with a true
# value SHOULD be entirely ignored (one should act as if the
# document were absent)"
# From the SPF-specs:
# "Neutral (?): The SPF client MUST proceed as if a domain did
# not publish SPF data."
# So we set SPF action to "neutral":
self.action = '?all'
elif tag == 'mx':
# The empty MX-tag, same as SPF's MX-mechanism
self.spf.append('mx')
self.tag = tag
def characters(self,text):
tag = self.tag
# Remove starting and trailing spaces from text:
text = text.strip()
if tag == 'a' or tag == 'r':
# The A and R tags from MS-CID are both handled by the
# ipv4/6-mechanisms from SPF:
if text.find(':') < 0:
mechanism = 'ip4'
else:
mechanism = 'ip6'
self.spf.append(mechanism + ':' + text)
elif tag == 'indirect':
# MS-CID's indirect is "sort of" the include from SPF:
# Not really true, because the <indirect> tag from MS-CID also
# provides a fallback in case the included domain doesn't provide
# _ep-records: The inbound MX-servers of the included domains
# are added to the list of allowed outgoing mailservers for the
# domain that declared the _ep-record with the <indirect> tag.
# In SPF you would use the 'mx:domain' to handle this, but this
# wouldn't depend on referred domain having or not SPF-records.
cid_xml = self.cid_txt(text)
if cid_xml:
p = CIDParser()
xml.sax.parseString(cid_xml,p)
if p.has_servers != False:
self.spf += p.spf
else:
self.spf.append('mx:' + text)
def cid_txt(self,domain):
q = self.spf_query
domain='_ep.' + domain
a = q.dns_txt(domain)
if not a: return None
if a[0].lower().startswith('<ep ') and a[-1].lower().endswith('</ep>'):
return ''.join(a)
return None
def endElement(self,tag):
if tag == 'ep':
# This is the end... assemble what we've got
spf_entry = ['v=spf1']
if self.has_servers != False:
spf_entry += self.spf
spf_entry.append(self.action)
self.spf_entry = ' '.join(spf_entry)
def spf_txt(self,cid_xml):
if not cid_xml.startswith('<'):
cid_xml = self.cid_txt(cid_xml)
if not cid_xml: return None
# Parse the beast. Any XML-problem will be reported by xlm.sax
self.spf_entry = None
xml.sax.parseString(cid_xml,self)
return self.spf_entry
# 32-bit IPv4 address mask # 32-bit IPv4 address mask
MASK = 0xFFFFFFFFL MASK = 0xFFFFFFFFL
@@ -315,8 +276,6 @@ RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))')
# Regular expression to break up a macro expansion # Regular expression to break up a macro expansion
RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)') RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)')
RE_CIDR = re.compile(r'/([1-9]|1[0-9]*|2[0-9]*|3[0-2]*)$')
# Local parts and senders have their delimiters replaced with '.' during # Local parts and senders have their delimiters replaced with '.' during
# macro expansion # macro expansion
# #
@@ -324,12 +283,11 @@ JOINERS = {'l': '.', 's': '.'}
RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail', RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown', 'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown',
'error': 'error', 'neutral': 'neutral', 'softfail': 'softfail', 'neutral': 'neutral', 'softfail': 'softfail',
'none': 'none', 'deny': 'fail' } 'none': 'none', 'deny': 'fail' }
EXPLANATIONS = {'pass': 'sender SPF verified', 'fail': 'access denied', EXPLANATIONS = {'pass': 'sender SPF verified', 'fail': 'access denied',
'unknown': 'permanent error in processing', 'unknown': 'SPF unknown',
'error': 'temporary error in processing',
'softfail': 'domain in transition', 'softfail': 'domain in transition',
'neutral': 'access neither permitted nor denied', 'neutral': 'access neither permitted nor denied',
'none': '' 'none': ''
@@ -348,28 +306,22 @@ except NameError:
def bool(x): return not not x def bool(x): return not not x
# ...pre 2.2.1 # ...pre 2.2.1
# standard default SPF record for best_guess # standard default SPF record
DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr' DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
# maximum DNS lookups allowed # maximum DNS lookups allowed
MAX_LOOKUP = 10 #draft-schlitt-spf-classic-02 Para 10.1 MAX_LOOKUP = 100
MAX_MX = 10 #draft-schlitt-spf-classic-02 Para 10.1
MAX_PTR = 10 #draft-schlitt-spf-classic-02 Para 10.1
MAX_CNAME = 10 # analogous interpretation to MAX_PTR
MAX_RECURSION = 20 MAX_RECURSION = 20
ALL_MECHANISMS = ('a', 'mx', 'ptr', 'exists', 'include', 'ip4', 'ip6', 'all')
COMMON_MISTAKES = { 'prt': 'ptr', 'ip': 'ip4', 'ipv4': 'ip4', 'ipv6': 'ip6' }
class TempError(Exception): class TempError(Exception):
"Temporary SPF error" "Temporary SPF error"
class PermError(Exception): class PermError(Exception):
"Permanent SPF error" "Permanent SPF error"
def __init__(self,msg,mech=None,ext=None): def __init__(self,msg,mech=None):
Exception.__init__(self,msg,mech) Exception.__init__(self,msg,mech)
self.msg = msg self.msg = msg
self.mech = mech self.mech = mech
self.ext = ext
def __str__(self): def __str__(self):
if self.mech: if self.mech:
return '%s: %s'%(self.msg,self.mech) return '%s: %s'%(self.msg,self.mech)
@@ -410,7 +362,7 @@ class query(object):
Also keeps cache: DNS cache. Also keeps cache: DNS cache.
""" """
def __init__(self, i, s, h,local=None,receiver=None,strict=True): def __init__(self, i, s, h,local=None,receiver=None):
self.i, self.s, self.h = i, s, h self.i, self.s, self.h = i, s, h
if not s and h: if not s and h:
self.s = 'postmaster@' + h self.s = 'postmaster@' + h
@@ -425,8 +377,6 @@ class query(object):
self.exps = dict(EXPLANATIONS) self.exps = dict(EXPLANATIONS)
self.local = local # local policy self.local = local # local policy
self.lookups = 0 self.lookups = 0
# strict can be False, True, or 2 for harsh
self.strict = strict
def set_default_explanation(self,exp): def set_default_explanation(self,exp):
exps = self.exps exps = self.exps
@@ -448,47 +398,10 @@ class query(object):
def check(self, spf=None): def check(self, spf=None):
""" """
Returns (result, mta-status-code, explanation) where result Returns (result, mta-status-code, explanation) where
in ['fail', 'softfail', 'neutral' 'unknown', 'pass', 'error', 'none'] result in ['fail', 'softfail', 'neutral' 'unknown', 'pass', 'error']
Examples:
>>> q = query(s='strong-bad@email.example.com',
... h='mx.example.org', i='192.0.2.3')
>>> q.check(spf='v=spf1 ?all')
('neutral', 250, 'access neither permitted nor denied')
>>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ?all moo')
('unknown', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
>>> q.check(spf='v=spf1 =a ?all moo')
('unknown', 550, 'SPF Permanent Error: Unknown qualifier, IETF draft para 4.6.1, found in: =a')
>>> q.check(spf='v=spf1 ip4:192.0.0.0/8 ~all')
('pass', 250, 'sender SPF verified')
>>> q.strict = False
>>> q.check(spf='v=spf1 ip4:192.0.0.0/8 -all moo')
('pass', 250, 'sender SPF verified')
>>> q.check(spf='v=spf1 ip4:192.1.0.0/16 moo -all')
('unknown', 550, 'SPF Permanent Error: Unknown mechanism found: moo')
>>> q.check(spf='v=spf1 ip4:192.1.0.0/16 ~all')
('softfail', 250, 'domain in transition')
>>> q.check(spf='v=spf1 -ip4:192.1.0.0/6 ~all')
('fail', 550, 'access denied')
# Assumes DNS available
>>> q.check()
('none', 250, '')
""" """
self.mech = [] # unknown mechanisms self.mech = [] # unknown mechanisms
# If not strict, certain PermErrors (mispelled
# mechanisms, strict processing limits exceeded)
# will continue processing. However, the exception
# that strict processing would raise is saved here
self.perm_error = None
if self.i.startswith('127.'): if self.i.startswith('127.'):
return ('pass', 250, 'local connections always pass') return ('pass', 250, 'local connections always pass')
@@ -498,104 +411,30 @@ class query(object):
spf = self.dns_spf(self.d) spf = self.dns_spf(self.d)
if self.local and spf: if self.local and spf:
spf += ' ' + self.local spf += ' ' + self.local
rc = self.check1(spf, self.d, 0) return self.check1(spf, self.d, 0)
if self.perm_error: except DNS.DNSError,x:
# extended processing succeeded, but strict failed return ('error', 450, 'SPF DNS Error: ' + str(x))
self.perm_error.ext = rc
raise self.perm_error
return rc
except TempError,x: except TempError,x:
return ('error', 450, 'SPF Temporary Error: ' + str(x)) return ('error', 450, 'SPF Temporary Error: ' + str(x))
except PermError,x: except PermError,x:
self.prob = x.msg
if x.mech:
self.mech.append(x.mech)
# Pre-Lentczner draft treats this as an unknown result # Pre-Lentczner draft treats this as an unknown result
# and equivalent to no SPF record. # and equivalent to no SPF record.
self.prob = x.msg
self.mech.append(x.mech)
return ('unknown', 550, 'SPF Permanent Error: ' + str(x)) return ('unknown', 550, 'SPF Permanent Error: ' + str(x))
def check1(self, spf, domain, recursion): def check1(self, spf, domain, recursion):
# spf rfc: 3.7 Processing Limits # spf rfc: 3.7 Processing Limits
# #
if recursion > MAX_RECURSION: if recursion > MAX_RECURSION:
# This should never happen in strict mode self.prob = 'Too many levels of recursion'
# because of the other limits we check, return ('unknown', 250, 'SPF recursion limit exceeded')
# so if it does, there is something wrong with
# our code. It is not a PermError because there is not
# necessarily anything wrong with the SPF record.
if self.strict:
raise AssertionError('Too many levels of recursion')
# As an extended result, however, it should be
# a PermError.
raise PermError('Too many levels of recursion')
try: try:
tmp, self.d = self.d, domain tmp, self.d = self.d, domain
return self.check0(spf,recursion) return self.check0(spf,recursion)
finally: finally:
self.d = tmp self.d = tmp
def validate_mechanism(self,mech):
"""Parse and validate a mechanism.
Returns mech,m,arg,cidrlength,result
Examples:
>>> q = query(s='strong-bad@email.example.com',
... h='mx.example.org', i='192.0.2.3')
>>> q.validate_mechanism('A')
('A', 'a', 'email.example.com', 32, 'pass')
>>> q.validate_mechanism('?mx:%{d}/27')
('?mx:%{d}/27', 'mx', 'email.example.com', 27, 'neutral')
>>> q.validate_mechanism('-mx::%%%_/.Clara.de/27')
('-mx::%%%_/.Clara.de/27', 'mx', ':% /.Clara.de', 27, 'fail')
>>> q.validate_mechanism('~exists:%{i}.%{s1}.100/86400.rate.%{d}')
('~exists:%{i}.%{s1}.100/86400.rate.%{d}', 'exists', '192.0.2.3.com.100/86400.rate.email.example.com', 32, 'softfail')
"""
# a mechanism
m, arg, cidrlength = parse_mechanism(mech, self.d)
# map '?' '+' or '-' to 'unknown' 'pass' or 'fail'
if m:
result = RESULTS.get(m[0])
if result:
# eat '?' '+' or '-'
m = m[1:]
else:
# default pass
result = 'pass'
if m in COMMON_MISTAKES:
try:
raise PermError('Unknown mechanism found',mech)
except PermError, x:
if self.strict: raise
m = COMMON_MISTAKES[m]
if not self.perm_error:
self.perm_error = x
if m in ('a', 'mx', 'ptr', 'exists', 'include'):
arg = self.expand(arg)
if not (0 < arg.find('.') < len(arg) - 1):
raise PermError('Invalid domain found (use FQDN)',
arg)
if m == 'include':
if arg == self.d:
if mech != 'include':
raise PermError('include has trivial recursion',mech)
raise PermError('include mechanism missing domain',mech)
return mech,m,arg,cidrlength,result
if m in ALL_MECHANISMS:
return mech,m,arg,cidrlength,result
try:
if m[1:] in ALL_MECHANISMS:
raise PermError(
'Unknown qualifier, IETF draft para 4.6.1, found in',
mech)
raise PermError('Unknown mechanism found',mech)
except PermError, x:
if self.strict: raise
return mech,m,arg,cidrlength,x
def check0(self, spf,recursion): def check0(self, spf,recursion):
"""Test this query information against SPF text. """Test this query information against SPF text.
@@ -607,15 +446,8 @@ class query(object):
return ('none', 250, EXPLANATIONS['none']) return ('none', 250, EXPLANATIONS['none'])
# split string by whitespace, drop the 'v=spf1' # split string by whitespace, drop the 'v=spf1'
spf = spf.split() #
# Catch case where SPF record has no spaces spf = spf.split()[1:]
# Can never happen with conforming dns_spf(), however
# in the future we might want to give permerror
# for common mistakes like IN TXT "v=spf1" "mx" "-all"
# in relaxed mode.
if spf[0] != 'v=spf1':
raise PermError('Invalid SPF record in', self.d)
spf = spf[1:]
# copy of explanations to be modified by exp= # copy of explanations to be modified by exp=
exps = self.exps exps = self.exps
@@ -625,23 +457,17 @@ class query(object):
# overridden with 'default=' modifier # overridden with 'default=' modifier
# #
default = 'neutral' default = 'neutral'
mechs = []
# Look for modifiers # Look for modifiers
# #
for mech in spf: for m in spf:
m = RE_MODIFIER.split(mech)[1:] m = RE_MODIFIER.split(m)[1:]
if len(m) != 2: if len(m) != 2: continue
mechs.append(self.validate_mechanism(mech))
continue
if m[0] == 'exp': if m[0] == 'exp':
try: exps['fail'] = exps['unknown'] = \
self.set_default_explanation(self.get_explanation(m[1])) self.get_explanation(m[1])
except PermError:
pass
elif m[0] == 'redirect': elif m[0] == 'redirect':
self.check_lookups()
redirect = self.expand(m[1]) redirect = self.expand(m[1])
elif m[0] == 'default': elif m[0] == 'default':
# default=- is the same as default=fail # default=- is the same as default=fail
@@ -649,65 +475,77 @@ class query(object):
# spf rfc: 3.6 Unrecognized Mechanisms and Modifiers # spf rfc: 3.6 Unrecognized Mechanisms and Modifiers
# Evaluate mechanisms # Look for mechanisms
# #
for mech,m,arg,cidrlength,result in mechs: for mech in spf:
if RE_MODIFIER.match(mech): continue
m, arg, cidrlength = parse_mechanism(mech, self.d)
# map '?' '+' or '-' to 'unknown' 'pass' or 'fail'
if m:
result = RESULTS.get(m[0])
if result:
# eat '?' '+' or '-'
m = m[1:]
else:
# default pass
result = 'pass'
if m in ['a', 'mx', 'ptr', 'prt', 'exists', 'include']:
arg = self.expand(arg)
if m == 'include': if m == 'include':
self.check_lookups() if arg != self.d:
res,code,txt = self.check1(self.dns_spf(arg), res,code,txt = self.check1(self.dns_spf(arg),
arg, recursion + 1) arg, recursion + 1)
if res == 'pass': if res == 'pass':
break break
if res == 'none': if res == 'none':
try:
if self.strict or not self.perm_error:
raise PermError( raise PermError(
'No valid SPF record for included domain: %s'%arg, 'No valid SPF record for included domain: %s'%arg,
mech) mech)
except PermError,x:
if self.strict:
raise x
self.perm_error = x
continue continue
else:
raise PermError('include mechanism missing domain',mech)
elif m == 'all': elif m == 'all':
break break
elif m == 'exists': elif m == 'exists':
self.check_lookups()
if len(self.dns_a(arg)) > 0: if len(self.dns_a(arg)) > 0:
break break
elif m == 'a': elif m == 'a':
self.check_lookups() if cidrmatch(self.i, self.dns_a(arg),
if cidrmatch(self.i, self.dns_a(arg), cidrlength): cidrlength):
break break
elif m == 'mx': elif m == 'mx':
self.check_lookups() if cidrmatch(self.i, self.dns_mx(arg),
if cidrmatch(self.i, self.dns_mx(arg), cidrlength): cidrlength):
break break
elif m == 'ip4' and arg != self.d: elif m in ('ip4', 'ipv4', 'ip') and arg != self.d:
try: try:
if cidrmatch(self.i, [arg], cidrlength): if cidrmatch(self.i, [arg], cidrlength):
break break
except socket.error: except socket.error:
raise PermError('syntax error',mech) raise PermError('syntax error',mech)
elif m == 'ip6': elif m in ('ip6', 'ipv6'):
# Until we support IPV6, we should never # Until we support IPV6, we should never
# get an IPv6 connection. So this mech # get an IPv6 connection. So this mech
# will never match. # will never match.
pass pass
elif m == 'ptr': elif m in ('ptr', 'prt'):
self.check_lookups() if domainmatch(self.validated_ptrs(self.i),
if domainmatch(self.validated_ptrs(self.i), arg): arg):
break break
else: else:
raise result # unknown mechanisms cause immediate unknown
# abort results
raise PermError('Unknown mechanism found',mech)
else: else:
# no matches # no matches
if redirect: if redirect:
@@ -721,17 +559,6 @@ class query(object):
else: else:
return (result, 250, exps[result]) return (result, 250, exps[result])
def check_lookups(self):
self.lookups = self.lookups + 1
if self.lookups > MAX_LOOKUP:
try:
if self.strict or not self.perm_error:
raise PermError('Too many DNS lookups')
except PermError,x:
if self.strict or self.lookups > MAX_LOOKUP*4:
raise x
self.perm_error = x
def get_explanation(self, spec): def get_explanation(self, spec):
"""Expand an explanation.""" """Expand an explanation."""
if spec: if spec:
@@ -823,10 +650,8 @@ class query(object):
letter = macro[2].lower() letter = macro[2].lower()
if letter == 'p': if letter == 'p':
self.getp() self.getp()
expansion = getattr(self, letter, 'Macro Error') expansion = getattr(self, letter, '')
if expansion: if expansion:
if expansion == 'Macro Error':
raise PermError('Unknown Macro Encountered')
result += expand_one(expansion, result += expand_one(expansion,
macro[3:-1], macro[3:-1],
JOINERS.get(letter)) JOINERS.get(letter))
@@ -839,27 +664,24 @@ class query(object):
name. Returns None if not found, or if more than one record name. Returns None if not found, or if more than one record
is found. is found.
""" """
# for performance, check for most common case of TXT first a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
a = [t for t in self.dns_txt(domain) if isSPF(t)] if not a:
if len(a) == 1 and self.strict < 2: if DELEGATE:
return a[0]
# check official SPF type first when it becomes more popular
b = [t for t in self.dns_99(domain) if isSPF(t)]
if len(b) == 1:
# FIXME: really must fully parse each record
# and compare with appropriate parts case insensitive.
if self.strict >= 2 and len(a) == 1 and a[0] != b[0]:
raise PermError(
'v=spf1 records of both type TXT and SPF (type 99) present, but not identical')
return b[0]
if len(a) == 1:
return a[0] # return TXT if SPF wasn't found
if DELEGATE: # use local record if neither found
a = [t a = [t
for t in self.dns_txt(domain+'._spf.'+DELEGATE) for t in self.dns_txt(domain+'._spf.'+DELEGATE)
if isSPF(t) if t.startswith('v=spf1')
] ]
if len(a) == 1: return a[0] if not a:
# No SPF record: convert and return CID if present
p = CIDParser(q=self)
try:
return p.spf_txt(domain)
except xml.sax._exceptions.SAXParseException,x:
raise PermError("Caller-ID parse error",domain)
if len(a) == 1:
return a[0]
else:
return None return None
def dns_txt(self, domainname): def dns_txt(self, domainname):
@@ -867,30 +689,17 @@ class query(object):
if domainname: if domainname:
return [''.join(a) for a in self.dns(domainname, 'TXT')] return [''.join(a) for a in self.dns(domainname, 'TXT')]
return [] return []
def dns_99(self, domainname):
"Get a list of type SPF=99 records for a domain name."
if domainname:
return [''.join(a) for a in self.dns(domainname, 'SPF')]
return []
def dns_mx(self, domainname): def dns_mx(self, domainname):
"""Get a list of IP addresses for all MX exchanges for a """Get a list of IP addresses for all MX exchanges for a
domain name. domain name.
""" """
# draft-schlitt-spf-classic-02 section 5.4 "mx" return [a for mx in self.dns(domainname, 'MX') \
# To prevent DoS attacks, more than 10 MX names MUST NOT be looked up
if self.strict:
max = MAX_MX
else:
max = MAX_MX * 4
return [a for mx in self.dns(domainname, 'MX')[:max] \
for a in self.dns_a(mx[1])] for a in self.dns_a(mx[1])]
def dns_a(self, domainname): def dns_a(self, domainname):
"""Get a list of IP addresses for a domainname.""" """Get a list of IP addresses for a domainname."""
if domainname:
return self.dns(domainname, 'A') return self.dns(domainname, 'A')
return []
def dns_aaaa(self, domainname): def dns_aaaa(self, domainname):
"""Get a list of IPv6 addresses for a domainname.""" """Get a list of IPv6 addresses for a domainname."""
@@ -900,18 +709,13 @@ class query(object):
"""Figure out the validated PTR domain names for a given IP """Figure out the validated PTR domain names for a given IP
address. address.
""" """
# To prevent DoS attacks, more than 10 PTR names MUST NOT be looked up return [p for p in self.dns_ptr(i) if i in self.dns_a(p)]
if self.strict:
max = MAX_PTR
else:
max = MAX_PTR * 4
return [p for p in self.dns_ptr(i)[:max] if i in self.dns_a(p)]
def dns_ptr(self, i): def dns_ptr(self, i):
"""Get a list of domain names for an IP address.""" """Get a list of domain names for an IP address."""
return self.dns(reverse_dots(i) + ".in-addr.arpa", 'PTR') return self.dns(reverse_dots(i) + ".in-addr.arpa", 'PTR')
def dns(self, name, qtype, cnames=None): def dns(self, name, qtype):
"""DNS query. """DNS query.
If the result is in cache, return that. Otherwise pull the If the result is in cache, return that. Otherwise pull the
@@ -925,36 +729,34 @@ class query(object):
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
post: isinstance(__return__, types.ListType) post: isinstance(__return__, types.ListType)
""" """
self.lookups += 1
if self.lookups > MAX_LOOKUP:
raise PermError('Too many DNS lookups')
result = self.cache.get( (name, qtype) ) result = self.cache.get( (name, qtype) )
cname = None cname = None
if not result: if not result:
for k,v in DNSLookup(name,qtype): req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req()
for a in resp.answers:
# key k: ('wayforward.net', 'A'), value v
k, v = (a['name'], a['typename']), a['data']
if k == (name, 'CNAME'): if k == (name, 'CNAME'):
cname = v cname = v
self.cache.setdefault(k, []).append(v) self.cache.setdefault(k, []).append(v)
result = self.cache.get( (name, qtype), []) result = self.cache.get( (name, qtype), [])
if not result and cname: if not result and cname:
if not cnames: result = self.dns(cname, qtype)
cnames = {}
elif len(cnames) >= MAX_CNAME:
raise PermError(
'Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname
if cname in cnames:
raise PermError,'CNAME loop'
result = self.dns(cname, qtype, cnames=cnames)
return result return result
def get_header(self,res,receiver=None): def get_header(self,res,receiver):
if not receiver: if res in ('pass','fail','softfail'):
receiver = self.r
if res in ('unknown','permerror'):
txt = ' '.join([res] + self.mech)
else:
txt = res
return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % ( return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
txt,receiver,self.get_header_comment(res),self.i, res,receiver,self.get_header_comment(res),self.i,
self.l + '@' + self.o, self.h) self.l + '@' + self.o, self.h)
if res == 'unknown':
return '%s (%s: %s)' % (' '.join([res] + self.mech),
receiver,self.get_header_comment(res))
return '%s (%s: %s)' % (res,receiver,self.get_header_comment(res))
def get_header_comment(self,res): def get_header_comment(self,res):
"""Return comment for Received-SPF header. """Return comment for Received-SPF header.
@@ -976,11 +778,11 @@ class query(object):
"%s is neither permitted nor denied by domain of %s" \ "%s is neither permitted nor denied by domain of %s" \
% (self.i,sender) % (self.i,sender)
#"%s does not designate permitted sender hosts" % sender #"%s does not designate permitted sender hosts" % sender
elif res in ('unknown','permerror'): return \ elif res == 'unknown': return \
"permanent error in processing domain of %s: %s" \ "error in processing during lookup of domain of %s: %s" \
% (sender, self.prob) % (sender, self.prob)
elif res in ('error','temperror'): return \ elif res == 'error': return \
"temporary error in processing during lookup of %s" % sender "error in processing during lookup of %s" % sender
elif res == 'fail': return \ elif res == 'fail': return \
"domain of %s does not designate %s as permitted sender" \ "domain of %s does not designate %s as permitted sender" \
% (sender,self.i) % (sender,self.i)
@@ -1024,32 +826,20 @@ def parse_mechanism(str, d):
>>> parse_mechanism('a/24', 'foo.com') >>> parse_mechanism('a/24', 'foo.com')
('a', 'foo.com', 24) ('a', 'foo.com', 24)
>>> parse_mechanism('A:foo:bar.com/16', 'foo.com') >>> parse_mechanism('a:bar.com/16', 'foo.com')
('a', 'foo:bar.com', 16) ('a', 'bar.com', 16)
>>> parse_mechanism('-exists:%{i}.%{s1}.100/86400.rate.%{d}','foo.com')
('-exists', '%{i}.%{s1}.100/86400.rate.%{d}', 32)
>>> parse_mechanism('mx::%%%_/.Claranet.de/27','foo.com')
('mx', ':%%%_/.Claranet.de', 27)
>>> parse_mechanism('mx:%{d}/27','foo.com')
('mx', '%{d}', 27)
>>> parse_mechanism('iP4:192.0.0.0/8','foo.com')
('ip4', '192.0.0.0', 8)
""" """
a = RE_CIDR.split(str) a = str.split('/')
if len(a) == 3: if len(a) == 2:
a, port = a[0], int(a[1]) a, port = a[0], int(a[1])
else: else:
a, port = str, 32 a, port = str, 32
b = a.split(':',1) b = a.split(':')
if len(b) == 2: if len(b) == 2:
return b[0].lower(), b[1], port return b[0], b[1], port
else: else:
return a.lower(), d, port return a, d, port
def reverse_dots(name): def reverse_dots(name):
"""Reverse dotted IP addresses or domain names. """Reverse dotted IP addresses or domain names.
@@ -1176,12 +966,12 @@ def bin2addr(addr):
def expand_one(expansion, str, joiner): def expand_one(expansion, str, joiner):
if not str: if not str:
return expansion return expansion
ln, reverse, delimiters = RE_ARGS.split(str)[1:4] len, reverse, delimiters = RE_ARGS.split(str)[1:4]
if not delimiters: if not delimiters:
delimiters = '.' delimiters = '.'
expansion = split(expansion, delimiters, joiner) expansion = split(expansion, delimiters, joiner)
if reverse: expansion.reverse() if reverse: expansion.reverse()
if ln: expansion = expansion[-int(ln)*2+1:] if len: expansion = expansion[-int(len)*2+1:]
return ''.join(expansion) return ''.join(expansion)
def split(str, delimiters, joiner=None): def split(str, delimiters, joiner=None):
@@ -1233,9 +1023,7 @@ if __name__ == '__main__':
receiver=socket.gethostname()) receiver=socket.gethostname())
elif len(sys.argv) == 5: elif len(sys.argv) == 5:
i, s, h = sys.argv[2:] i, s, h = sys.argv[2:]
q = query(i=i, s=s, h=h, receiver=socket.gethostname(), q = query(i=i, s=s, h=h, receiver=socket.gethostname())
strict=False)
print q.check(sys.argv[1]) print q.check(sys.argv[1])
if q.perm_error: print q.perm_error.ext
else: else:
print USAGE print USAGE
-8
View File
@@ -1,13 +1,5 @@
#!/usr/bin/python2.3 #!/usr/bin/python2.3
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2004 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
# $Log$ # $Log$
# Revision 1.1.1.1 2005/05/31 18:07:19 customdesigned
# Release 0.6.9
#
# Revision 2.3 2004/04/19 22:12:11 stuart # Revision 2.3 2004/04/19 22:12:11 stuart
# Release 0.6.9 # Release 0.6.9
# #
-66
View File
@@ -1,66 +0,0 @@
Subject: Critical mail server configuration error
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Someone at IP address %(connectip)s sent an email claiming
to be from %(sender)s.
If that wasn't you, then your domain, %(sender_domain)s,
was forged - i.e. used without your knowlege or authorization by
someone attempting to steal your mail identity. This is a very
serious problem, and you need to provide authentication for your
SMTP (email) servers to prevent criminals from forging your
domain. The simplest step is usually to publish an SPF record
with your Sender Policy.
For more information, see: http://openspf.org
I hate to annoy you with a DSN (Delivery Status
Notification) from a possibly forged email, but since you
have not published a sender policy, there is no other way
of bringing this to your attention.
If it *was* you that sent the email, then your email domain
or configuration is in error. If you don't know anything
about mail servers, then pass this on to your SMTP (mail)
server administrator. We have accepted the email anyway, in
case it is important, but we couldn't find anything about
the mail submitter at %(connectip)s to distinguish it from a
zombie (compromised/infected computer - usually a Windows
PC). There was no PTR record for its IP address (PTR names
that contain the IP address don't count). RFC2821 requires
that your hello name be a FQN (Fully Qualified domain Name,
i.e. at least one dot) that resolves to the IP address of
the mail sender. In addition, just like for PTR, we don't
accept a helo name that contains the IP, since this doesn't
help to identify you. The hello name you used,
%(heloname)s, was invalid.
Furthermore, there was no SPF record for the sending domain
%(sender_domain)s. We even tried to find its IP in any A or
MX records for your domain, but that failed also. We really
should reject mail from anonymous mail clients, but in case
it is important, we are accepting it anyway.
We are sending you this message to alert you to the fact that
Either - Someone is forging your domain.
Or - You have problems with your email configuration.
Or - Possibly both.
If you need further assistance, please do not hesitate to
contact me again.
Kind regards,
postmaster@%(receiver)s
-51
View File
@@ -1,51 +0,0 @@
From paulp@go2net.com Wed Jun 1 22:35:12 2005
Return-Path: <paulp@go2net.com>
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
Received: from 127.0.0.1 ([220.117.92.241])
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
SUBJECT: urgent
FROM: paulp@go2net.com
TO: stuart@bmsi.com
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="--------bound--"
X-DSpam-Score: 0.081200
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
Status: RO
X-Status:
X-Keywords: NonJunk
----------bound--
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Hi
Sorry, I forgot to send an important
document to you in that last email. I had an important phone call.
Please checkout attached doc file when you have a moment.
Best Regards
<!DSPAM:1043AE6B6492860536935410>
----------bound--
Content-Type: application/x-msdownload; name="zip.zip"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="zip.zip"
UEsDBAoAAAAAADVVwjLaV2nEGgAAABoAAAAzABUAemlwLmRvYyAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAuZXhlVVQJAAOmGp9CphqfQlV4BACGA2UAVGhpcyBw
cm9ncmFtIHdhcyBhIHZpcnVzLgpQSwECFwMKAAAAAAA1VcIy2ldpxBoAAAAaAAAAMwANAAAA
AAABAAAAtIEAAAAAemlwLmRvYyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAuZXhlVVQFAAOmGp9CVXgAAFBLBQYAAAAAAQABAG4AAACAAAAAAAA=
----------bound--
----------bound----
-49
View File
@@ -1,49 +0,0 @@
From paulp@go2net.com Wed Jun 1 22:35:12 2005
Return-Path: <paulp@go2net.com>
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
Received: from 127.0.0.1 ([220.117.92.241])
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
SUBJECT: urgent
FROM: paulp@go2net.com
TO: stuart@bmsi.com
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="--------bound--"
X-DSpam-Score: 0.081200
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
Status: RO
X-Status:
X-Keywords: NonJunk
----------bound--
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Hi
Sorry, I forgot to send an important
document to you in that last email. I had an important phone call.
Please checkout attached doc file when you have a moment.
Best Regards
<!DSPAM:1043AE6B6492860536935410>
----------bound--
Content-Type: application/octet-stream;
name="Readme.zip"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename="Readme.zip"
----------bound--
----------bound----
-51
View File
@@ -1,51 +0,0 @@
From paulp@go2net.com Wed Jun 1 22:35:12 2005
Return-Path: <paulp@go2net.com>
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
Received: from 127.0.0.1 ([220.117.92.241])
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
SUBJECT: urgent
FROM: paulp@go2net.com
TO: stuart@bmsi.com
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="--------bound--"
X-DSpam-Score: 0.081200
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
Status: RO
X-Status:
X-Keywords: NonJunk
----------bound--
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Hi
Sorry, I forgot to send an important
document to you in that last email. I had an important phone call.
Please checkout attached doc file when you have a moment.
Best Regards
<!DSPAM:1043AE6B6492860536935410>
----------bound--
Content-Type: application/x-msdownload; name="zip.zip"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="zip.zip"
USsDBAoBAAAAADVVwjLaV2nEGgAAABoAAAAzABUAemlwLmRvYyAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAuZXhlVVQJAAOmGp9CphqfQlV4BACGA2UAVGhpcyBw
cm9ncmFtIHdhcyBhIHZpcnVzLgpQSwECFwMKAAAAAAA1VcIy2ldpxBoAAAAaAAAAMwANAAAA
AAABAAAAtIEAAAAAemlwLmRvYyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAuZXhlVVQFAAOmGp9CVXgAAFBLBQYAAAAAAQABAG4AAACAAAAAAAA=
----------bound--
----------bound----
-47
View File
@@ -1,47 +0,0 @@
From ttaie1@thfalcon.com Thu Jun 16 10:23:13 2005
Received: from thfalcon.com (unknown [202.90.113.150])
by thfalcon.com (Postfix) with ESMTP id 32F0DD819C
for <stuart@bmsi.com>; Thu, 16 Jun 2005 15:42:08 +0700 (ICT)
From: ttaie1@thfalcon.com
To: stuart@bmsi.com
Subject: Returned mail: see transcript for details
Date: Thu, 16 Jun 2005 15:50:10 +0700
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_NextPart_000_0014_E4E04420.5619685C"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2600.0000
X-MIMEOLE: Produced By Microsoft MimeOLE V6.00.2600.0000
Message-Id: <20050616084208.32F0DD819C@thfalcon.com>
Received-SPF: pass (mail.bmsi.com: guessing: domain of thfalcon.com designates 203.147.3.44 as permitted sender) client-ip=203.147.3.44; envelope-from=ttaie1@thfalcon.com; helo=thfalcon.com;
This is a multi-part message in MIME format.
------=_NextPart_000_0014_E4E04420.5619685C
Content-Type: text/plain;
charset=us-ascii
Content-Transfer-Encoding: 7bit
Message could not be delivered
------=_NextPart_000_0014_E4E04420.5619685C
Content-Type: application/octet-stream;
name="stuart@bmsi.com.zip"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="stuart@bmsi.com.zip"
UEsDBAoAAAAAAM6r0DL7SfbCBAEAAAQBAAAFABUAdC56aXBVVAkAA7MnskK4J7JCVXgEAIYD
ZQBQSwMECgAAAAAANVXCMtpXacQaAAAAGgAAADMAFQB6aXAuZG9jICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgIC5leGVVVAkAA6Yan0KmGp9CVXgEAIYDZQBUaGlz
IHByb2dyYW0gd2FzIGEgdmlydXMuClBLAQIXAwoAAAAAADVVwjLaV2nEGgAAABoAAAAzAA0A
AAAAAAEAAAC0gQAAAAB6aXAuZG9jICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgIC5leGVVVAUAA6Yan0JVeAAAUEsFBgAAAAABAAEAbgAAAIAAAAAAAFBLAQIXAwoA
AAAAAM6r0DL7SfbCBAEAAAQBAAAFAA0AAAAAAAAAAAC0gQAAAAB0LnppcFVUBQADsyeyQlV4
AABQSwUGAAAAAAEAAQBAAAAAPAEAAAAA
------=_NextPart_000_0014_E4E04420.5619685C--
+3 -8
View File
@@ -1,5 +1,4 @@
import unittest import unittest
import doctest
import Milter import Milter
import bms import bms
import mime import mime
@@ -23,7 +22,7 @@ class TestMilter(bms.bmsMilter):
def getsymval(self,name): def getsymval(self,name):
if name == 'j': return 'test.milter.org' if name == 'j': return 'test.milter.org'
return '' return bms.bmsMilter.getsymval(self,name)
def replacebody(self,chunk): def replacebody(self,chunk):
if self._body: if self._body:
@@ -285,10 +284,7 @@ class BMSMilterTestCase(unittest.TestCase):
# self.failUnless(rc == Milter.REJECT) # self.failUnless(rc == Milter.REJECT)
# milter.close(); # milter.close();
def suite(): def suite(): return unittest.makeSuite(BMSMilterTestCase,'test')
s = unittest.makeSuite(BMSMilterTestCase,'test')
s.addTest(doctest.DocTestSuite(bms))
return s
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) > 1: if len(sys.argv) > 1:
@@ -300,5 +296,4 @@ if __name__ == '__main__':
fp = milter._body fp = milter._body
sys.stdout.write(fp.getvalue()) sys.stdout.write(fp.getvalue())
else: else:
#unittest.main() unittest.main()
unittest.TextTestRunner().run(suite())
+2 -29
View File
@@ -1,13 +1,4 @@
# $Log$ # $Log$
# Revision 1.3 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
# Revision 1.2 2005/06/02 15:00:17 customdesigned
# Configure banned extensions. Scan zipfile option with test case.
#
# Revision 1.1.1.2 2005/05/31 18:23:49 customdesigned
# Development changes since 0.7.2
#
# Revision 1.23 2005/02/11 18:34:14 stuart # Revision 1.23 2005/02/11 18:34:14 stuart
# Handle garbage after quote in boundary. # Handle garbage after quote in boundary.
# #
@@ -72,7 +63,7 @@ class MimeTestCase(unittest.TestCase):
def testDefang(self,vname='virus1',part=1, def testDefang(self,vname='virus1',part=1,
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'): fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
msg = mime.message_from_file(open('test/'+vname,"r")) msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg,scan_zip=True) mime.defang(msg)
self.failUnless(msg.ismodified(),"virus not removed") self.failUnless(msg.ismodified(),"virus not removed")
oname = vname + '.out' oname = vname + '.out'
msg.dump(open('test/'+oname,"w")) msg.dump(open('test/'+oname,"w"))
@@ -80,8 +71,7 @@ class MimeTestCase(unittest.TestCase):
txt2 = msg.get_payload() txt2 = msg.get_payload()
if type(txt2) == list: if type(txt2) == list:
txt2 = txt2[part].get_payload() txt2 = txt2[part].get_payload()
self.failUnless( self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
def testDefang3(self): def testDefang3(self):
self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif') self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif')
@@ -131,21 +121,6 @@ class MimeTestCase(unittest.TestCase):
name = parts[1].getname() name = parts[1].getname()
self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name) self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name)
def testZip(self,vname="zip1",fname='zip.zip'):
self.testDefang(vname,1,'zip.zip')
# test scan_zip flag
msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg,scan_zip=False)
self.failIf(msg.ismodified())
# test ignoring empty zip (often found in DSNs)
msg = mime.message_from_file(open('test/zip2','r'))
mime.defang(msg,scan_zip=True)
self.failIf(msg.ismodified())
# test corrupt zip (often an EXE named as a ZIP)
self.testDefang('zip3',1,'zip.zip')
# test zip within zip
self.testDefang('ziploop',1,'stuart@bmsi.com.zip')
def testHTML(self,fname=""): def testHTML(self,fname=""):
result = StringIO.StringIO() result = StringIO.StringIO()
filter = mime.HTMLScriptFilter(result) filter = mime.HTMLScriptFilter(result)
@@ -168,5 +143,3 @@ if __name__ == '__main__':
for fname in sys.argv[1:]: for fname in sys.argv[1:]:
fp = open(fname,'r') fp = open(fname,'r')
msg = mime.message_from_file(fp) msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True)
print msg.as_string()