From f6a3b57fb932155a75d6c5dbeb3b5b953298f04f Mon Sep 17 00:00:00 2001 From: Stuart Gathman <stuart@gathman.org> Date: Tue, 16 Jun 2009 21:45:45 +0000 Subject: [PATCH] enable_protocols class decorator, doc updates --- Milter/__init__.py | 72 +++++++++++++++++++++++++++++++++++----------- doc/milter.py | 15 ++++++++++ 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/Milter/__init__.py b/Milter/__init__.py index fe796c9..217514e 100755 --- a/Milter/__init__.py +++ b/Milter/__init__.py @@ -40,6 +40,41 @@ OPTIONAL_CALLBACKS = { 'header':(P_NR_HDR,P_NOHDRS) } +def decode_mask(bits,names): + t = [ (s,getattr(milter,s)) for s in names] + nms = [s for s,m in t if bits & m] + for s,m in t: bits &= ~m + if bits: nms += hex(bits) + return nms + +## 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 +# 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, +# for instance, if <code>self._protocol & Milter.P_RCPT_REJ</code> +# is true, then that feature was successfully negotiated with the MTA. +# +# Sample use: +# <pre> +# class myMilter(Milter.Base): +# def envrcpt(self,to,*params): +# return Milter.CONTINUE +# myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ) +# </pre> +# @since 0.9.3 +# @param klass the milter application class to modify +# @param mask a bitmask of protocol steps to enable +# @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, # increasing efficiency. All the callbacks (except negotiate) @@ -48,6 +83,7 @@ OPTIONAL_CALLBACKS = { # 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. +# @since 0.9.2 def nocallback(func): try: func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1] @@ -62,6 +98,7 @@ def nocallback(func): # CONTINUE in case the MTA does not support protocol negotiation. # The decorator arranges to change the return code to NOREPLY # when supported by the MTA. +# @since 0.9.2 def noreply(func): try: nr_mask = OPTIONAL_CALLBACKS[func.__name__][0] @@ -81,6 +118,7 @@ def noreply(func): # connection in the negotiate callback. If the application then calls # the feature anyway via an instance method, this exception is # thrown. +# @since 0.9.2 class DisabledAction(RuntimeError): pass @@ -89,7 +127,7 @@ class DisabledAction(RuntimeError): # unless they are using the low lever milter module directly. # All optional callbacks are disabled, and automatically # reenabled when overridden. -# +# @since 0.9.2 class Base(object): "The core class interface to the milter module." @@ -109,6 +147,7 @@ class Base(object): # 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 @@ -121,6 +160,7 @@ class Base(object): # 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 @@ -159,6 +199,7 @@ class Base(object): ## Called when the SMTP client says DATA. # Returning REJECT rejects the message without wasting bandwidth # on the unwanted message. + # @since 0.9.2 @nocallback def data(self): return CONTINUE ## Called for each header field in the message body. @@ -173,6 +214,7 @@ class Base(object): def body(self,blk): return CONTINUE ## Called when the SMTP client issues an unknown command. # @param cmd the unknown command + # @since 0.9.2 @nocallback def unknown(self,cmd): return CONTINUE ## Called at the end of the message body. @@ -194,12 +236,13 @@ class Base(object): # Libmilter passes the protocol bits that the current MTA knows # how to skip. We clear the ones we don't want to skip. # The negation is somewhat mind bending, but it is simple. + # @since 0.9.2 @classmethod def protocol_mask(klass): try: return klass._protocol_mask except AttributeError: - p = 0 + p = P_RCPT_REJ | P_HDR_LEADSPC # turn these new features off by default for func,(nr,nc) in OPTIONAL_CALLBACKS.items(): func = getattr(klass,func) ca = getattr(func,'milter_protocol',0) @@ -212,11 +255,11 @@ class Base(object): # Default negotiation sets P_NO* and P_NR* for callbacks # marked @@nocallback and @@noreply respectively, leaves all # actions enabled, and enables Milter.SKIP. + # @since 0.9.2 def negotiate(self,opts): try: self._actions,p,f1,f2 = opts - opts[1] = self._protocol = \ - p & ~self.protocol_mask() & ~P_RCPT_REJ & ~P_HDR_LEADSPC + opts[1] = self._protocol = p & ~self.protocol_mask() opts[2] = 0 opts[3] = 0 #self.log("Negotiated:",opts) @@ -241,9 +284,12 @@ class Base(object): return self._ctx.setreply(rcode,xcode,msg,*ml) ## Tell the MTA which macro names will be used. - # The <code>Milter.ADDHDRS</code> action flag must be set. + # The <code>Milter.SETSMLIST</code> action flag must be set. # # May only be called from negotiate callback. + # @since 0.9.2 + # @param stage the protocol stage to set to macro list for + # @param macros a string with a space delimited list of macros def setsmlist(self,stage,macros): if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST") if type(macros) in (list,tuple): @@ -319,6 +365,7 @@ class Base(object): # The <code>Milter.CHGFROM</code> action flag must be set. # # May be called from eom callback only. + # @since 0.9.1 # @param sender the new sender address # @param params an optional list of ESMTP parameters def chgfrom(self,sender,params=None): @@ -452,7 +499,9 @@ def dictfromlist(args): ## Convert ESMTP parm list to keyword dictionary. # Params with no value are set to None in the dictionary. +# @since 0.9.3 # @param str list of param strings of the form "NAME" or "NAME=VALUE" +# @return a dictionary of ESMTP param names and values def param2dict(str): "Convert ESMTP parm list to keyword dictionary." pairs = [x.split('=',1) for x in str] @@ -475,19 +524,8 @@ def envcallback(c,args): return c(*pargs,**kw) ## Run the milter. -# The MTA can communicate with the milter by means of a -# unix, inet, or inet6 socket. By default, a unix domain socket -# is used. It must not exist, -# and sendmail will throw warnings if, eg, the file is under a -# group or world writable directory. -# <pre> -# setconn('unix:/var/run/pythonfilter') -# setconn('inet:8800') # listen on ANY interface -# setconn('inet:7871@@publichost') # listen on a specific interface -# setconn('inet6:8020') -# </pre> # @param name the name of the milter known by the MTA -# @param socketname the descriptor of the unix socket +# @param socketname the socket to be passed to <code>milter.setconn</code> # @param timeout the time in secs the MTA should wait for a response before # considering this milter dead def runmilter(name,socketname,timeout = 0): diff --git a/doc/milter.py b/doc/milter.py index bcd8ee6..547dd8f 100644 --- a/doc/milter.py +++ b/doc/milter.py @@ -40,5 +40,20 @@ def main(): pass def setdbg(lev): pass def settimeout(secs): pass def setbacklog(n): pass + +## Set the socket used to communicate with the MTA. +# The MTA can communicate with the milter by means of a +# unix, inet, or inet6 socket. By default, a unix domain socket +# is used. It must not exist, +# and sendmail will throw warnings if, eg, the file is under a +# group or world writable directory. +# <pre> +# setconn('unix:/var/run/pythonfilter') +# setconn('inet:8800') # listen on ANY interface +# setconn('inet:7871@@publichost') # listen on a specific interface +# setconn('inet6:8020') +# </pre> def setconn(s): pass + +## Stop the milter gracefully. def stop(): pass -- GitLab