diff --git a/Doxyfile b/Doxyfile index d14d1c785f07ce1b52f554cac0768164529e2a07..3c9bff8e304c66096649deac41c85ec13295fcbe 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 b0efd244f4080d4a68cf38f90b2f4b073d6b1c7c..6e3e9eb47e1d14e47f1e9d0892dbe7a40d439e6e 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 <code>_protocol</code> -# 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 <code>self._protocol & Milter.P_RCPT_REJ</code> -# 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: # <pre> @@ -67,22 +68,29 @@ def decode_mask(bits,names): # return Milter.CONTINUE # myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ) # </pre> +# or with python-2.6 and later: +# <pre> +# @Milter.enable_protocols(Milter.P_RCPT_REJ) +# class myMilter(Milter.Base): +# def envrcpt(self,to,*params): +# return Milter.CONTINUE +# </pre> # @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: + # <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT + # CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST</code>. + # The <code>Milter.CURR_ACTS</code> 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: <code> + # 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 + # </code> (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 <code>milter.set_flags</code>, or by overriding - # the negotiate callback. The bits include: - # <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT - # CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST</code>. - # The <code>Milter.CURR_ACTS</code> 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: <code> - # 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 - # </code> (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 + # <a href="https://www.milter.org/developers/api/xxfi_negotiate"> + # xffi_negotiate</a> 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 <a href="https://www.milter.org/developers/api/smfi_addheader"> + # smfi_addheader</a>. # The <code>Milter.ADDHDRS</code> 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 <a href="https://www.milter.org/developers/api/smfi_chgheader"> + # smfi_chgheader</a>. # The <code>Milter.CHGHDRS</code> 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 <a href="https://www.milter.org/developers/api/smfi_addrcpt"> + # smfi_addrcpt</a>. # 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 <a href="https://www.milter.org/developers/api/smfi_delrcpt"> + # smfi_delrcpt</a>. # The recipient should match one passed to the envrcpt callback. # The <code>Milter.DELRCPT</code> 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 <a href="https://www.milter.org/developers/api/smfi_replacebody"> + # smfi_replacebody</a>. # The entire message body must be replaced. # Call repeatedly with blocks of data until the entire body is transferred. # The <code>Milter.MODBODY</code> 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 <a href="https://www.milter.org/developers/api/smfi_chgfrom"> + # smfi_chgfrom</a>. # 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 <code>self.chgfrom('<bar@example.com>')</code>. @@ -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 <a href="https://www.milter.org/developers/api/smfi_quarantine"> + # smfi_quarantine</a>. # When quarantined, a message goes into the mailq as if to be delivered, # but delivery is deferred until the message is unquarantined. # The <code>Milter.QUARANTINE</code> 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 <a href="https://www.milter.org/developers/api/smfi_progress"> + # smfi_progress</a>. # 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 <code>milter.setconn</code> +## 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 2fe7776b6266ea6d012a37efc30beef6db247416..3e207653e3cac6d930d26981f33cbd47b5c58df3 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 +# <a href="https://www.milter.org/developers/api/xxfi_negotiate"> +# xxfi_negotiate</a> callback, called to negotiate supported +# actions, callbacks, and protocol steps. +# @param unknown the +# <a href="https://www.milter.org/developers/api/xxfi_unknown"> +# xxfi_unknown</a> callback, called when for SMTP commands +# not recognized by the MTA. (Extend SMTP in your milter!) +# @param data the +# <a href="https://www.milter.org/developers/api/xxfi_data"> +# xxfi_data</a> 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 bbc27559ba6bc51b1445fd8b95950bc73c13bb96..105d476bd656051887e4d24e98f9df6a6f461b30 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 736ee30997db1bbfa8c54e721c659bcc9a6932a5..ba42ab5d29e7e36621c97bcbe266145543571b75 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 7df94b32902547b9779dc4590be4c9e068f89487..6a614e1b2a0e68f4a3076cb62820bf65585ab56b 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)