diff --git a/Doxyfile b/Doxyfile
index d14d1c7..3c9bff8 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -814,7 +814,7 @@ DOCSET_FEEDNAME = "Doxygen generated docs"
# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen
# will append .docset to the name.
-DOCSET_BUNDLE_ID = org.doxygen.Project
+DOCSET_BUNDLE_ID = com.bmsi.pymilter
# If the GENERATE_HTMLHELP tag is set to YES, additional index files
# will be generated that can be used as input for tools like the
diff --git a/Milter/__init__.py b/Milter/__init__.py
index b0efd24..6e3e9eb 100755
--- a/Milter/__init__.py
+++ b/Milter/__init__.py
@@ -20,7 +20,7 @@ _seq_lock = thread.allocate_lock()
_seq = 0
def uniqueID():
- """Return a sequence number unique to this process.
+ """Return a unique sequence number (incremented on each call).
"""
global _seq
_seq_lock.acquire()
@@ -49,16 +49,17 @@ def decode_mask(bits,names):
## Class decorator to enable optional protocol steps.
# P_SKIP is enabled by default when supported, but
-# milter applications may wish to enable P_HDR_LEADSPC
+# applications may wish to enable P_HDR_LEADSPC
# to send and receive the leading space of header continuation
# lines unchanged, and/or P_RCPT_REJ to have recipients
# detected as invalid by the MTA passed to the envcrpt callback.
#
# Applications may want to check whether the protocol is actually
-# supported by the MTA in use. The _protocol
-# member is a bitmask of protocol options negotiated. So,
+# supported by the MTA in use. Base._protocol
+# is a bitmask of protocol options negotiated. So,
# for instance, if self._protocol & Milter.P_RCPT_REJ
-# is true, then that feature was successfully negotiated with the MTA.
+# is true, then that feature was successfully negotiated with the MTA
+# and the application will see recipients the MTA has flagged as invalid.
#
# Sample use:
#
@@ -67,22 +68,29 @@ def decode_mask(bits,names): # return Milter.CONTINUE # myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ) #+# or with python-2.6 and later: +#
+# @Milter.enable_protocols(Milter.P_RCPT_REJ) +# class myMilter(Milter.Base): +# def envrcpt(self,to,*params): +# return Milter.CONTINUE +## @since 0.9.3 -# @param klass the milter application class to modify +# @param klass the %milter application class to modify # @param mask a bitmask of protocol steps to enable -# @return the modified milter class +# @return the modified %milter class def enable_protocols(klass,mask): klass._protocol_mask = klass.protocol_mask() & ~mask return klass ## Function decorator to disable callback methods. -# If the MTA supports it, tells the MTA not to call this callback, +# If the MTA supports it, tells the MTA not to invoke this callback, # increasing efficiency. All the callbacks (except negotiate) # are disabled in Milter.Base, and overriding them reenables the # callback. An application may need to use @@nocallback when it extends -# another milter and wants to disable a callback again. +# another %milter and wants to disable a callback again. # The disabled method should still return Milter.CONTINUE, in case the MTA does -# not support protocol negotiation. +# not support protocol negotiation, and for when called from a test harness. # @since 0.9.2 def nocallback(func): try: @@ -122,45 +130,81 @@ def noreply(func): class DisabledAction(RuntimeError): pass -## A do "nothing" Milter base class. +## A do "nothing" Milter base class representing an SMTP connection. +# # Python milters should derive from this class -# unless they are using the low lever milter module directly. -# All optional callbacks are disabled, and automatically -# reenabled when overridden. +# unless they are using the low level milter module directly. +# +# Most of the methods are either "actions" or "callbacks". Callbacks +# are invoked by the MTA at certain points in the SMTP protocol. For +# instance when the HELO command is seen, the MTA calls the helo +# callback before returning a response code. All callbacks must +# return one of these constants: CONTINUE, TEMPFAIL, REJECT, ACCEPT, +# DISCARD, SKIP. The NOREPLY response is supplied automatically by +# the @@noreply decorator if negotiation with the MTA is successful. +# @@noreply and @@nocallback methods should return CONTINUE for two reasons: +# the MTA may not support negotiation, and the class may be running in a test +# harness. +# +# Optional callbacks are disabled with the @@nocallback decorator, and +# automatically reenabled when overridden. Disabled callbacks should +# still return CONTINUE for testing and MTAs that do not support +# negotiation. + +# Each SMTP connection to the MTA calls the factory method you provide to +# create an instance derived from this class. This is typically the +# constructor for a class derived from Base. The _setctx() method attaches +# the instance to the low level milter.milterContext object. When the SMTP +# connection terminates, the close callback is called, the low level connection +# object is destroyed, and this normally causes instances of this class to be +# garbage collected as well. The close() method should release any global +# resources held by instances. # @since 0.9.2 class Base(object): - "The core class interface to the milter module." + "The core class interface to the %milter module." ## Attach this Milter to the low level milter.milterContext object. def _setctx(self,ctx): + ## The low level @ref milter.milterContext object. self._ctx = ctx + ## A bitmask of actions this connection has negotiated to use. + # By default, all actions are enabled. High throughput milters + # may want to disable unused actions to increase efficiency. + # Some optional actions may be disabled by calling milter.set_flags(), or + # by overriding the negotiate callback. The bits include: + #
ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
+ # CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST.
+ # The Milter.CURR_ACTS bitmask is all actions
+ # known when the milter module was compiled.
+ # Application code can also inspect this field to determine
+ # which actions are available. This is especially useful in
+ # generic library code designed to work in multiple milters.
+ # @since 0.9.2
+ #
self._actions = CURR_ACTS # all actions enabled by default
+ ## A bitmask of protocol options this connection has negotiated.
+ # An application may inspect this
+ # variable to determine which protocol steps are supported. Options
+ # of interest to applications: the SKIP result code is allowed
+ # only if the P_SKIP bit is set, rejected recipients are passed to the
+ # %milter application only if the P_RCPT_REJ bit is set, and
+ # header values are sent and received with leading spaces (in the
+ # continuation lines) intact if the P_HDR_LEADSPC bit is set (so
+ # that the application can customize indenting).
+ #
+ # The P_N* bits should be negotiated via the @@noreply and @@nocallback
+ # method decorators, and P_RCPT_REJ, P_HDR_LEADSPC should
+ # be enabled using the enable_protocols class decorator.
+ #
+ # The bits include:
+ # P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN
+ # P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT
+ # P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP
+ # (all under the Milter namespace).
+ # @since 0.9.2
self._protocol = 0 # no protocol options by default
if ctx:
ctx.setpriv(self)
- ## @var _actions
- # A bitmask of actions this milter has negotiated to use.
- # By default, all actions are enabled. This may be changed
- # by calling milter.set_flags, or by overriding
- # the negotiate callback. The bits include:
- # ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
- # CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST.
- # The Milter.CURR_ACTS bitmask is all actions
- # known when the milter module was compiled.
- # @since 0.9.2
- #
-
- ## @var _protocol
- # A bitmask of protocol options this milter has negotiated.
- # The bits generally indicate that a particular step should be
- # skipped, since previous versions of the milter protocol had
- # no provision for skipping steps.
- # The bits include:
- # P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN
- # P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT
- # P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP
- # (all under the Milter namespace).
- # @since 0.9.2
## Defined by subclasses to write log messages.
def log(self,*msg): pass
@@ -251,10 +295,17 @@ class Base(object):
klass._protocol_mask = p
return p
- ## Negotiate milter protocol options.
+ ## Negotiate milter protocol options. Called by the
+ #
+ # xffi_negotiate callback.
+ # Options are passed as
+ # a list of 4 32-bit ints which can be modified and are passed
+ # back to libmilter on return.
# Default negotiation sets P_NO* and P_NR* for callbacks
# marked @@nocallback and @@noreply respectively, leaves all
- # actions enabled, and enables Milter.SKIP.
+ # actions enabled, and enables Milter.SKIP. The @@enable_protocols
+ # class decorator can customize which protocol steps are implemented.
+ # @param opts a modifiable list of 4 ints with negotiated options
# @since 0.9.2
def negotiate(self,opts):
try:
@@ -299,28 +350,36 @@ class Base(object):
# Milter methods which can only be called from eom callback.
## Add a mail header field.
+ # Calls
+ # smfi_addheader.
# The Milter.ADDHDRS action flag must be set.
#
# May be called from eom callback only.
# @param field the header field name
# @param value the header field value
# @param idx header field index from the top of the message to insert at
+ # @throws DisabledAction if ADDHDRS is not enabled
def addheader(self,field,value,idx=-1):
if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
return self._ctx.addheader(field,value,idx)
## Change the value of a mail header field.
+ # Calls
+ # smfi_chgheader.
# The Milter.CHGHDRS action flag must be set.
#
# May be called from eom callback only.
# @param field the name of the field to change
# @param idx index of the field to change when there are multiple instances
# @param value the new value of the field
+ # @throws DisabledAction if CHGHDRS is not enabled
def chgheader(self,field,idx,value):
if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
return self._ctx.chgheader(field,idx,value)
- ## Add a recipient to the message.
+ ## Add a recipient to the message.
+ # Calls
+ # smfi_addrcpt.
# If no corresponding mail header is added, this is like a Bcc.
# The syntax of the recipient is the same as used in the SMTP
# RCPT TO command (and as delivered to the envrcpt callback), for example
@@ -332,33 +391,42 @@ class Base(object):
# May be called from eom callback only.
# @param rcpt the message recipient
# @param params an optional list of ESMTP parameters
+ # @throws DisabledAction if ADDRCPT or ADDRCPT_PAR is not enabled
def addrcpt(self,rcpt,params=None):
if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT")
if params and not self._actions & ADDRCPT_PAR:
raise DisabledAction("ADDRCPT_PAR")
return self._ctx.addrcpt(rcpt,params)
## Delete a recipient from the message.
+ # Calls
+ # smfi_delrcpt.
# The recipient should match one passed to the envrcpt callback.
# The Milter.DELRCPT action flag must be set.
#
# May be called from eom callback only.
# @param rcpt the message recipient to delete
+ # @throws DisabledAction if DELRCPT is not enabled
def delrcpt(self,rcpt):
if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
return self._ctx.delrcpt(rcpt)
## Replace the message body.
+ # Calls
+ # smfi_replacebody.
# The entire message body must be replaced.
# Call repeatedly with blocks of data until the entire body is transferred.
# The Milter.MODBODY action flag must be set.
#
# May be called from eom callback only.
# @param body a chunk of body data
+ # @throws DisabledAction if MODBODY is not enabled
def replacebody(self,body):
if not self._actions & MODBODY: raise DisabledAction("MODBODY")
return self._ctx.replacebody(body)
## Change the SMTP envelope sender address.
+ # Calls
+ # smfi_chgfrom.
# The syntax of the sender is that same as used in the SMTP
# MAIL FROM command (and as delivered to the envfrom callback),
# for example self.chgfrom('') .
@@ -368,22 +436,28 @@ class Base(object):
# @since 0.9.1
# @param sender the new sender address
# @param params an optional list of ESMTP parameters
+ # @throws DisabledAction if CHGFROM is not enabled
def chgfrom(self,sender,params=None):
if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
return self._ctx.chgfrom(sender,params)
## Quarantine the message.
+ # Calls
+ # smfi_quarantine.
# When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined.
# The Milter.QUARANTINE action flag must be set.
#
# May be called from eom callback only.
# @param reason a string describing the reason for quarantine
+ # @throws DisabledAction if QUARANTINE is not enabled
def quarantine(self,reason):
if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
return self._ctx.quarantine(reason)
## Tell the MTA to wait a bit longer.
+ # Calls
+ # smfi_progress.
# Resets timeouts in the MTA that detect a "hung" milter.
def progress(self):
return self._ctx.progress()
@@ -464,13 +538,22 @@ class Milter(Base):
# change in configuration.
factory = Milter
+## @fn set_flags(flags)
+# @brief Enable optional %milter actions.
+# Certain %milter actions need to be enabled before calling milter.runmilter()
+# or they throw an exception.
+# @param flags Bit or mask of optional actions to enable
+# def set_flags(flags): pass
+
## @private
+# @brief Connect context to connection instance and return enabled callbacks.
def negotiate_callback(ctx,opts):
m = factory()
m._setctx(ctx)
return m.negotiate(opts)
## @private
+# @brief Connect context if needed and invoke connect method.
def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
m = ctx.getpriv()
if not m:
@@ -481,6 +564,7 @@ def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
return m.connect(hostname,family,hostaddr)
## @private
+# @brief Disconnect milterContext and call close method.
def close_callback(ctx):
m = ctx.getpriv()
if not m: return CONTINUE
@@ -527,11 +611,11 @@ def envcallback(c,args):
pargs.append(s)
return c(*pargs,**kw)
-## Run the milter.
-# @param name the name of the milter known by the MTA
-# @param socketname the socket to be passed to milter.setconn
+## Run the %milter.
+# @param name the name of the %milter known to the MTA
+# @param socketname the socket to be passed to milter.setconn()
# @param timeout the time in secs the MTA should wait for a response before
-# considering this milter dead
+# considering this %milter dead
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,
diff --git a/doc/milter.py b/doc/milter.py
index 2fe7776..3e20765 100644
--- a/doc/milter.py
+++ b/doc/milter.py
@@ -36,6 +36,10 @@ class milterContext(object):
class error(Exception): pass
+## Enable optional milter actions.
+# Certain milter actions need to be enabled before calling milter.runmilter()
+# or they throw an exception.
+# @param flags Bit or mask of optional actions to enable
def set_flags(flags): pass
def set_connect_callback(cb): pass
def set_helo_callback(cb): pass
@@ -47,6 +51,28 @@ def set_body_callback(cb): pass
def set_abort_callback(cb): pass
def set_close_callback(cb): pass
def set_exception_policy(code): pass
+## Register python milter with libmilter.
+# The name we pass is used to identify the milter in the MTA configuration.
+# Callback functions must be set using the set_*_callback() functions before
+# registering the milter.
+# Three additional callbacks are specified as keyword parameters. These
+# were added by recent versions of libmilter. The keyword parameters is
+# a nicer way to do it, I think, since it makes clear that you have to do
+# it before registering. I may move all the callbacks
+# in the future (perhaps keeping the set functions for compatibility).
+# @param name the milter name by which the MTA finds us
+# @param negotiate the
+#
+# xxfi_negotiate callback, called to negotiate supported
+# actions, callbacks, and protocol steps.
+# @param unknown the
+#
+# xxfi_unknown callback, called when for SMTP commands
+# not recognized by the MTA. (Extend SMTP in your milter!)
+# @param data the
+#
+# xxfi_data callback, called when the DATA
+# SMTP command is received.
def register(name,negotiate=None,unknown=None,data=None): pass
def opensocket(rmsock): pass
def main(): pass
diff --git a/pymilter.spec b/pymilter.spec
index bbc2755..105d476 100644
--- a/pymilter.spec
+++ b/pymilter.spec
@@ -1,8 +1,8 @@
%define __python python2.6
+%define pythonbase python26
%define libdir %{_libdir}/pymilter
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
-%define pythonbase python26
Summary: Python interface to sendmail milter API
Name: %{pythonbase}-pymilter
diff --git a/start.sh b/start.sh
index 736ee30..ba42ab5 100755
--- a/start.sh
+++ b/start.sh
@@ -1,13 +1,16 @@
#!/bin/sh
appname="$1"
script="${2:-${appname}}"
-datadir="/var/log/milter"
+datadir="/var/lib/milter"
+logdir="/var/log/milter"
piddir="/var/run/milter"
libdir="/usr/lib/pymilter"
python="python2.4"
-exec >>${datadir}/${appname}.log 2>&1
+exec >>${logdir}/${appname}.log 2>&1
if test -s ${datadir}/${script}.py; then
- cd ${datadir} # use version in log dir if it exists for debugging
+ cd ${datadir} # use version in data dir if it exists for debugging
+elif test -s ${logdir}/${script}.py; then
+ cd ${logdir} # use version in log dir if it exists for debugging
else
cd ${libdir}
fi
diff --git a/testmime.py b/testmime.py
index 7df94b3..6a614e1 100644
--- a/testmime.py
+++ b/testmime.py
@@ -1,4 +1,7 @@
# $Log$
+# Revision 1.4 2005/07/20 14:49:44 customdesigned
+# Handle corrupt and empty ZIP files.
+#
# Revision 1.3 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
@@ -26,6 +29,7 @@ import socket
import StringIO
import email
import sys
+import Milter
from email import Errors
samp1_txt1 = """Dear Agent 1
@@ -146,6 +150,31 @@ class MimeTestCase(unittest.TestCase):
# test zip within zip
self.testDefang('ziploop',1,'stuart@bmsi.com.zip')
+ def _chk_name(self,name):
+ self.filename = name
+
+ def _chk_attach(self,msg):
+ "Filter attachments by content."
+ # check for bad extensions
+ mime.check_name(msg,ckname=self._chk_name,scan_zip=True)
+ # remove scripts from HTML
+ mime.check_html(msg)
+ # don't let a tricky virus slip one past us
+ msg = msg.get_submsg()
+ if isinstance(msg,email.Message.Message):
+ return mime.check_attachments(msg,self._chk_attach)
+ return Milter.CONTINUE
+
+ def testCheckAttach(self,fname="test1"):
+ # test1 contains a very long filename
+ msg = mime.message_from_file(open('test/'+fname,'r'))
+ mime.defang(msg,scan_zip=True)
+ self.failIf(msg.ismodified())
+ msg = mime.message_from_file(open('test/tmpytgcE5.fail','r'))
+ rc = mime.check_attachments(msg,self._chk_attach)
+ self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
+ self.assertEquals(rc,Milter.CONTINUE)
+
def testHTML(self,fname=""):
result = StringIO.StringIO()
filter = mime.HTMLScriptFilter(result)