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()