From baeddd9fa5be7d310ef04c475e012e02ca528a24 Mon Sep 17 00:00:00 2001 From: Stuart Gathman Date: Sat, 9 Mar 2013 05:42:14 +0000 Subject: [PATCH] Make TestBase members private, fix getsymlist misspelling. --- Milter/__init__.py | 10 ++--- Milter/test.py | 97 ++++++++++++++++++++++++++++++++++------------ doc/milter.py | 14 ++++++- miltermodule.c | 23 ++++++----- pymilter.spec | 6 +++ testsample.py | 12 +++--- 6 files changed, 114 insertions(+), 48 deletions(-) diff --git a/Milter/__init__.py b/Milter/__init__.py index 28ae202..d2e0426 100755 --- a/Milter/__init__.py +++ b/Milter/__init__.py @@ -227,7 +227,7 @@ class Base(object): # 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. + # CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST. # The Milter.CURR_ACTS bitmask is all actions # known when the milter module was compiled. # Application code can also inspect this field to determine @@ -440,7 +440,7 @@ class Base(object): ## Tell the MTA which macro names will be used. # This information can reduce the size of messages received from sendmail, # and hence could reduce bandwidth between sendmail and your milter where - # that is a factor. The Milter.SETSMLIST action flag must be + # that is a factor. The Milter.SETSYMLIST action flag must be # set. # # May only be called from negotiate callback. @@ -448,9 +448,9 @@ class Base(object): # @param stage the protocol stage to set to macro list for, # one of the M_* constants defined in Milter # @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): + def setsymlist(self,stage,macros): + if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST") + if type(macros) != str: macros = ' '.join(macros) return self._ctx.setsmlist(stage,macros) diff --git a/Milter/test.py b/Milter/test.py index 5643f54..dc2a8c4 100644 --- a/Milter/test.py +++ b/Milter/test.py @@ -5,31 +5,52 @@ import rfc822 import StringIO import Milter -## -# +## Test mixin for unit testing milter applications. +# This mixin overrides many Milter.MilterBase methods +# with stub versions that simply record what was done. +# @since 0.9.8 class TestBase(object): _protocol = 0 - def __init__(self): - self.logfp = open("test/milter.log","a") - self._delrcpt = [] # record deleted rcpts for testing - self._addrcpt = [] # record added rcpts for testing + def __init__(self,logfile='test/milter.log'): + self.logfp = open(logfile,"a") + ## List of recipients deleted + self._delrcpt = [] + ## List of recipients added + self._addrcpt = [] + ## Macros defined self._macros = { } + ## The message body. + self._body = None + ## True if the milter replaced the message body. + self._bodyreplaced = False + ## True if the milter changed any headers. + self._headerschanged = False + ## Reply codes and messages set by milter + self._reply = None + ## The rfc822 message object for the current email being fed to the milter. + self._msg = None + self._symlist = [ None, None, None, None, None, None, None ] def log(self,*msg): for i in msg: print >>self.logfp, i, print >>self.logfp - def setsymval(self,name,val,step=None): + ## Set a macro value. + # These are retrieved by the milter with getsymval. + # @param name the macro name, as passed to getsymval + # @param val the macro value + def setsymval(self,name,val): self._macros[name] = val def getsymval(self,name): + # FIXME: track stage, and use _symlist return self._macros.get(name,'') def replacebody(self,chunk): if self._body: self._body.write(chunk) - self.bodyreplaced = True + self._bodyreplaced = True else: raise IOError,"replacebody not called from eom()" @@ -43,14 +64,14 @@ class TestBase(object): del self._msg[field] else: self._msg[field] = value - self.headerschanged = True + self._headerschanged = True def addheader(self,field,value,idx=-1): if not self._body: raise IOError,"addheader not called from eom()" self.log('addheader: %s=%s' % (field,value)) self._msg[field] = value - self.headerschanged = True + self._headerschanged = True def delrcpt(self,rcpt): if not self._body: @@ -62,19 +83,38 @@ class TestBase(object): raise IOError,"addrcpt not called from eom()" self._addrcpt.append(rcpt) + ## Save the reply codes and messages in self._reply. def setreply(self,rcode,xcode,*msg): - self.reply = (rcode,xcode) + msg + self._reply = (rcode,xcode) + msg - def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com"): + def setsymlist(self,stage,macros): + if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST") + if type(macros) != str: + macros = ' '.join(macros) + self._symlist[stage] = macros + + ## Feed a file like object to the milter. Calls envfrom, envrcpt for + # each recipient, header for each header field, body for each body + # block, and finally eom. A return code from the milter other than + # CONTINUE returns immediately with that return code. + # + # This is a convenience method, a test could invoke the callbacks + # in sequence on its own - and for some complex tests, this may + # be necessary. + # @param fp the file with rfc2822 message stream + # @param sender the MAIL FROM + # @param rcpt RCPT TO - additional recipients may follow + def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts): self._body = None - self.bodyreplaced = False - self.headerschanged = False - self.reply = None + self._bodyreplaced = False + self._headerschanged = False + self._reply = None msg = rfc822.Message(fp) rc = self.envfrom('<%s>'%sender) if rc != Milter.CONTINUE: return rc - rc = self.envrcpt('<%s>'%rcpt) - if rc != Milter.CONTINUE: return rc + for rcpt in (rcpt,) + rcpts: + rc = self.envrcpt('<%s>'%rcpt) + if rc != Milter.CONTINUE: return rc line = None for h in msg.headers: if h[:1].isspace(): @@ -103,7 +143,7 @@ class TestBase(object): self._msg = msg self._body = StringIO.StringIO() rc = self.eom() - if self.bodyreplaced: + if self._bodyreplaced: body = self._body.getvalue() else: msg.rewindbody() @@ -114,17 +154,24 @@ class TestBase(object): self._body.write(body) return rc - def feedMsg(self,fname,sender="spam@adv.com",rcpt="victim@lamb.com"): - fp = open('test/'+fname,'r') - rc = self.feedFile(fp,sender,rcpt) - fp.close() - return rc + ## Feed an email contained in a file to the milter. + # This is a convenience method that invokes @link #feedFile feedFile @endlink. + # @param sender MAIL FROM + # @param rcpts RCPT TO, multiple recipients may be supplied + def feedMsg(self,fname,sender="spam@adv.com",*rcpts): + with open('test/'+fname,'r') as fp: + return self.feedFile(fp,sender,*rcpts) + ## Call the connect and helo callbacks. + # The helo callback is not called if connect does not return CONTINUE. + # @param host the hostname passed to the connect callback + # @param helo the hostname passed to the helo callback + # @param ip the IP address passed to the connect callback def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'): self._body = None - self.bodyreplaced = False + self._bodyreplaced = False rc = super(TestBase,self).connect(host,1,(ip,1234)) - if rc != Milter.CONTINUE and rc != Milter.ACCEPT: + if rc != Milter.CONTINUE: self.close() return rc rc = self.hello(helo) diff --git a/doc/milter.py b/doc/milter.py index c35d991..efa9f1f 100644 --- a/doc/milter.py +++ b/doc/milter.py @@ -54,7 +54,12 @@ class milterContext(object): def chgfrom(self,sender,param=None): pass ## Tell the MTA which macro values we are interested in for a given stage. # Of interest only when you need to squeeze a few more bytes of bandwidth. - def setsmlist(self,stage,macrolist): pass + # It may only be called from the negotiate callback. + # The protocol stages are + # M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH. + # Calls smfi_setsymlist. + # @param stage protocol stage in which the macro list should be used + def setsymlist(self,stage,macrolist): pass class error(Exception): pass @@ -77,7 +82,12 @@ def set_abort_callback(cb): pass def set_close_callback(cb): pass ## Sets the return code for untrapped Python exceptions during a callback. -# Must be one of TEMPFAIL,REJECT,CONTINUE +# Must be one of TEMPFAIL,REJECT,CONTINUE. The default is TEMPFAIL. +# You should not depend on this handler. Your application should +# have its own top level exception handler for each callback. You can +# then choose your own reply message, log the stack track were you please, +# and so on. However, if you miss one, this last ditch handler will +# print a standard stack trace to sys.stderr, and return to sendmail. def set_exception_policy(code): pass ## Register python milter with libmilter. diff --git a/miltermodule.c b/miltermodule.c index 3fa2842..1c89dab 100644 --- a/miltermodule.c +++ b/miltermodule.c @@ -35,6 +35,9 @@ $ python setup.py help libraries=["milter","smutil","resolv"] * $Log$ + * Revision 1.33 2013/03/09 00:25:23 customdesigned + * Better untrapped exception message. const char for doc comments. + * * Revision 1.32 2013/01/13 01:46:16 customdesigned * Doc updates. * @@ -1490,23 +1493,23 @@ milter_progress(PyObject *self, PyObject *args) { } #endif -#ifdef SMFIF_SETSMLIST -static const char milter_setsmlist__doc__[] = -"setsmlist(stage,macrolist) -> None\n\ +#ifdef SMFIF_SETSYMLIST +static const char milter_setsymlist__doc__[] = +"setsymlist(stage,macrolist) -> None\n\ Tell the MTA which macro values we are interested in for a given stage"; static PyObject * -milter_setsmlist(PyObject *self, PyObject *args) { +milter_setsymlist(PyObject *self, PyObject *args) { SMFICTX *ctx; PyThreadState *t; int stage = 0; char *smlist = 0; - if (!PyArg_ParseTuple(args, "is:setsmlist",&stage, &smlist)) return NULL; + if (!PyArg_ParseTuple(args, "is:setsymlist",&stage, &smlist)) return NULL; ctx = _find_context(self); if (ctx == NULL) return NULL; t = PyEval_SaveThread(); - return _thread_return(t,smfi_setsmlist(ctx,stage,smlist), + return _thread_return(t,smfi_setsymlist(ctx,stage,smlist), "cannot set macro list"); } #endif @@ -1530,8 +1533,8 @@ static PyMethodDef context_methods[] = { #ifdef SMFIF_CHGFROM { "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__}, #endif -#ifdef SMFIF_SETSMLIST - { "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__doc__}, +#ifdef SMFIF_SETSYMLIST + { "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__doc__}, #endif { NULL, NULL } }; @@ -1654,8 +1657,8 @@ initmilter(void) { #ifdef SMFIF_CHGFROM setitem(d,"CHGFROM",SMFIF_CHGFROM); #endif -#ifdef SMFIF_SETSMLIST - setitem(d,"SETSMLIST",SMFIF_SETSMLIST); +#ifdef SMFIF_SETSYMLIST + setitem(d,"SETSYMLIST",SMFIF_SETSYMLIST); setitem(d,"M_CONNECT",SMFIM_CONNECT);/* connect */ setitem(d,"M_HELO",SMFIM_HELO); /* HELO/EHLO */ setitem(d,"M_ENVFROM",SMFIM_ENVFROM);/* MAIL From */ diff --git a/pymilter.spec b/pymilter.spec index 35ed394..67c1d77 100644 --- a/pymilter.spec +++ b/pymilter.spec @@ -75,6 +75,12 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh rm -rf $RPM_BUILD_ROOT %changelog +* Sat Mar 9 2013 Stuart Gathman 0.9.8-1 +- Add Milter.test module for unit testing milters. +- Fix typo that prevented setsymlist from being active. +- Change untrapped exception message to: +- "pymilter: untrapped exception in milter app" + * Sat Feb 25 2012 Stuart Gathman 0.9.7-1 - Raise RuntimeError when result != CONTINUE for @noreply and @nocallback - Remove redundant table in miltermodule diff --git a/testsample.py b/testsample.py index 2e08454..be10b06 100644 --- a/testsample.py +++ b/testsample.py @@ -19,7 +19,7 @@ class BMSMilterTestCase(unittest.TestCase): self.failUnless(rc == Milter.CONTINUE) rc = milter.feedMsg(fname) self.failUnless(rc == Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") + self.failUnless(milter._bodyreplaced,"Message body not replaced") fp = milter._body open('test/'+fname+".tstout","w").write(fp.getvalue()) #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) @@ -34,7 +34,7 @@ class BMSMilterTestCase(unittest.TestCase): milter.connect('somehost') rc = milter.feedMsg(fname) self.failUnless(rc == Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") fp = milter._body open('test/'+fname+".tstout","w").write(fp.getvalue()) milter.close() @@ -44,17 +44,17 @@ class BMSMilterTestCase(unittest.TestCase): milter.connect('somehost') rc = milter.feedMsg('samp1') self.failUnless(rc == Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") rc = milter.feedMsg("virus3") self.failUnless(rc == Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") + self.failUnless(milter._bodyreplaced,"Message body not replaced") fp = milter._body open("test/virus3.tstout","w").write(fp.getvalue()) #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read()) rc = milter.feedMsg("virus6") self.failUnless(rc == Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") - self.failUnless(milter.headerschanged,"Message headers not adjusted") + self.failUnless(milter._bodyreplaced,"Message body not replaced") + self.failUnless(milter._headerschanged,"Message headers not adjusted") fp = milter._body open("test/virus6.tstout","w").write(fp.getvalue()) milter.close()