Make TestBase members private, fix getsymlist misspelling.

This commit is contained in:
Stuart Gathman
2013-03-09 05:42:14 +00:00
parent 4854f95b59
commit baeddd9fa5
6 changed files with 114 additions and 48 deletions
+5 -5
View File
@@ -227,7 +227,7 @@ class Base(object):
# Some optional actions may be disabled by calling milter.set_flags(), or # Some optional actions may be disabled by calling milter.set_flags(), or
# by overriding the negotiate callback. The bits include: # by overriding the negotiate callback. The bits include:
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT # <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
# CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST</code>. # CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</code>.
# The <code>Milter.CURR_ACTS</code> bitmask is all actions # The <code>Milter.CURR_ACTS</code> bitmask is all actions
# known when the milter module was compiled. # known when the milter module was compiled.
# Application code can also inspect this field to determine # 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. ## Tell the MTA which macro names will be used.
# This information can reduce the size of messages received from sendmail, # This information can reduce the size of messages received from sendmail,
# and hence could reduce bandwidth between sendmail and your milter where # and hence could reduce bandwidth between sendmail and your milter where
# that is a factor. The <code>Milter.SETSMLIST</code> action flag must be # that is a factor. The <code>Milter.SETSYMLIST</code> action flag must be
# set. # set.
# #
# May only be called from negotiate callback. # 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, # @param stage the protocol stage to set to macro list for,
# one of the M_* constants defined in Milter # one of the M_* constants defined in Milter
# @param macros a string with a space delimited list of macros # @param macros a string with a space delimited list of macros
def setsmlist(self,stage,macros): def setsymlist(self,stage,macros):
if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST") if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
if type(macros) in (list,tuple): if type(macros) != str:
macros = ' '.join(macros) macros = ' '.join(macros)
return self._ctx.setsmlist(stage,macros) return self._ctx.setsmlist(stage,macros)
+72 -25
View File
@@ -5,31 +5,52 @@ import rfc822
import StringIO import StringIO
import Milter 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): class TestBase(object):
_protocol = 0 _protocol = 0
def __init__(self): def __init__(self,logfile='test/milter.log'):
self.logfp = open("test/milter.log","a") self.logfp = open(logfile,"a")
self._delrcpt = [] # record deleted rcpts for testing ## List of recipients deleted
self._addrcpt = [] # record added rcpts for testing self._delrcpt = []
## List of recipients added
self._addrcpt = []
## Macros defined
self._macros = { } 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): def log(self,*msg):
for i in msg: print >>self.logfp, i, for i in msg: print >>self.logfp, i,
print >>self.logfp 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 self._macros[name] = val
def getsymval(self,name): def getsymval(self,name):
# FIXME: track stage, and use _symlist
return self._macros.get(name,'') return self._macros.get(name,'')
def replacebody(self,chunk): def replacebody(self,chunk):
if self._body: if self._body:
self._body.write(chunk) self._body.write(chunk)
self.bodyreplaced = True self._bodyreplaced = True
else: else:
raise IOError,"replacebody not called from eom()" raise IOError,"replacebody not called from eom()"
@@ -43,14 +64,14 @@ class TestBase(object):
del self._msg[field] del self._msg[field]
else: else:
self._msg[field] = value self._msg[field] = value
self.headerschanged = True self._headerschanged = True
def addheader(self,field,value,idx=-1): def addheader(self,field,value,idx=-1):
if not self._body: if not self._body:
raise IOError,"addheader not called from eom()" raise IOError,"addheader not called from eom()"
self.log('addheader: %s=%s' % (field,value)) self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value self._msg[field] = value
self.headerschanged = True self._headerschanged = True
def delrcpt(self,rcpt): def delrcpt(self,rcpt):
if not self._body: if not self._body:
@@ -62,19 +83,38 @@ class TestBase(object):
raise IOError,"addrcpt not called from eom()" raise IOError,"addrcpt not called from eom()"
self._addrcpt.append(rcpt) self._addrcpt.append(rcpt)
## Save the reply codes and messages in self._reply.
def setreply(self,rcode,xcode,*msg): 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._body = None
self.bodyreplaced = False self._bodyreplaced = False
self.headerschanged = False self._headerschanged = False
self.reply = None self._reply = None
msg = rfc822.Message(fp) msg = rfc822.Message(fp)
rc = self.envfrom('<%s>'%sender) rc = self.envfrom('<%s>'%sender)
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
rc = self.envrcpt('<%s>'%rcpt) for rcpt in (rcpt,) + rcpts:
if rc != Milter.CONTINUE: return rc rc = self.envrcpt('<%s>'%rcpt)
if rc != Milter.CONTINUE: return rc
line = None line = None
for h in msg.headers: for h in msg.headers:
if h[:1].isspace(): if h[:1].isspace():
@@ -103,7 +143,7 @@ class TestBase(object):
self._msg = msg self._msg = msg
self._body = StringIO.StringIO() self._body = StringIO.StringIO()
rc = self.eom() rc = self.eom()
if self.bodyreplaced: if self._bodyreplaced:
body = self._body.getvalue() body = self._body.getvalue()
else: else:
msg.rewindbody() msg.rewindbody()
@@ -114,17 +154,24 @@ class TestBase(object):
self._body.write(body) self._body.write(body)
return rc return rc
def feedMsg(self,fname,sender="spam@adv.com",rcpt="victim@lamb.com"): ## Feed an email contained in a file to the milter.
fp = open('test/'+fname,'r') # This is a convenience method that invokes @link #feedFile feedFile @endlink.
rc = self.feedFile(fp,sender,rcpt) # @param sender MAIL FROM
fp.close() # @param rcpts RCPT TO, multiple recipients may be supplied
return rc 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'): def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
self._body = None self._body = None
self.bodyreplaced = False self._bodyreplaced = False
rc = super(TestBase,self).connect(host,1,(ip,1234)) rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE and rc != Milter.ACCEPT: if rc != Milter.CONTINUE:
self.close() self.close()
return rc return rc
rc = self.hello(helo) rc = self.hello(helo)
+12 -2
View File
@@ -54,7 +54,12 @@ class milterContext(object):
def chgfrom(self,sender,param=None): pass def chgfrom(self,sender,param=None): pass
## Tell the MTA which macro values we are interested in for a given stage. ## 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. # 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 <a href="https://www.milter.org/developers/api/smfi_setsymlist">smfi_setsymlist</a>.
# @param stage protocol stage in which the macro list should be used
def setsymlist(self,stage,macrolist): pass
class error(Exception): pass class error(Exception): pass
@@ -77,7 +82,12 @@ def set_abort_callback(cb): pass
def set_close_callback(cb): pass def set_close_callback(cb): pass
## Sets the return code for untrapped Python exceptions during a callback. ## 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 def set_exception_policy(code): pass
## Register python milter with libmilter. ## Register python milter with libmilter.
+13 -10
View File
@@ -35,6 +35,9 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $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 * Revision 1.32 2013/01/13 01:46:16 customdesigned
* Doc updates. * Doc updates.
* *
@@ -1490,23 +1493,23 @@ milter_progress(PyObject *self, PyObject *args) {
} }
#endif #endif
#ifdef SMFIF_SETSMLIST #ifdef SMFIF_SETSYMLIST
static const char milter_setsmlist__doc__[] = static const char milter_setsymlist__doc__[] =
"setsmlist(stage,macrolist) -> None\n\ "setsymlist(stage,macrolist) -> None\n\
Tell the MTA which macro values we are interested in for a given stage"; Tell the MTA which macro values we are interested in for a given stage";
static PyObject * static PyObject *
milter_setsmlist(PyObject *self, PyObject *args) { milter_setsymlist(PyObject *self, PyObject *args) {
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
int stage = 0; int stage = 0;
char *smlist = 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); ctx = _find_context(self);
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
t = PyEval_SaveThread(); 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"); "cannot set macro list");
} }
#endif #endif
@@ -1530,8 +1533,8 @@ static PyMethodDef context_methods[] = {
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__}, { "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
#endif #endif
#ifdef SMFIF_SETSMLIST #ifdef SMFIF_SETSYMLIST
{ "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__doc__}, { "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__doc__},
#endif #endif
{ NULL, NULL } { NULL, NULL }
}; };
@@ -1654,8 +1657,8 @@ initmilter(void) {
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
setitem(d,"CHGFROM",SMFIF_CHGFROM); setitem(d,"CHGFROM",SMFIF_CHGFROM);
#endif #endif
#ifdef SMFIF_SETSMLIST #ifdef SMFIF_SETSYMLIST
setitem(d,"SETSMLIST",SMFIF_SETSMLIST); setitem(d,"SETSYMLIST",SMFIF_SETSYMLIST);
setitem(d,"M_CONNECT",SMFIM_CONNECT);/* connect */ setitem(d,"M_CONNECT",SMFIM_CONNECT);/* connect */
setitem(d,"M_HELO",SMFIM_HELO); /* HELO/EHLO */ setitem(d,"M_HELO",SMFIM_HELO); /* HELO/EHLO */
setitem(d,"M_ENVFROM",SMFIM_ENVFROM);/* MAIL From */ setitem(d,"M_ENVFROM",SMFIM_ENVFROM);/* MAIL From */
+6
View File
@@ -75,6 +75,12 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%changelog %changelog
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 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 <stuart@bmsi.com> 0.9.7-1 * Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback - Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
- Remove redundant table in miltermodule - Remove redundant table in miltermodule
+6 -6
View File
@@ -19,7 +19,7 @@ class BMSMilterTestCase(unittest.TestCase):
self.failUnless(rc == Milter.CONTINUE) self.failUnless(rc == Milter.CONTINUE)
rc = milter.feedMsg(fname) rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced") self.failUnless(milter._bodyreplaced,"Message body not replaced")
fp = milter._body fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue()) open('test/'+fname+".tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
@@ -34,7 +34,7 @@ class BMSMilterTestCase(unittest.TestCase):
milter.connect('somehost') milter.connect('somehost')
rc = milter.feedMsg(fname) rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.")
fp = milter._body fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue()) open('test/'+fname+".tstout","w").write(fp.getvalue())
milter.close() milter.close()
@@ -44,17 +44,17 @@ class BMSMilterTestCase(unittest.TestCase):
milter.connect('somehost') milter.connect('somehost')
rc = milter.feedMsg('samp1') rc = milter.feedMsg('samp1')
self.failUnless(rc == Milter.ACCEPT) 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") rc = milter.feedMsg("virus3")
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced") self.failUnless(milter._bodyreplaced,"Message body not replaced")
fp = milter._body fp = milter._body
open("test/virus3.tstout","w").write(fp.getvalue()) open("test/virus3.tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read()) #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
rc = milter.feedMsg("virus6") rc = milter.feedMsg("virus6")
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced") self.failUnless(milter._bodyreplaced,"Message body not replaced")
self.failUnless(milter.headerschanged,"Message headers not adjusted") self.failUnless(milter._headerschanged,"Message headers not adjusted")
fp = milter._body fp = milter._body
open("test/virus6.tstout","w").write(fp.getvalue()) open("test/virus6.tstout","w").write(fp.getvalue())
milter.close() milter.close()