Compare commits

..

31 Commits

Author SHA1 Message Date
cvs2svn d7ef47d76b This commit was manufactured by cvs2svn to create tag 'pymilter-1_0'.
Sprout from master 2014-03-01 23:38:51 UTC Stuart Gathman <stuart@gathman.org> 'Release 1.0-2'
Cherrypick from bmsi 2005-05-31 18:10:47 UTC Stuart Gathman <stuart@gathman.org> 'Release 0.7.2':
    test/big5
    test/bounce
    test/bounce1
    test/bound
    test/honey
    test/missingboundary
    test/samp1
    test/spam44
    test/spam7
    test/spam8
    test/test1
    test/test8
    test/virus1
    test/virus13
    test/virus2
    test/virus3
    test/virus4
    test/virus5
    test/virus6
    test/virus7
2014-03-01 23:38:52 +00:00
Stuart Gathman 8f7c090879 Release 1.0-2 2014-03-01 23:38:51 +00:00
Stuart Gathman d69c002020 Release 1.0 2014-03-01 23:30:12 +00:00
Stuart Gathman 980dc5f599 pymilter SELinux policy as addon package 2013-06-26 22:24:02 +00:00
Stuart Gathman 8770262622 Initial selinux policy support 2013-06-26 18:28:49 +00:00
Stuart Gathman af49a7a45e Clean while exporting, and handle exporting IP6 2013-06-16 03:39:47 +00:00
Stuart Gathman fca8d83370 Import and export csv for converting existing greylist database. 2013-05-22 18:25:13 +00:00
Stuart Gathman f28cab2d1c Doc updates 2013-04-18 04:06:02 +00:00
Stuart Gathman 76424c7c3f Selinux policy additions. 2013-04-18 04:04:42 +00:00
Stuart Gathman 3e1754acff Call opensocket to check and remove unix domain sockets before starting. 2013-04-18 04:03:36 +00:00
Stuart Gathman 40de08925d Recognize IPv6 localhost. 2013-03-27 02:21:30 +00:00
Stuart Gathman 522a631192 Update Doxyfile 2013-03-22 18:12:50 +00:00
Stuart Gathman 5c8c189330 Remove bad setreply example, doc updates. 2013-03-19 21:25:10 +00:00
Stuart Gathman 5330047902 Move many configs to datadir 2013-03-15 23:04:38 +00:00
Stuart Gathman a8f373ea65 Allow ACCEPT as an exception policy. 2013-03-15 20:50:01 +00:00
Stuart Gathman f357be1e99 Release 0.9.8 2013-03-14 22:11:26 +00:00
Stuart Gathman 84eeecf9a6 tabnanny, restore missing test email 2013-03-12 01:46:08 +00:00
Stuart Gathman a180b212c6 Call negotiate from test mixin so that the noreply exception works. 2013-03-11 23:52:21 +00:00
Stuart Gathman bd0df5d77a Accept any combination of lists and space separated strings. 2013-03-11 22:21:14 +00:00
Stuart Gathman 34746823f7 Use python locking to avoid busy wait. 2013-03-09 22:23:27 +00:00
Stuart Gathman baeddd9fa5 Make TestBase members private, fix getsymlist misspelling. 2013-03-09 05:42:14 +00:00
Stuart Gathman 4854f95b59 Handle varargs in setreply 2013-03-09 00:26:03 +00:00
Stuart Gathman 242f2fa78f Better untrapped exception message. const char for doc comments. 2013-03-09 00:25:23 +00:00
Stuart Gathman 1e0324399b Add mixin class for unit testing milters. 2013-03-08 17:37:20 +00:00
Stuart Gathman 078d9f2078 Read then write sqlite transactions must use BEGIN IMMEDIATE 2013-02-25 19:10:57 +00:00
Stuart Gathman ff06b5f1b4 Close Cursor objects explicitly 2013-02-17 05:13:38 +00:00
Stuart Gathman dd581f5d9a Optional sqlite3 greylist implementation. 2013-02-16 19:27:39 +00:00
Stuart Gathman 3fb9beb5c0 Testcase for greylist 2013-02-16 05:40:46 +00:00
Stuart Gathman b12c4c9746 Doc updates. 2013-01-13 01:46:17 +00:00
Stuart Gathman f3fbb1c99d Missing regex 2012-11-15 03:53:02 +00:00
Stuart Gathman 27887daf3f Release 0.9.7 2012-11-13 02:43:44 +00:00
32 changed files with 1622 additions and 1185 deletions
+2 -3
View File
@@ -1,5 +1,5 @@
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
kludge and added threading and context objects to it, wrote a proper OO kludge and added threading and context objects to it, wrote a proper OO
wrapper (Milter.py) that handles attachments, did lots of testing, packaged wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a it with distutils, and generally transformed it from a quick hack to a
@@ -7,7 +7,6 @@ real, usable Python extension.
Other contributors (in random order): Other contributors (in random order):
Daniel Troeder Daniel Troeder
for pointing out a typo in @noreply for pointing out a typo in @noreply
arkanes@irc.freenode.net arkanes@irc.freenode.net
@@ -44,4 +43,4 @@ Business Management Systems - http://www.bmsi.com
for hosting the website, and providing paying clients who need milter service for hosting the website, and providing paying clients who need milter service
so I can work on it as part of my day job. so I can work on it as part of my day job.
If I have left anybody out, send me a reminder: stuart@bmsi.com If I have left anybody out, send me a reminder: stuart@gathman.org
+687 -638
View File
File diff suppressed because it is too large Load Diff
+56 -59
View File
@@ -8,9 +8,10 @@
# Copyright 2001,2009 Business Management Systems, Inc. # Copyright 2001,2009 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details. # This code is under the GNU General Public License. See COPYING for details.
__version__ = '0.9.7' __version__ = '0.9.8'
import os import os
import re
import milter import milter
import thread import thread
@@ -20,12 +21,6 @@ from functools import wraps
_seq_lock = thread.allocate_lock() _seq_lock = thread.allocate_lock()
_seq = 0 _seq = 0
## @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 ored mask of optional actions to enable
def uniqueID(): def uniqueID():
"""Return a unique sequence number (incremented on each call). """Return a unique sequence number (incremented on each call).
""" """
@@ -48,6 +43,9 @@ OPTIONAL_CALLBACKS = {
'header':(P_NR_HDR,P_NOHDRS) 'header':(P_NR_HDR,P_NOHDRS)
} }
## @private
R = re.compile(r'%+')
## @private ## @private
def decode_mask(bits,names): def decode_mask(bits,names):
t = [ (s,getattr(milter,s)) for s in names] t = [ (s,getattr(milter,s)) for s in names]
@@ -103,7 +101,7 @@ def rejected_recipients(klass):
return enable_protocols(klass,P_RCPT_REJ) return enable_protocols(klass,P_RCPT_REJ)
## Milter leading space on headers. A class decorator that calls ## Milter leading space on headers. A class decorator that calls
# enable_protocols() with the P_HDR_LEADSPC flag. By default, # enable_protocols() with the P_HEAD_LEADSPC flag. By default,
# header continuation lines are collected and joined before getting # header continuation lines are collected and joined before getting
# sent to a milter. Headers modified or added by the milter are # sent to a milter. Headers modified or added by the milter are
# folded by the MTA as necessary according to its own standards. # folded by the MTA as necessary according to its own standards.
@@ -121,7 +119,7 @@ def rejected_recipients(klass):
# @param klass the %milter application class to modify # @param klass the %milter application class to modify
# @return the modified %milter class # @return the modified %milter class
def header_leading_space(klass): def header_leading_space(klass):
return enable_protocols(klass,P_HDR_LEADSPC) return enable_protocols(klass,P_HEAD_LEADSPC)
## Function decorator to disable callback methods. ## Function decorator to disable callback methods.
# If the MTA supports it, tells the MTA not to invoke this callback, # If the MTA supports it, tells the MTA not to invoke this callback,
@@ -223,7 +221,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
@@ -259,7 +257,7 @@ class Base(object):
## Defined by subclasses to write log messages. ## Defined by subclasses to write log messages.
def log(self,*msg): pass def log(self,*msg): pass
## Called for each connection to the MTA. Called by the ## Called for each connection to the MTA. Called by the
# <a href="milter_api/xxfi_connect.html"> # <a href="https://www.milter.org/developers/api/xxfi_connect">
# xxfi_connect</a> callback. # xxfi_connect</a> callback.
# The <code>hostname</code> provided by the local MTA is either # The <code>hostname</code> provided by the local MTA is either
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available. # the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
@@ -296,7 +294,7 @@ class Base(object):
@nocallback @nocallback
def hello(self,hostname): return CONTINUE def hello(self,hostname): return CONTINUE
## Called when the SMTP client says MAIL FROM. Called by the ## Called when the SMTP client says MAIL FROM. Called by the
# <a href="milter_api/xxfi_envfrom.html"> # <a href="https://www.milter.org/developers/api/xxfi_envfrom">
# xxfi_envfrom</a> callback. # xxfi_envfrom</a> callback.
# Returning REJECT rejects the message, but not the connection. # Returning REJECT rejects the message, but not the connection.
# The sender is the "envelope" from as defined by # The sender is the "envelope" from as defined by
@@ -307,7 +305,7 @@ class Base(object):
@nocallback @nocallback
def envfrom(self,f,*str): return CONTINUE def envfrom(self,f,*str): return CONTINUE
## Called when the SMTP client says RCPT TO. Called by the ## Called when the SMTP client says RCPT TO. Called by the
# <a href="milter_api/xxfi_envrcpt.html"> # <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
# xxfi_envrcpt</a> callback. # xxfi_envrcpt</a> callback.
# Returning REJECT rejects the current recipient, not the entire message. # Returning REJECT rejects the current recipient, not the entire message.
# The recipient is the "envelope" recipient as defined by # The recipient is the "envelope" recipient as defined by
@@ -373,7 +371,7 @@ class Base(object):
return p return p
## Negotiate milter protocol options. Called by the ## Negotiate milter protocol options. Called by the
# <a href="milter_api/xxfi_negotiate.html"> # <a href="https://www.milter.org/developers/api/xxfi_negotiate">
# xffi_negotiate</a> callback. This is an advanced callback, # xffi_negotiate</a> callback. This is an advanced callback,
# do not override unless you know what you are doing. Most # do not override unless you know what you are doing. Most
# negotiation can be done simply by using the supplied # negotiation can be done simply by using the supplied
@@ -404,7 +402,7 @@ class Base(object):
## Return the value of an MTA macro. Sendmail macro names ## Return the value of an MTA macro. Sendmail macro names
# are either single chars (e.g. "j") or multiple chars enclosed # are either single chars (e.g. "j") or multiple chars enclosed
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent. # in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
# See <a href="milter_api/smfi_getsymval.html"> # See <a href="https://www.milter.org/developers/api/smfi_getsymval">
# smfi_getsymval</a> for default sendmail macros. # smfi_getsymval</a> for default sendmail macros.
# @param sym the macro name # @param sym the macro name
def getsymval(self,sym): def getsymval(self,sym):
@@ -412,13 +410,13 @@ class Base(object):
## Set the SMTP reply code and message. ## Set the SMTP reply code and message.
# If the MTA does not support setmlreply, then only the # If the MTA does not support setmlreply, then only the
# first msg line is used. Any '%' in a message line # first msg line is used. Any '%%' in a message line
# must be doubled, or libmilter will silently ignore the setreply. # must be doubled, or libmilter will silently ignore the setreply.
# Beginning with 0.9.6, we test for that case and throw ValueError to avoid # Beginning with 0.9.6, we test for that case and throw ValueError to avoid
# head scratching. What will <i>really</i> irritate you, however, # head scratching. What will <i>really</i> irritate you, however,
# is that if you carefully double any '%%', your message will be # is that if you carefully double any '%%', your message will be
# sent - but with the '%%' still doubled! # sent - but with the '%%' still doubled!
# See <a href="milter_api/smfi_setreply.html"> # See <a href="https://www.milter.org/developers/api/smfi_setreply">
# smfi_setreply</a> for more information. # smfi_setreply</a> for more information.
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string. # @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>. # rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
@@ -434,22 +432,34 @@ class Base(object):
return self._ctx.setreply(rcode,xcode,msg,*ml) return self._ctx.setreply(rcode,xcode,msg,*ml)
## Tell the MTA which macro names will be used. ## Tell the MTA which macro names will be used.
# The <code>Milter.SETSMLIST</code> action flag must be set. # 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 <code>Milter.SETSYMLIST</code> action flag must be
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT,
# M_DATA, M_EOM, M_EOH.
# #
# May only be called from negotiate callback. # May only be called from negotiate callback.
# @since 0.9.2 # @since 0.9.8, previous version was misspelled!
# @param stage the protocol stage to set to macro list for # @param stage the protocol stage to set to macro list for,
# @param macros a string with a space delimited list of macros # one of the M_* constants defined in Milter
def setsmlist(self,stage,macros): # @param macros space separated and/or lists of strings
if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST") def setsymlist(self,stage,*macros):
if type(macros) in (list,tuple): if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
macros = ' '.join(macros) a = []
return self._ctx.setsmlist(stage,macros) for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(' ')
except: pass
a += m
return self._ctx.setsmlist(stage,' '.join(a))
# Milter methods which can only be called from eom callback. # Milter methods which can only be called from eom callback.
## Add a mail header field. ## Add a mail header field.
# Calls <a href="milter_api/smfi_addheader.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_addheader">
# smfi_addheader</a>. # smfi_addheader</a>.
# The <code>Milter.ADDHDRS</code> action flag must be set. # The <code>Milter.ADDHDRS</code> action flag must be set.
# #
@@ -463,7 +473,7 @@ class Base(object):
return self._ctx.addheader(field,value,idx) return self._ctx.addheader(field,value,idx)
## Change the value of a mail header field. ## Change the value of a mail header field.
# Calls <a href="milter_api/smfi_chgheader.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">
# smfi_chgheader</a>. # smfi_chgheader</a>.
# The <code>Milter.CHGHDRS</code> action flag must be set. # The <code>Milter.CHGHDRS</code> action flag must be set.
# #
@@ -477,7 +487,7 @@ class Base(object):
return self._ctx.chgheader(field,idx,value) return self._ctx.chgheader(field,idx,value)
## Add a recipient to the message. ## Add a recipient to the message.
# Calls <a href="milter_api/smfi_addrcpt.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">
# smfi_addrcpt</a>. # smfi_addrcpt</a>.
# If no corresponding mail header is added, this is like a Bcc. # 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 # The syntax of the recipient is the same as used in the SMTP
@@ -497,7 +507,7 @@ class Base(object):
raise DisabledAction("ADDRCPT_PAR") raise DisabledAction("ADDRCPT_PAR")
return self._ctx.addrcpt(rcpt,params) return self._ctx.addrcpt(rcpt,params)
## Delete a recipient from the message. ## Delete a recipient from the message.
# Calls <a href="milter_api/smfi_delrcpt.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">
# smfi_delrcpt</a>. # smfi_delrcpt</a>.
# The recipient should match one passed to the envrcpt callback. # The recipient should match one passed to the envrcpt callback.
# The <code>Milter.DELRCPT</code> action flag must be set. # The <code>Milter.DELRCPT</code> action flag must be set.
@@ -510,7 +520,7 @@ class Base(object):
return self._ctx.delrcpt(rcpt) return self._ctx.delrcpt(rcpt)
## Replace the message body. ## Replace the message body.
# Calls <a href="milter_api/smfi_replacebody.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">
# smfi_replacebody</a>. # smfi_replacebody</a>.
# The entire message body must be replaced. # The entire message body must be replaced.
# Call repeatedly with blocks of data until the entire body is transferred. # Call repeatedly with blocks of data until the entire body is transferred.
@@ -524,7 +534,7 @@ class Base(object):
return self._ctx.replacebody(body) return self._ctx.replacebody(body)
## Change the SMTP envelope sender address. ## Change the SMTP envelope sender address.
# Calls <a href="milter_api/smfi_chgfrom.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">
# smfi_chgfrom</a>. # smfi_chgfrom</a>.
# The syntax of the sender is that same as used in the SMTP # The syntax of the sender is that same as used in the SMTP
# MAIL FROM command (and as delivered to the envfrom callback), # MAIL FROM command (and as delivered to the envfrom callback),
@@ -541,7 +551,7 @@ class Base(object):
return self._ctx.chgfrom(sender,params) return self._ctx.chgfrom(sender,params)
## Quarantine the message. ## Quarantine the message.
# Calls <a href="milter_api/smfi_quarantine.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">
# smfi_quarantine</a>. # smfi_quarantine</a>.
# When quarantined, a message goes into the mailq as if to be delivered, # When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined. # but delivery is deferred until the message is unquarantined.
@@ -555,7 +565,7 @@ class Base(object):
return self._ctx.quarantine(reason) return self._ctx.quarantine(reason)
## Tell the MTA to wait a bit longer. ## Tell the MTA to wait a bit longer.
# Calls <a href="milter_api/smfi_progress.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_progress">
# smfi_progress</a>. # smfi_progress</a>.
# Resets timeouts in the MTA that detect a "hung" milter. # Resets timeouts in the MTA that detect a "hung" milter.
def progress(self): def progress(self):
@@ -569,9 +579,9 @@ class Milter(Base):
## Provide simple logging to sys.stdout ## Provide simple logging to sys.stdout
def log(self,*msg): def log(self,*msg):
print('Milter:',end=None) print 'Milter:',
for i in msg: print(i,end=None) for i in msg: print i,
print() print
@noreply @noreply
def connect(self,hostname,family,hostaddr): def connect(self,hostname,family,hostaddr):
@@ -708,28 +718,7 @@ def envcallback(c,args):
# @param socketname the socket to be passed to milter.setconn() # @param socketname the socket to be passed to milter.setconn()
# @param timeout the time in secs the MTA should wait for a response before # @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): def runmilter(name,socketname,timeout = 0,rmsock=True):
# 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,
# libmilter will throw a warning. If sendmail is running, this is still
# safe if there are no messages currently being processed. It's safer to
# shutdown sendmail, kill the filter process, restart the filter, and then
# restart sendmail.
pos = socketname.find(':')
if pos > 1:
s = socketname[:pos]
fname = socketname[pos+1:]
else:
s = "unix"
fname = socketname
if s == "unix" or s == "local":
print "Removing %s" % fname
try:
os.unlink(fname)
except os.error, x:
import errno
if x.errno != errno.ENOENT:
raise milter.error(x)
# The default flags set include everything # The default flags set include everything
# milter.set_flags(milter.ADDHDRS) # milter.set_flags(milter.ADDHDRS)
@@ -760,6 +749,14 @@ def runmilter(name,socketname,timeout = 0):
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd), unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
negotiate=ncb negotiate=ncb
) )
# We remove the socket here by default on the assumption that you will be
# starting this filter before sendmail. If sendmail is not running and the
# socket already exists, libmilter will throw a warning. If sendmail is
# running, this is still safe if there are no messages currently being
# processed. It's safer to shutdown sendmail, kill the filter process,
# restart the filter, and then restart sendmail.
milter.opensocket(rmsock)
start_seq = _seq start_seq = _seq
try: try:
milter.main() milter.main()
+12 -9
View File
@@ -10,6 +10,9 @@
# CBV results. # CBV results.
# #
# $Log$ # $Log$
# Revision 1.9 2008/05/08 21:35:57 customdesigned
# Allow explicitly whitelisted email from banned_users.
#
# Revision 1.8 2007/09/03 16:18:45 customdesigned # Revision 1.8 2007/09/03 16:18:45 customdesigned
# Delete unparseable timestamps when loading address cache. These have # Delete unparseable timestamps when loading address cache. These have
# arisen because of failure to parse MAIL FROM properly. Will have to # arisen because of failure to parse MAIL FROM properly. Will have to
@@ -72,8 +75,8 @@ class AddrCache(object):
except OSError: except OSError:
fp = () fp = ()
for ln in fp: for ln in fp:
try: try:
rcpt,ts = ln.strip().split(None,1) rcpt,ts = ln.strip().split(None,1)
try: try:
l = time.strptime(ts,AddrCache.time_format) l = time.strptime(ts,AddrCache.time_format)
t = time.mktime(l) t = time.mktime(l)
@@ -84,11 +87,11 @@ class AddrCache(object):
except: # unparsable timestamp - likely garbage except: # unparsable timestamp - likely garbage
changed = True changed = True
continue continue
except: # manual entry (no timestamp) except: # manual entry (no timestamp)
cache[ln.strip().lower()] = (now,None) cache[ln.strip().lower()] = (now,None)
wfp.write(ln) wfp.write(ln)
if changed: if changed:
lock.commit(self.fname+'.old') lock.commit(self.fname+'.old')
else: else:
lock.unlock() lock.unlock()
except IOError: except IOError:
@@ -126,13 +129,13 @@ class AddrCache(object):
ts,res = self.cache[lsender] ts,res = self.cache[lsender]
too_old = time.time() - self.age*24*60*60 # max age in days too_old = time.time() - self.age*24*60*60 # max age in days
if not ts or ts > too_old: if not ts or ts > too_old:
return res return res
del self.cache[lsender] del self.cache[lsender]
raise KeyError, sender raise KeyError, sender
except KeyError,x: except KeyError,x:
try: try:
user,host = sender.split('@',1) user,host = sender.split('@',1)
return self.__getitem__(host) return self.__getitem__(host)
except ValueError: except ValueError:
raise x raise x
+15 -13
View File
@@ -1,4 +1,5 @@
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
import os.path
class MilterConfigParser(ConfigParser): class MilterConfigParser(ConfigParser):
@@ -20,7 +21,7 @@ class MilterConfigParser(ConfigParser):
return [q.strip() for q in self.get(sect,opt).split(',')] return [q.strip() for q in self.get(sect,opt).split(',')]
return [] return []
def getaddrset(self,sect,opt): def getaddrset(self,sect,opt,dir=''):
if not self.has_option(sect,opt): if not self.has_option(sect,opt):
return {} return {}
s = self.get(sect,opt) s = self.get(sect,opt)
@@ -29,13 +30,14 @@ class MilterConfigParser(ConfigParser):
q = q.strip() q = q.strip()
if q.startswith('file:'): if q.startswith('file:'):
domain = q[5:].lower() domain = q[5:].lower()
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split() fname = os.path.join(dir,domain)
d[domain] = d.setdefault(domain,[]) + open(fname,'r').read().split()
else: else:
user,domain = q.split('@') user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user) d.setdefault(domain.lower(),[]).append(user)
return d return d
def getaddrdict(self,sect,opt): def getaddrdict(self,sect,opt,dir=''):
if not self.has_option(sect,opt): if not self.has_option(sect,opt):
return {} return {}
d = {} d = {}
@@ -43,14 +45,14 @@ class MilterConfigParser(ConfigParser):
q = q.strip() q = q.strip()
if self.has_option(sect,q): if self.has_option(sect,q):
l = self.get(sect,q) l = self.get(sect,q)
for addr in l.split(','): for addr in l.split(','):
addr = addr.strip() addr = addr.strip()
if addr.startswith('file:'): if addr.startswith('file:'):
fname = addr[5:] fname = os.path.join(dir,addr[5:])
for a in open(fname,'r').read().split(): for a in open(fname,'r').read().split():
d[a] = q d[a] = q
else: else:
d[addr] = q d[addr] = q
return d return d
def getdefault(self,sect,opt,default=None): def getdefault(self,sect,opt,default=None):
+1 -2
View File
@@ -73,7 +73,6 @@ class Session(object):
if name.endswith('.'): name = name[:-1] if name.endswith('.'): name = name[:-1]
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True): if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
return [] # invalid DNS name (too long or empty) return [] # invalid DNS name (too long or empty)
name = name.lower()
result = self.cache.get( (name, qtype) ) result = self.cache.get( (name, qtype) )
cname = None cname = None
if result: return result if result: return result
@@ -97,7 +96,7 @@ class Session(object):
#return result # if too many == NX_DOMAIN #return result # if too many == NX_DOMAIN
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME) raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname cnames[name] = cname
if cname.lower().rstrip('.') in cnames: if cname in cnames:
raise DNSError('CNAME loop') raise DNSError('CNAME loop')
result = self.dns(cname, qtype, cnames=cnames) result = self.dns(cname, qtype, cnames=cnames)
if result: if result:
+11 -8
View File
@@ -5,6 +5,9 @@
# Send DSNs, do call back verification, # Send DSNs, do call back verification,
# and generate DSN messages from a template # and generate DSN messages from a template
# $Log$ # $Log$
# Revision 1.22 2011/03/18 20:41:31 customdesigned
# Python2.6 SMTP.close() fails when instance never connected.
#
# Revision 1.21 2011/03/03 05:11:58 customdesigned # Revision 1.21 2011/03/03 05:11:58 customdesigned
# Release 0.9.4 # Release 0.9.4
# #
@@ -114,17 +117,17 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
if a[0] == receiver: if a[0] == receiver:
return (553,'Fraudulent MX for %s: %s' % (domain,host)) return (553,'Fraudulent MX for %s: %s' % (domain,host))
if not (200 <= code <= 299): if not (200 <= code <= 299):
raise smtplib.SMTPHeloError(code, resp) raise smtplib.SMTPHeloError(code, resp)
if msg: if msg:
try: try:
smtp.sendmail('<%s>'%ourfrom,mailfrom,msg) smtp.sendmail('<%s>'%ourfrom,mailfrom,msg)
except smtplib.SMTPSenderRefused: except smtplib.SMTPSenderRefused:
# does not accept DSN, try postmaster (at the risk of mail loops) # does not accept DSN, try postmaster (at the risk of mail loops)
smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg) smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg)
else: # CBV else: # CBV
code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom) code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom)
if code != 250: if code != 250:
raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom) raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom)
if isinstance(mailfrom,basestring): if isinstance(mailfrom,basestring):
mailfrom = [mailfrom] mailfrom = [mailfrom]
badrcpts = {} badrcpts = {}
@@ -132,7 +135,7 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
code,resp = smtp.rcpt(rcpt) code,resp = smtp.rcpt(rcpt)
if code not in (250,251): if code not in (250,251):
badrcpts[rcpt] = (code,resp)# permanent error badrcpts[rcpt] = (code,resp)# permanent error
smtp.quit() smtp.quit()
if len(badrcpts) == 1: if len(badrcpts) == 1:
return badrcpts.values()[0] # permanent error return badrcpts.values()[0] # permanent error
if badrcpts: if badrcpts:
+3 -3
View File
@@ -68,8 +68,8 @@ def is_dynip(host,addr):
if ia[2:] in (g[:2],g[-2:]): return True if ia[2:] in (g[:2],g[-2:]): return True
for m in ip3.finditer(host): for m in ip3.finditer(host):
if int(m.group()) == ia[3]: if int(m.group()) == ia[3]:
h = host[:m.start()] + '<3>' + host[m.end():] h = host[:m.start()] + '<3>' + host[m.end():]
break break
if rehmac.search(h): return True if rehmac.search(h): return True
if host.find(''.join(a[:3])) >= 0: return True if host.find(''.join(a[:3])) >= 0: return True
if host.find(''.join(a[1:])) >= 0: return True if host.find(''.join(a[1:])) >= 0: return True
@@ -86,7 +86,7 @@ if __name__ == '__main__':
if a[3:5] == ['connect','from']: if a[3:5] == ['connect','from']:
host = a[5] host = a[5]
if host.startswith('[') and host.endswith(']'): if host.startswith('[') and host.endswith(']'):
continue # no PTR continue # no PTR
ip = a[7][2:-2] ip = a[7][2:-2]
if ip in seen: continue if ip in seen: continue
seen.add(ip) seen.add(ip)
+54 -8
View File
@@ -18,13 +18,19 @@ def quoteAddress(s):
class Record(object): class Record(object):
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' ) __slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
def __init__(self): def __init__(self,timeinc=0):
now = time.time() now = time.time() + timeinc
self.firstseen = now self.firstseen = now
self.lastseen = now self.lastseen = now
self.cnt = 0 self.cnt = 0
self.umis = None self.umis = None
def __str__(self):
return "Grey[%s:%s:%s:%d]" % (
time.ctime(self.firstseen),time.ctime(self.lastseen),
self.umis,self.cnt
)
class Greylist(object): class Greylist(object):
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36): def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
@@ -34,8 +40,38 @@ class Greylist(object):
self.greylist_retain = grey_retain * 24 * 3600 # days self.greylist_retain = grey_retain * 24 * 3600 # days
self.dbp = shelve.open(dbname,'c',protocol=2) self.dbp = shelve.open(dbname,'c',protocol=2)
self.lock = thread.allocate_lock() self.lock = thread.allocate_lock()
def export_csv(self,fp,timeinc=0):
"Export records to csv."
import csv
dbp = self.dbp
w = csv.writer(fp)
now = time.time() + timeinc
for key, r in dbp.iteritems():
if now > r.lastseen + self.greylist_retain: continue
ip,sender,recipient = key.rsplit(':',2)
w.writerow([ip,sender,recipient,r.firstseen,r.lastseen,r.cnt,r.umis])
def clean(self,timeinc=0):
"Delete records past the retention limit."
now = time.time() + timeinc
cnt = 0
dbp = self.dbp
for key, r in dbp.iteritems():
#print key,r,time.ctime(now)
if now > r.lastseen + self.greylist_retain:
self.lock.acquire()
try:
r = dbp[key]
now = time.time() + timeinc
if now > r.lastseen + self.greylist_retain:
del dbp[key]
cnt += 1
finally:
self.lock.release()
return cnt
def check(self,ip,sender,recipient): def check(self,ip,sender,recipient,timeinc=0):
"Return number of allowed messages for greylist triple." "Return number of allowed messages for greylist triple."
sender = quoteAddress(sender) sender = quoteAddress(sender)
recipient = quoteAddress(recipient) recipient = quoteAddress(recipient)
@@ -45,15 +81,15 @@ class Greylist(object):
dbp = self.dbp dbp = self.dbp
try: try:
r = dbp[key] r = dbp[key]
now = time.time() now = time.time() + timeinc
if now > r.lastseen + self.greylist_retain: if now > r.lastseen + self.greylist_retain:
# expired # expired
log.debug('Expired greylist: %s',key) log.debug('Expired greylist: %s',key)
r = Record() r = Record(timeinc)
elif now < r.firstseen + self.greylist_time + 5: elif now < r.firstseen + self.greylist_time + 5:
# still greylisted # still greylisted
log.debug('Early greylist: %s',key) log.debug('Early greylist: %s',key)
#r = Record() #r = Record(timeinc)
r.lastseen = now r.lastseen = now
elif r.cnt or now < r.firstseen + self.greylist_expire: elif r.cnt or now < r.firstseen + self.greylist_expire:
# in greylist window or active # in greylist window or active
@@ -63,12 +99,22 @@ class Greylist(object):
else: else:
# passed greylist window # passed greylist window
log.debug('Late greylist: %s',key) log.debug('Late greylist: %s',key)
r = Record() r = Record(timeinc)
dbp[key] = r dbp[key] = r
except: except:
r = Record() r = Record(timeinc)
dbp[key] = r dbp[key] = r
dbp.sync() dbp.sync()
finally: finally:
self.lock.release() self.lock.release()
return r.cnt return r.cnt
def close(self):
self.dbp.close()
if __name__ == '__main__':
import sys
g = Greylist(sys.argv[1],5,24,36)
try:
g.export_csv(sys.stdout)
finally: g.close()
+106
View File
@@ -0,0 +1,106 @@
import time
import logging
import urllib
import sqlite3
import thread
from datetime import datetime
log = logging.getLogger('milter.greylist')
_db_lock = thread.allocate_lock()
class Greylist(object):
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
self.ignoreLastByte = False
self.greylist_time = grey_time * 60 # minutes
self.greylist_expire = grey_expire * 3600 # hours
self.greylist_retain = grey_retain * 24 * 3600 # days
self.conn = sqlite3.connect(dbname)
self.conn.row_factory = sqlite3.Row
try:
self.conn.execute('''create table greylist(
ip text , sender text, recipient text,
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
primary key (ip,sender,recipient))''')
except: pass
def import_csv(self,fp):
import csv
rdr = csv.reader(fp)
cur = self.conn.execute('begin immediate')
try:
for r in rdr:
cur.execute('''insert into
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
values(?,?,?,?,?,?,?)''', r)
self.conn.commit()
finally:
cur.close();
def clean(self,timeinc=0):
"Delete records past the retention limit."
now = time.time() + timeinc - self.greylist_retain
cur = self.conn.cursor()
try:
cur.execute('delete from greylist where lastseen < ?',(now,))
cnt = cur.rowcount
self.conn.commit()
finally: cur.close()
return cnt
def check(self,ip,sender,recipient,timeinc=0):
"Return number of allowed messages for greylist triple."
_db_lock.acquire()
cur = self.conn.execute('begin immediate')
try:
cur.execute('''select firstseen,lastseen,cnt,umis from greylist where
ip=? and sender=? and recipient=?''',(ip,sender,recipient))
r = cur.fetchone()
now = time.time() + timeinc
cnt = 0
if not r:
cur.execute('''insert into
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
values(?,?,?,?,?,?,?)''', (ip,sender,recipient,now,now,0,None))
elif now > r['lastseen'] + self.greylist_retain:
# expired
log.debug('Expired greylist: %s:%s:%s',ip,sender,recipient)
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
where ip=? and sender=? and recipient=?''',
(now,now,0,None,ip,sender,recipient))
elif now < r['firstseen'] + self.greylist_time + 5:
# still greylisted
log.debug('Early greylist: %s:%s:%s',ip,sender,recipient)
#r = Record()
cur.execute('''update greylist set lastseen=?
where ip=? and sender=? and recipient=?''',
(now,ip,sender,recipient))
elif r['cnt'] or now < r['firstseen'] + self.greylist_expire:
# in greylist window or active
cnt = r['cnt'] + 1
cur.execute('''update greylist set lastseen=?,cnt=?
where ip=? and sender=? and recipient=?''',
(now,cnt,ip,sender,recipient))
log.debug('Active greylist(%d): %s:%s:%s',cnt,ip,sender,recipient)
else:
# passed greylist window
log.debug('Late greylist: %s:%s:%s',ip,sender,recipient)
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
where ip=? and sender=? and recipient=?''',
(now,now,0,None,ip,sender,recipient))
self.conn.commit()
finally:
cur.close()
_db_lock.release()
return cnt
def close(self):
self.conn.close()
if __name__ == '__main__':
import sys
g = Greylist(sys.argv[1])
try:
g.import_csv(sys.stdin)
finally: g.close()
+3 -3
View File
@@ -31,8 +31,8 @@ class PLock(object):
os.chown(self.lockname,-1,st.st_gid) os.chown(self.lockname,-1,st.st_gid)
except: except:
if strict_perms: if strict_perms:
self.unlock() self.unlock()
raise raise
return self.fp return self.fp
def wlock(self,lockname=None): def wlock(self,lockname=None):
@@ -51,7 +51,7 @@ class PLock(object):
self.fp = None self.fp = None
if backname: if backname:
try: try:
os.remove(backname) os.remove(backname)
except OSError: pass except OSError: pass
os.link(self.basename,backname) os.link(self.basename,backname)
os.rename(self.lockname,self.basename) os.rename(self.lockname,self.basename)
+21 -21
View File
@@ -48,11 +48,11 @@ def inet_ntop(s):
e = n[:l] e = n[:l]
for i in range(9-l): for i in range(9-l):
if a[i:i+l] == e: if a[i:i+l] == e:
if i == 0: if i == 0:
return ':'+':%x'*(8-l) % a[l:] return ':'+':%x'*(8-l) % a[l:]
if i == 8 - l: if i == 8 - l:
return '%x:'*(8-l) % a[:-l] + ':' return '%x:'*(8-l) % a[:-l] + ':'
return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:] return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:]
return "%x:%x:%x:%x:%x:%x:%x:%x" % a return "%x:%x:%x:%x:%x:%x:%x:%x" % a
def inet_pton(p): def inet_pton(p):
@@ -89,29 +89,29 @@ def inet_pton(p):
m = RE_IP4.search(s) m = RE_IP4.search(s)
try: try:
if m: if m:
pos = m.start() pos = m.start()
ip4 = [int(i) for i in s[pos:].split('.')] ip4 = [int(i) for i in s[pos:].split('.')]
if not pos: if not pos:
return struct.pack('!QLBBBB',0,65535,*ip4) return struct.pack('!QLBBBB',0,65535,*ip4)
s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4) s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4)
a = s.split('::') a = s.split('::')
if len(a) == 2: if len(a) == 2:
l,r = a l,r = a
if not l: if not l:
r = r.split(':') r = r.split(':')
return struct.pack('!HHHHHHHH', return struct.pack('!HHHHHHHH',
*[0]*(8-len(r)) + [int(s,16) for s in r]) *[0]*(8-len(r)) + [int(s,16) for s in r])
if not r: if not r:
l = l.split(':') l = l.split(':')
return struct.pack('!HHHHHHHH', return struct.pack('!HHHHHHHH',
*[int(s,16) for s in l] + [0]*(8-len(l))) *[int(s,16) for s in l] + [0]*(8-len(l)))
l = l.split(':') l = l.split(':')
r = r.split(':') r = r.split(':')
return struct.pack('!HHHHHHHH', return struct.pack('!HHHHHHHH',
*[int(s,16) for s in l] + [0]*(8-len(l)-len(r)) *[int(s,16) for s in l] + [0]*(8-len(l)-len(r))
+ [int(s,16) for s in r]) + [int(s,16) for s in r])
if len(a) == 1: if len(a) == 1:
return struct.pack('!HHHHHHHH', return struct.pack('!HHHHHHHH',
*[int(s,16) for s in a[0].split(':')]) *[int(s,16) for s in a[0].split(':')])
except ValueError: pass except ValueError: pass
raise ValueError,p raise ValueError,p
+192
View File
@@ -0,0 +1,192 @@
## @package Milter.test
# A test framework for milters
import rfc822
import StringIO
import Milter
Milter.NOREPLY = Milter.CONTINUE
## 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):
def __init__(self,logfile='test/milter.log'):
self._protocol = 0
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 the %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
## 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
else:
raise IOError,"replacebody not called from eom()"
# FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a %milter
def chgheader(self,field,idx,value):
if not self._body:
raise IOError,"chgheader not called from eom()"
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '':
del self._msg[field]
else:
self._msg[field] = value
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
def delrcpt(self,rcpt):
if not self._body:
raise IOError,"delrcpt not called from eom()"
self._delrcpt.append(rcpt)
def addrcpt(self,rcpt):
if not self._body:
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
def setsymlist(self,stage,macros):
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
# not used yet, but just for grins we save the data
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(' ')
except: pass
a += m
self._symlist[stage] = set(a)
## 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
msg = rfc822.Message(fp)
rc = self.envfrom('<%s>'%sender)
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():
line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
if len(s) > 1: val = s[1].strip()
else: val = ''
rc = self.header(s[0],val)
if rc != Milter.CONTINUE: return rc
line = h
if line:
s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
while 1:
buf = fp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = StringIO.StringIO()
rc = self.eom()
if self._bodyreplaced:
body = self._body.getvalue()
else:
msg.rewindbody()
body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body)
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
opts = [ Milter.CURR_ACTS,~0,0,0 ]
rc = self.negotiate(opts)
rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE:
self.close()
return rc
rc = self.hello(helo)
if rc != Milter.CONTINUE:
self.close()
return rc
+8 -37
View File
@@ -6,14 +6,11 @@ import re
import struct import struct
import socket import socket
import email.Errors import email.Errors
import email.base64mime
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from email.Header import decode_header from email.Header import decode_header
from binascii import a2b_base64
#import email.Utils #import email.Utils
import rfc822 import rfc822
dnsre = re.compile(r'^[a-z][-a-z\d.]+$', re.IGNORECASE)
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4) PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
ip4re = re.compile(PAT_IP4+'$') ip4re = re.compile(PAT_IP4+'$')
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$' ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
@@ -70,12 +67,6 @@ def iniplist(ipaddr,iplist):
True True
>>> iniplist('192.168.0.45',['192.168.0.*']) >>> iniplist('192.168.0.45',['192.168.0.*'])
True True
>>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
True
>>> iniplist('2607:f8b0:4004:801::',['google.com/40'])
True
>>> iniplist('4.2.2.2',['nothing.example.com'])
False
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48']) >>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
True True
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48']) >>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
@@ -84,10 +75,8 @@ def iniplist(ipaddr,iplist):
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3 ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
""" """
if ip4re.match(ipaddr): if ip4re.match(ipaddr):
fam = socket.AF_INET
ipnum = addr2bin(ipaddr) ipnum = addr2bin(ipaddr)
elif ip6re.match(ipaddr): elif ip6re.match(ipaddr):
fam = socket.AF_INET6
ipnum = bin2long6(inet_pton(ipaddr)) ipnum = bin2long6(inet_pton(ipaddr))
else: else:
raise ValueError('Invalid ip syntax:'+ipaddr) raise ValueError('Invalid ip syntax:'+ipaddr)
@@ -95,25 +84,18 @@ def iniplist(ipaddr,iplist):
p = pat.split('/',1) p = pat.split('/',1)
if ip4re.match(p[0]): if ip4re.match(p[0]):
if len(p) > 1: if len(p) > 1:
n = int(p[1]) n = int(p[1])
else: else:
n = 32 n = 32
if cidr(addr2bin(p[0]),n) == cidr(ipnum,n): if cidr(addr2bin(p[0]),n) == cidr(ipnum,n):
return True return True
elif ip6re.match(p[0]): elif ip6re.match(p[0]):
if len(p) > 1: if len(p) > 1:
n = int(p[1]) n = int(p[1])
else: else:
n = 128 n = 128
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6): if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
return True return True
elif dnsre.match(p[0]):
try:
sfx = '/'.join(['']+p[1:])
addrlist = [r[4][0]+sfx for r in socket.getaddrinfo(p[0],25,fam)]
if iniplist(ipaddr,addrlist):
return True
except socket.gaierror: pass
elif fnmatchcase(ipaddr,pat): elif fnmatchcase(ipaddr,pat):
return True return True
return False return False
@@ -121,7 +103,6 @@ def iniplist(ipaddr,iplist):
## Split email into Fullname and address. ## Split email into Fullname and address.
# This replaces <code>email.Utils.parseaddr</code> but fixes # This replaces <code>email.Utils.parseaddr</code> but fixes
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>. # some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
# Additional tricky cases are still broken. Patches welcome.
# #
def parseaddr(t): def parseaddr(t):
"""Split email into Fullname and address. """Split email into Fullname and address.
@@ -159,17 +140,7 @@ def parseaddr(t):
addrspec = addrspec[pos1+1:] addrspec = addrspec[pos1+1:]
return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec)) return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
return res return res
## Fix email.base64mime.decode to add any missing padding
def decode(s, convert_eols=None):
if not s: return s
while len(s) % 4: s += '=' # add missing padding
dec = a2b_base64(s)
if convert_eols:
return dec.replace(CRLF, convert_eols)
return dec
email.base64mime.decode = decode
def parse_addr(t): def parse_addr(t):
"""Split email into user,domain. """Split email into user,domain.
@@ -214,15 +185,15 @@ def parse_header(val):
for s,enc in h: for s,enc in h:
if enc: if enc:
try: try:
u.append(unicode(s,enc,'replace')) u.append(unicode(s,enc,'replace'))
except LookupError: except LookupError:
u.append(unicode(s)) u.append(unicode(s))
else: else:
u.append(unicode(s)) u.append(unicode(s))
u = u''.join(u) u = ''.join(u)
for enc in ('us-ascii','iso-8859-1','utf-8'): for enc in ('us-ascii','iso-8859-1','utf8'):
try: try:
return u.encode(enc) return u.encode(enc)
except UnicodeError: continue except UnicodeError: continue
except UnicodeDecodeError: pass except UnicodeDecodeError: pass
except LookupError: pass except LookupError: pass
+28 -7
View File
@@ -11,24 +11,25 @@ any point, tell Sendmail to reject, discard, or accept the message.
Requirements Requirements
------------ ------------
Python milter extension: http://https://pypi.python.org/pypi/pymilter/ This python milter extension: http://www.bmsi.com/python/milter.html
Python: http://www.python.org Python: http://www.python.org
Sendmail: http://www.sendmail.org Sendmail: http://www.sendmail.org
NB: From Sendmail's libmilter/README: NB: From Sendmail's libmilter/README:
libmilter requires pthread support in the operating system. Moreover, it libmilter requires pthread support in the operating system. Moreover, it
requires that the library functions it uses are thread safe; which is true requires that the library functions it uses are thread safe; which is true
for the operating systems libmilter has been developed and tested on. On for the operating systems libmilter has been developed and tested on. On
some operating systems this requires special compile time options (e.g., some operating systems this requires special compile time options (e.g.,
not just -pthread). libmilter is currently known to work on (modulo problems not just -pthread). libmilter is currently known to work on (modulo
in the pthread support of some specific versions): problems in the pthread support of some specific versions):
FreeBSD 3.x, 4.x FreeBSD 3.x, 4.x
SunOS 5.x (x >= 5) SunOS 5.x (x >= 5)
AIX 4.3.x AIX 4.3.x
HP UX 11.x HP UX 11.x
Linux (recent versions/distributions) Linux (recent versions/distributions)
OpenBSD
AIX 4.1.5
libmilter is currently not supported on: libmilter is currently not supported on:
@@ -109,11 +110,31 @@ _FFR_MILTER for the cf macros. For example,
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
RedHat 6.2 Notes
----------------
The Redhat 6.2 sendmail RPM does not enable milter. You can obtain a
modified spec file at
http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec
use it to rebuild the Redhat 7.2 SRPM. The RH6.2 SRPM does not have
recent sendmail security patches.
RedHat 7.2 Notes
----------------
The Redhat 7.2 sendmail RPM enables milter in sendmail - but does not include
the headers needed for compiling a milter. You can obtain a modified spec
file with a sendmail-devel package that includes the needed static libraries
and headers at
http://www.bmsi.com/linux/sendmail-rh72.spec
IPv6 Notes IPv6 Notes
---------- ----------
IPv6 is still experimental.
The IPv6 protocol is supported if your operation system supports it The IPv6 protocol is supported if your operation system supports it
and if sendmail was compiled with IPv6 support. To determine if your and if sendmail was compiled with IPv6 support. To determine if your
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6 sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
@@ -171,7 +192,7 @@ Authors
------- -------
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
kludge and added threading and context objects to it, wrote a proper OO kludge and added threading and context objects to it, wrote a proper OO
wrapper (Milter.py) that handles attachments, did lots of testing, packaged wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a it with distutils, and generally transformed it from a quick hack to a
-4
View File
@@ -1,6 +1,2 @@
Support smfi_negotiate and auto negotiate only those callbacks for which
Milter.Milter methods have been overridden. (Python should be able to
do that.)
Lookup exact RFC syntax of real name / email and make Lookup exact RFC syntax of real name / email and make
Milter.utils.parse_addr() pass all unit tests. Milter.utils.parse_addr() pass all unit tests.
+15 -45
View File
@@ -1,46 +1,48 @@
## @mainpage Writing Milters in Python ## @mainpage Writing Milters in Python
# #
# At the lowest level, the <code>milter</code> module provides a thin wrapper # At the lowest level, the <code>milter</code> module provides a thin wrapper
# around the <a href="milter_api/index.html"> sendmail # around the <a href="https://www.milter.org/developers/api/index"> sendmail
# libmilter API</a>. This API lets you register callbacks for a number of # libmilter API</a>. This API lets you register callbacks for a number of
# events in the process of sendmail receiving a message via SMTP. These # events in the process of sendmail receiving a message via SMTP. These
# events include the initial connection from a MTA, the envelope sender and # events include the initial connection from a MTA, the envelope sender and
# recipients, the top level mail headers, and the message body. There are # recipients, the top level mail headers, and the message body. There are
# options to mangle all of these components of the message as it passes through # options to mangle all of these components of the message as it passes through
# the milter. # the %milter.
# #
# At the next level, the <code>Milter</code> module (note the case difference) # At the next level, the <code>Milter</code> module (note the case difference)
# provides a Python friendly object oriented wrapper for the low level API. To # provides a Python friendly object oriented wrapper for the low level API. To
# use the Milter module, an application registers a 'factory' to create an # use the Milter module, an application registers a 'factory' to create an
# object for each connection from a MTA to sendmail. These connection objects # object for each connection from a MTA to sendmail. These connection objects
# must provide methods corresponding to the libmilter callback events. # must provide methods corresponding to the libmilter event callbacks.
# #
# Each event method returns a code to tell sendmail whether to proceed with # Each callback method returns a code to tell sendmail whether to proceed with
# processing the message. This is a big advantage of milters over other mail # processing the message. This is a big advantage of milters over other mail
# filtering systems. Unwanted mail can be stopped in its tracks at the # filtering systems. Unwanted mail can be stopped in its tracks at the
# earliest possible point. # earliest possible point. The callback return codes are
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
# #
# The <code>Milter.Base</code> class provides default implementations for # The Milter.Base class provides default implementations for
# event methods that do nothing, and also provides wrappers for the libmilter # event methods that do nothing, and also provides wrappers for the libmilter
# methods to mutate the message. It automatically negotiates with MTA # methods to mutate the message. It automatically negotiates with MTA
# which protocol steps need to be processed by the milter, based on # which protocol steps need to be processed by the %milter, based on
# which callback methods are overridden. # which callback methods are overridden.
# #
# The <code>Milter.Milter</code> class provides an alternate default # The Milter.Milter class provides an alternate default
# implementation that logs the main milter events, but otherwise does nothing. # implementation that logs the main milter callbacks, but otherwise does
# It is provided for compatibility. # nothing. It is provided for compatibility.
# #
# The <code>mime</code> module provides a wrapper for the Python email package # The mime module provides a wrapper for the Python email package
# that fixes some bugs, and simplifies modifying selected parts of a MIME # that fixes some bugs, and simplifies modifying selected parts of a MIME
# message. # message.
# #
# @section threading # @section threading
# #
# The libmilter library which pymilter wraps # The libmilter library which pymilter wraps
# <a href="milter_overview#SignalHandling">handles # <a href="https://www.milter.org/developers/overview#SignalHandling">handles
# all signals</a> itself, and expects to be called from a single main thread. # all signals</a> itself, and expects to be called from a single main thread.
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to # It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
# <a href="milter_api/smfi_stop.html">smfi_stop</a> # <a href="https://www.milter.org/developers/api/smfi_stop">smfi_stop</a>
# and the last to an internal ABORT. # and the last to an internal ABORT.
# #
# If you use python threads or threading modules, then signal handling gets # If you use python threads or threading modules, then signal handling gets
@@ -52,35 +54,3 @@
# multiprocessing</a> module useful. It can be a drop-in # multiprocessing</a> module useful. It can be a drop-in
# replacement for threading as illustrated in # replacement for threading as illustrated in
# <a href="milter-template_8py-example.html">milter-template.py</a>. # <a href="milter-template_8py-example.html">milter-template.py</a>.
#
# @section Useful python packages for milters
#
# <a href="https://pypi.python.org/pypi/pyspf">pyspf</a> checks the
# SMTP envelope sender (MAIL FROM, passed to the Milter.Base.envfrom callback)
# against a Sender Policy published in DNS by the sending domain. This
# can prevent forgery of the MAIL FROM. SPF is Sender Policy Framework.
#
# <a href="https://launchpad.net/dkimpy">pydkim</a> checks a DKIM signature
# of the email body and headers against a public key published in DNS by
# the signing domain. DKIM is DomainKeys Identified Mail.
#
# The <a href="https://pypi.python.org/pypi/authres/">authres</a> module
# parses and formats the Authentication-Results email header, providing
# a standard place to summarize the results from DKIM, SPF, rDNS, SMTP AUTH,
# and other email authentication methods.
#
# <a href="https://pypi.python.org/pypi/pydspam/">pydspam</a> wraps
# the libdspam API of the <a href="http://dspam.sourceforge.net/">DSPAM</a>
# project.
#
# @section Milters written with pymilter
#
# <a href="https://github.com/croessner/vrfydmn">Verify Domain</a> is a
# Postfix milter that rejects/fixes manipulated From: header
# on a mail host with multiple virtual domains.
#
# <a href="https://pypi.python.org/pypi/milter/">BMS Milter</a> has several
# milters, a big complicated spam filter that integrates multiple
# authentication protocols with pydpsm, and two simple ones: spfmilter.py and
# dkim-milter.py.
#
+105 -37
View File
@@ -3,10 +3,65 @@
## @package milter ## @package milter
# #
# A thin wrapper around libmilter. # A thin wrapper around libmilter. Most users will not import
# milter directly, but will instead import Milter and subclass
# Milter.Base. This module gives you ultimate low level control
# from python.
# #
## Hold context for a milter connection. ## Continue processing the current connection, message, or recipient.
CONTINUE = 0
## For a connection-oriented routine, reject this connection;
# call Milter.Base.close(). For a message-oriented routine, except
# Milter.Base.eom() or Milter.Base.abort(), reject this message. For a
# recipient-oriented routine, reject the current recipient (but continue
# processing the current message).
REJECT = 1
## For a message- or recipient-oriented routine, accept this message, but
# silently discard it. SMFIS_DISCARD should not be returned by a
# connection-oriented routine.
DISCARD = 2
## For a connection-oriented routine, accept this connection without further
# filter processing; call Milter.Base.close(). For a message- or
# recipient-oriented routine, accept this message without further filtering.
ACCEPT = 3
## Return a temporary failure, i.e., the corresponding SMTP command will return
# an appropriate 4xx status code. For a message-oriented routine, except
# Milter.Base.envfrom(), fail for this message. For a connection-oriented
# routine, fail for this connection; call Milter.Base.close(). For a recipient-oriented
# routine, only
# fail for the current recipient; continue message processing.
TEMPFAIL = 4
## Skip further callbacks of the same type in this transaction.
# Currently this return value is only allowed in Milter.Base.body(). It can be
# used if a %milter has received sufficiently many body chunks to make a
# decision, but still wants to invoke message modification functions that are
# only allowed to be called from Milter.Base.eom(). Note: the %milter must
# negotiate this behavior with the MTA, i.e., it must check whether the
# protocol action SMFIP_SKIP is available and if so, the %milter must request
# it.
SKIP = 5
## Do not send a reply back to the MTA.
# The %milter must negotiate this behavior with the MTA, i.e., it must check
# whether the appropriate protocol action P_NR_* is available and if so,
# the %milter must request it. If you set the P_NR_* protocol action for a
# callback, that callback must always reply with NOREPLY. Using any other
# reply code is a violation of the API. If in some cases your callback may
# return another value (e.g., due to some resource shortages), then you must
# not set P_NR_* and you must use CONTINUE as the default return
# code. (Alternatively you can try to delay reporting the problem to a later
# callback for which P_NR_* is not set.)
#
# This is negotiated and returned automatically by the Milter.noreply
# function decorator.
NOREPLY = 6
## Hold context for a %milter connection.
# Each connection to sendmail creates a new <code>SMFICTX</code> struct within # Each connection to sendmail creates a new <code>SMFICTX</code> struct within
# libmilter. The milter module in turn creates a milterContext # libmilter. The milter module in turn creates a milterContext
# tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code> # tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code>
@@ -20,52 +75,52 @@
# and converts function callbacks to instance method invocations. # and converts function callbacks to instance method invocations.
# #
class milterContext(object): class milterContext(object):
## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_getsymval">smfi_getsymval</a>.
def getsymval(self,sym): pass def getsymval(self,sym): pass
## Calls <a href="milter_api/smfi_setreply.html"> ## Calls <a href="https://www.milter.org/developers/api/smfi_setreply">
# smfi_setreply</a> or # smfi_setreply</a> or
# <a href="milter_api/smfi_setmlreply.html"> # <a href="https://www.milter.org/developers/api/smfi_setmlreply">
# smfi_setmlreply</a>. # smfi_setmlreply</a>.
# @param rcode SMTP response code # @param rcode SMTP response code
# @param xcode extended SMTP response code # @param xcode extended SMTP response code
# @param msg one or more message lines. If the MTA does not support # @param msg one or more message lines. If the MTA does not support
# multiline messages, only the first is used. # multiline messages, only the first is used.
def setreply(self,rcode,xcode,*msg): pass def setreply(self,rcode,xcode,*msg): pass
## Calls <a href="milter_api/smfi_addheader.html">smfi_addheader</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_addheader">smfi_addheader</a>.
def addheader(self,name,value,idx=-1): pass def addheader(self,name,value,idx=-1): pass
## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">smfi_chgheader</a>.
def chgheader(self,name,idx,value): pass def chgheader(self,name,idx,value): pass
## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">smfi_addrcpt</a>.
def addrcpt(self,rcpt,params=None): pass def addrcpt(self,rcpt,params=None): pass
## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">smfi_delrcpt</a>.
def delrcpt(self,rcpt): pass def delrcpt(self,rcpt): pass
## Calls <a href="milter_api/smfi_replacebody.html">smfi_replacebody</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">smfi_replacebody</a>.
def replacebody(self,data): pass def replacebody(self,data): pass
## Attach a Python object to this connection context. ## Attach a Python object to this connection context.
# @return the old value or None # @return the old value or None
def setpriv(self,priv): pass def setpriv(self,priv): pass
## Return the Python object attached to this connection context. ## Return the Python object attached to this connection context.
def getpriv(self): pass def getpriv(self): pass
## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">smfi_quarantine</a>.
def quarantine(self,reason): pass def quarantine(self,reason): pass
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_progress">smfi_progress</a>.
def progress(self): pass def progress(self): pass
## Calls <a href="milter_api/smfi_chgfrom.html">smfi_chgfrom</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">smfi_chgfrom</a>.
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.
# It may only be called from the negotiate callback. # It may only be called from the negotiate callback.
# The protocol stages are # The protocol stages are
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH. # M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>. # 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 # @param stage protocol stage in which the macro list should be used
# @param macrolist a space separated list of macro names # @param macrolist a space separated list of macro names
def setsymlist(self,stage,macrolist): pass def setsymlist(self,stage,macrolist): pass
class error(Exception): pass class error(Exception): pass
## Enable optional milter actions. ## Enable optional %milter actions.
# Certain milter actions need to be enabled before calling main() # Certain %milter actions need to be enabled before calling main()
# or they throw an exception. Pymilter enables them all by # or they throw an exception. Pymilter enables them all by
# default (since 0.9.2), but you may wish to disable unneeded # default (since 0.9.2), but you may wish to disable unneeded
# actions as an optimization. # actions as an optimization.
@@ -83,29 +138,37 @@ 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 # 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.
# @param code one of #TEMPFAIL,#REJECT,#CONTINUE, or since 1.0, #ACCEPT
def set_exception_policy(code): pass def set_exception_policy(code): pass
## Register python milter with libmilter. ## Register python %milter with libmilter.
# The name we pass is used to identify the milter in the MTA configuration. # 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 # Callback functions must be set using the set_*_callback() functions before
# registering the milter. # registering the %milter.
# Three additional callbacks are specified as keyword parameters. These # Three additional callbacks are specified as keyword parameters. These
# were added by recent versions of libmilter. The keyword parameters is # 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 # 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 # it before registering. I may move all the callbacks in the future (perhaps
# in the future (perhaps keeping the set functions for compatibility). # keeping the set functions for compatibility). Note that Milter.Base
# @param name the milter name by which the MTA finds us # automatically maps all callbacks to member functions, and negotiates which
# member functions are actually overridden by an application class.
# @param name the %milter name by which the MTA finds us
# @param negotiate the # @param negotiate the
# <a href="milter_api/xxfi_negotiate.html"> # <a href="https://www.milter.org/developers/api/xxfi_negotiate">
# xxfi_negotiate</a> callback, called to negotiate supported # xxfi_negotiate</a> callback, called to negotiate supported
# actions, callbacks, and protocol steps. # actions, callbacks, and protocol steps.
# @param unknown the # @param unknown the
# <a href="milter_api/xxfi_unknown.html"> # <a href="https://www.milter.org/developers/api/xxfi_unknown">
# xxfi_unknown</a> callback, called when for SMTP commands # xxfi_unknown</a> callback, called when for SMTP commands
# not recognized by the MTA. (Extend SMTP in your milter!) # not recognized by the MTA. (Extend SMTP in your milter!)
# @param data the # @param data the
# <a href="milter_api/xxfi_data.html"> # <a href="https://www.milter.org/developers/api/xxfi_data">
# xxfi_data</a> callback, called when the DATA # xxfi_data</a> callback, called when the DATA
# SMTP command is received. # SMTP command is received.
def register(name,negotiate=None,unknown=None,data=None): pass def register(name,negotiate=None,unknown=None,data=None): pass
@@ -115,19 +178,19 @@ def register(name,negotiate=None,unknown=None,data=None): pass
# call to milter.setconn() which will be the interface between MTAs and the # call to milter.setconn() which will be the interface between MTAs and the
# %milter. This allows the calling application to ensure that the socket can be # %milter. This allows the calling application to ensure that the socket can be
# created. If this is not called, milter.main() will do so implicitly. # created. If this is not called, milter.main() will do so implicitly.
# Calls <a href="milter_api/smfi_opensocket.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_opensocket">
# smfi_opensocket</a>. While not documented for libmilter, my experiments # smfi_opensocket</a>. While not documented for libmilter, my experiments
# indicate that you must call register() before calling opensocket(). # indicate that you must call register() before calling opensocket().
# @param rmsock Try to remove an existing unix domain socket if true. # @param rmsock Try to remove an existing unix domain socket if true.
def opensocket(rmsock): pass def opensocket(rmsock): pass
## Transfer control to libmilter. ## Transfer control to libmilter.
# Calls <a href="milter_api/smfi_main.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_main">
# smfi_main</a>. # smfi_main</a>.
def main(): pass def main(): pass
## Set the libmilter debugging level. ## Set the libmilter debugging level.
# <a href="milter_api/smfi_setdbg.html">smfi_setdbg</a> # <a href="https://www.milter.org/developers/api/smfi_setdbg">smfi_setdbg</a>
# sets the %milter library's internal debugging level to a new level # sets the %milter library's internal debugging level to a new level
# so that code details may be traced. A level of zero turns off debugging. The # so that code details may be traced. A level of zero turns off debugging. The
# greater (more positive) the level the more detailed the debugging. Six is the # greater (more positive) the level the more detailed the debugging. Six is the
@@ -135,12 +198,12 @@ def main(): pass
def setdbg(lev): pass def setdbg(lev): pass
## Set timeout for MTA communication. ## Set timeout for MTA communication.
# Calls <a href="milter_api/smfi_settimeout.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_settimeout">
# smfi_settimeout</a>. Must be called before calling main(). # smfi_settimeout</a>. Must be called before calling main().
def settimeout(secs): pass def settimeout(secs): pass
## Set socket backlog. ## Set socket backlog.
# Calls <a href="milter_api/smfi_setbacklog.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_setbacklog">
# smfi_setbacklog</a>. Must be called before calling main(). # smfi_setbacklog</a>. Must be called before calling main().
def setbacklog(n): pass def setbacklog(n): pass
@@ -149,16 +212,21 @@ def setbacklog(n): pass
# unix, inet, or inet6 socket. By default, a unix domain socket # unix, inet, or inet6 socket. By default, a unix domain socket
# is used. It must not exist, # is used. It must not exist,
# and sendmail will throw warnings if, eg, the file is under a # and sendmail will throw warnings if, eg, the file is under a
# group or world writable directory. # group or world writable directory. milter.setconn() will not fail with
# an invalid socket - this will be detected only when calling milter.main()
# or milter.opensocket().
# @param s the socket address in proto:address format
# <pre> # <pre>
# setconn('unix:/var/run/pythonfilter') # milter.setconn('unix:/var/run/pythonfilter') # a named pipe
# setconn('inet:8800') # listen on ANY interface # milter.setconn('local:/var/run/pythonfilter') # a named pipe
# setconn('inet:7871@@publichost') # listen on a specific interface # milter.setconn('inet:8800') # listen on ANY interface
# setconn('inet6:8020') # milter.setconn('inet:7871@@publichost') # listen on a specific interface
# milter.setconn('inet6:8020')
# milter.setconn('inet6:8020@[2001:db8:1234::1]') # listen on specific IP
# </pre> # </pre>
def setconn(s): pass def setconn(s): pass
## Stop the milter gracefully. ## Stop the %milter gracefully.
def stop(): pass def stop(): pass
## Retrieve diagnostic info. ## Retrieve diagnostic info.
+3 -4
View File
@@ -1,11 +1,10 @@
web: web:
doxygen doxygen
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-devel-* doc/html/milter_api
rsync -ravKk doc/html/ spidey2.bmsi.com:/Public/pymilter
cd doc/html; zip -r ../../doc . cd doc/html; zip -r ../../doc .
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
VERSION=0.9.6 VERSION=1.0
CVSTAG=pymilter-0_9_6 CVSTAG=pymilter-1_0
PKG=pymilter-$(VERSION) PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz SRCTAR=$(PKG).tar.gz
+4 -2
View File
@@ -1,6 +1,6 @@
## To roll your own milter, create a class that extends Milter. ## To roll your own milter, create a class that extends Milter.
# See the pymilter project at http://bmsi.com/python/milter.html # See the pymilter project at http://bmsi.com/python/milter.html
# based on Sendmail's milter API # based on Sendmail's milter API http://www.milter.org/milter_api/api.html
# This code is open-source on the same terms as Python. # This code is open-source on the same terms as Python.
## Milter calls methods of your class at milter events. ## Milter calls methods of your class at milter events.
@@ -71,6 +71,9 @@ class myMilter(Milter.Base):
self.fromparms = Milter.dictfromlist(str) # ESMTP parms self.fromparms = Milter.dictfromlist(str) # ESMTP parms
self.user = self.getsymval('{auth_authen}') # authenticated user self.user = self.getsymval('{auth_authen}') # authenticated user
self.log("mail from:", mailfrom, *str) self.log("mail from:", mailfrom, *str)
# NOTE: self.fp is only an *internal* copy of message data. You
# must use addheader, chgheader, replacebody to change the message
# on the MTA.
self.fp = StringIO.StringIO() self.fp = StringIO.StringIO()
self.canon_from = '@'.join(parse_addr(mailfrom)) self.canon_from = '@'.join(parse_addr(mailfrom))
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime())) self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
@@ -104,7 +107,6 @@ class myMilter(Milter.Base):
def eom(self): def eom(self):
self.fp.seek(0) self.fp.seek(0)
msg = email.message_from_file(self.fp) msg = email.message_from_file(self.fp)
self.setreply('250','2.5.1','Grokked by pymilter')
# many milter functions can only be called from eom() # many milter functions can only be called from eom()
# example of adding a Bcc: # example of adding a Bcc:
self.addrcpt('<%s>' % 'spy@example.com') self.addrcpt('<%s>' % 'spy@example.com')
+95 -87
View File
@@ -35,6 +35,18 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* Revision 1.35 2013/03/14 22:11:25 customdesigned
* Release 0.9.8
*
* Revision 1.34 2013/03/09 05:42:14 customdesigned
* Make TestBase members private, fix getsymlist misspelling.
*
* 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.
*
* Revision 1.31 2012/04/12 23:32:50 customdesigned * Revision 1.31 2012/04/12 23:32:50 customdesigned
* Replace redundant callback array with macros. If this doesn't break anything, * Replace redundant callback array with macros. If this doesn't break anything,
* macros can be eliminated with code changes. * macros can be eliminated with code changes.
@@ -331,7 +343,7 @@ static struct MilterCallback {
{ NULL , NULL } { NULL , NULL }
}; };
static struct smfiDesc description; /* forward declaration */ staticforward struct smfiDesc description; /* forward declaration */
static PyObject *MilterError; static PyObject *MilterError;
/* The interpreter instance that called milter.main */ /* The interpreter instance that called milter.main */
@@ -343,7 +355,7 @@ typedef struct {
static milter_Diag diag; static milter_Diag diag;
static PyTypeObject milter_ContextType; staticforward PyTypeObject milter_ContextType;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
@@ -440,7 +452,7 @@ _thread_return(PyThreadState *t,int val,char *errstr) {
return _generic_return(val,errstr); return _generic_return(val,errstr);
} }
static char milter_set_flags__doc__[] = static const char milter_set_flags__doc__[] =
"set_flags(int) -> None\n\ "set_flags(int) -> None\n\
Set flags for filter capabilities; OR of one or more of:\n\ Set flags for filter capabilities; OR of one or more of:\n\
ADDHDRS - filter may add headers\n\ ADDHDRS - filter may add headers\n\
@@ -481,7 +493,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
return Py_None; return Py_None;
} }
static char milter_set_connect_callback__doc__[] = static const char milter_set_connect_callback__doc__[] =
"set_connect_callback(Function) -> None\n\ "set_connect_callback(Function) -> None\n\
Sets the Python function invoked when a connection is made to sendmail.\n\ Sets the Python function invoked when a connection is made to sendmail.\n\
Function takes args (ctx, hostname, integer, hostaddr) -> int\n\ Function takes args (ctx, hostname, integer, hostaddr) -> int\n\
@@ -508,7 +520,7 @@ milter_set_connect_callback(PyObject *self, PyObject *args) {
"O:set_connect_callback", &connect_callback); "O:set_connect_callback", &connect_callback);
} }
static char milter_set_helo_callback__doc__[] = static const char milter_set_helo_callback__doc__[] =
"set_helo_callback(Function) -> None\n\ "set_helo_callback(Function) -> None\n\
Sets the Python function invoked upon SMTP HELO.\n\ Sets the Python function invoked upon SMTP HELO.\n\
Function takes args (ctx, hostname) -> int\n\ Function takes args (ctx, hostname) -> int\n\
@@ -519,7 +531,7 @@ milter_set_helo_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_helo_callback", &helo_callback); return generic_set_callback(args, "O:set_helo_callback", &helo_callback);
} }
static char milter_set_envfrom_callback__doc__[] = static const char milter_set_envfrom_callback__doc__[] =
"set_envfrom_callback(Function) -> None\n\ "set_envfrom_callback(Function) -> None\n\
Sets the Python function invoked on envelope from.\n\ Sets the Python function invoked on envelope from.\n\
Function takes args (ctx, from, *str) -> int\n\ Function takes args (ctx, from, *str) -> int\n\
@@ -532,7 +544,7 @@ milter_set_envfrom_callback(PyObject *self, PyObject *args) {
&envfrom_callback); &envfrom_callback);
} }
static char milter_set_envrcpt_callback__doc__[] = static const char milter_set_envrcpt_callback__doc__[] =
"set_envrcpt_callback(Function) -> None\n\ "set_envrcpt_callback(Function) -> None\n\
Sets the Python function invoked on each envelope recipient.\n\ Sets the Python function invoked on each envelope recipient.\n\
Function takes args (ctx, rcpt, *str) -> int\n\ Function takes args (ctx, rcpt, *str) -> int\n\
@@ -545,7 +557,7 @@ milter_set_envrcpt_callback(PyObject *self, PyObject *args) {
&envrcpt_callback); &envrcpt_callback);
} }
static char milter_set_header_callback__doc__[] = static const char milter_set_header_callback__doc__[] =
"set_header_callback(Function) -> None\n\ "set_header_callback(Function) -> None\n\
Sets the Python function invoked on each message header.\n\ Sets the Python function invoked on each message header.\n\
Function takes args (ctx, field, value) ->int\n\ Function takes args (ctx, field, value) ->int\n\
@@ -558,7 +570,7 @@ milter_set_header_callback(PyObject *self, PyObject *args) {
&header_callback); &header_callback);
} }
static char milter_set_eoh_callback__doc__[] = static const char milter_set_eoh_callback__doc__[] =
"set_eoh_callback(Function) -> None\n\ "set_eoh_callback(Function) -> None\n\
Sets the Python function invoked at end of header.\n\ Sets the Python function invoked at end of header.\n\
Function takes args (ctx) -> int"; Function takes args (ctx) -> int";
@@ -568,7 +580,7 @@ milter_set_eoh_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback); return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback);
} }
static char milter_set_body_callback__doc__[] = static const char milter_set_body_callback__doc__[] =
"set_body_callback(Function) -> None\n\ "set_body_callback(Function) -> None\n\
Sets the Python function invoked for each body chunk. There may\n\ Sets the Python function invoked for each body chunk. There may\n\
be multiple body chunks passed to the filter. End-of-lines are\n\ be multiple body chunks passed to the filter. End-of-lines are\n\
@@ -581,7 +593,7 @@ milter_set_body_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_body_callback", &body_callback); return generic_set_callback(args, "O:set_body_callback", &body_callback);
} }
static char milter_set_eom_callback__doc__[] = static const char milter_set_eom_callback__doc__[] =
"set_eom_callback(Function) -> None\n\ "set_eom_callback(Function) -> None\n\
Sets the Python function invoked at end of message.\n\ Sets the Python function invoked at end of message.\n\
This routine is the only place where special operations\n\ This routine is the only place where special operations\n\
@@ -594,7 +606,7 @@ milter_set_eom_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_eom_callback", &eom_callback); return generic_set_callback(args, "O:set_eom_callback", &eom_callback);
} }
static char milter_set_abort_callback__doc__[] = static const char milter_set_abort_callback__doc__[] =
"set_abort_callback(Function) -> None\n\ "set_abort_callback(Function) -> None\n\
Sets the Python function invoked if message is aborted\n\ Sets the Python function invoked if message is aborted\n\
outside of the control of the filter, for example,\n\ outside of the control of the filter, for example,\n\
@@ -608,7 +620,7 @@ milter_set_abort_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_abort_callback", &abort_callback); return generic_set_callback(args, "O:set_abort_callback", &abort_callback);
} }
static char milter_set_close_callback__doc__[] = static const char milter_set_close_callback__doc__[] =
"set_close_callback(Function) -> None\n\ "set_close_callback(Function) -> None\n\
Sets the Python function invoked at end of the connection. This\n\ Sets the Python function invoked at end of the connection. This\n\
is called on close even if the previous mail transaction was aborted.\n\ is called on close even if the previous mail transaction was aborted.\n\
@@ -621,7 +633,7 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
static int exception_policy = SMFIS_TEMPFAIL; static int exception_policy = SMFIS_TEMPFAIL;
static char milter_set_exception_policy__doc__[] = static const char milter_set_exception_policy__doc__[] =
"set_exception_policy(i) -> None\n\ "set_exception_policy(i) -> None\n\
Sets the policy for untrapped Python exceptions during a callback.\n\ Sets the policy for untrapped Python exceptions during a callback.\n\
Must be one of TEMPFAIL,REJECT,CONTINUE"; Must be one of TEMPFAIL,REJECT,CONTINUE";
@@ -632,7 +644,8 @@ milter_set_exception_policy(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i)) if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i))
return NULL; return NULL;
switch (i) { switch (i) {
case SMFIS_REJECT: case SMFIS_TEMPFAIL: case SMFIS_CONTINUE: case SMFIS_REJECT: case SMFIS_TEMPFAIL:
case SMFIS_CONTINUE: case SMFIS_ACCEPT:
exception_policy = i; exception_policy = i;
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
@@ -647,23 +660,30 @@ _release_thread(PyThreadState *t) {
PyEval_ReleaseThread(t); PyEval_ReleaseThread(t);
} }
/** Report and clear any python exception before returning to libmilter. /** Report and clear any python exception before returning to libmilter.
The interpreter is locked when we are called, and we unlock it. */ The interpreter is locked when we are called, and we unlock it. */
static int _report_exception(milter_ContextObject *self) { static int _report_exception(milter_ContextObject *self) {
char untrapped_msg[80];
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
sprintf(untrapped_msg,"pymilter: untrapped exception in %.40s",
description.xxfi_name);
PyErr_Print(); PyErr_Print();
PyErr_Clear(); /* must clear since not returning to python */ PyErr_Clear(); /* must clear since not returning to python */
_release_thread(self->t); _release_thread(self->t);
switch (exception_policy) { switch (exception_policy) {
case SMFIS_REJECT: case SMFIS_REJECT:
smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure"); smfi_setreply(self->ctx, "554", "5.3.0", untrapped_msg);
return SMFIS_REJECT; return SMFIS_REJECT;
case SMFIS_TEMPFAIL: case SMFIS_TEMPFAIL:
smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure"); smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg);
return SMFIS_TEMPFAIL; return SMFIS_TEMPFAIL;
} }
return SMFIS_CONTINUE; return exception_policy;
} }
/* This should never happen, _report_exception is only called when
* the caller has already detected a python exception. If it
* does somehow happen, pretend nothing is wrong... */
_release_thread(self->t); _release_thread(self->t);
return SMFIS_CONTINUE; return SMFIS_CONTINUE;
} }
@@ -680,7 +700,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
result = PyEval_CallObject(cb, arglist); result = PyEval_CallObject(cb, arglist);
Py_DECREF(arglist); Py_DECREF(arglist);
if (result == NULL) return _report_exception(self); if (result == NULL) return _report_exception(self);
if (!PyLong_Check(result)) { if (!PyInt_Check(result)) {
const struct MilterCallback *p; const struct MilterCallback *p;
const char *cbname = "milter"; const char *cbname = "milter";
char buf[40]; char buf[40];
@@ -695,7 +715,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
PyErr_SetString(MilterError,buf); PyErr_SetString(MilterError,buf);
return _report_exception(self); return _report_exception(self);
} }
retval = PyLong_AS_LONG(result); retval = PyInt_AS_LONG(result);
Py_DECREF(result); Py_DECREF(result);
_release_thread(self->t); _release_thread(self->t);
return retval; return retval;
@@ -712,7 +732,7 @@ makeipaddr(struct sockaddr_in *addr) {
sprintf(buf, "%d.%d.%d.%d", sprintf(buf, "%d.%d.%d.%d",
(int) (x>>24) & 0xff, (int) (x>>16) & 0xff, (int) (x>>24) & 0xff, (int) (x>>16) & 0xff,
(int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff); (int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff);
return PyUnicode_FromString(buf); return PyString_FromString(buf);
} }
#ifdef HAVE_IPV6_SUPPORT #ifdef HAVE_IPV6_SUPPORT
@@ -720,8 +740,8 @@ static PyObject *
makeip6addr(struct sockaddr_in6 *addr) { makeip6addr(struct sockaddr_in6 *addr) {
char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */ char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */
const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf); const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf);
if (s) return PyUnicode_FromString(s); if (s) return PyString_FromString(s);
return PyUnicode_FromString("inet6:unknown"); return PyString_FromString("inet6:unknown");
} }
#endif #endif
@@ -812,7 +832,7 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
for (i=0;i<count;i++) { for (i=0;i<count;i++) {
/* There's some error checking performed in do_mkvalue() for a string */ /* There's some error checking performed in do_mkvalue() for a string */
/* that's not currently done here - it probably should be */ /* that's not currently done here - it probably should be */
PyObject *o = PyUnicode_FromStringAndSize(argv[i], strlen(argv[i])); PyObject *o = PyString_FromStringAndSize(argv[i], strlen(argv[i]));
if (o == NULL) { /* out of memory */ if (o == NULL) { /* out of memory */
Py_DECREF(arglist); Py_DECREF(arglist);
return _report_exception(self); return _report_exception(self);
@@ -943,7 +963,7 @@ milter_wrap_negotiate(SMFICTX *ctx,
int i; int i;
for (i = 0; i < 4; ++i) { for (i = 0; i < 4; ++i) {
*pa[i] = (i <= len) *pa[i] = (i <= len)
? PyLong_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i)) ? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
: fa[i]; : fa[i];
} }
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
@@ -991,7 +1011,7 @@ milter_wrap_close(SMFICTX *ctx) {
return r; return r;
} }
static char milter_register__doc__[] = static const char milter_register__doc__[] =
"register(name,unknown=,data=,negotiate=) -> None\n\ "register(name,unknown=,data=,negotiate=) -> None\n\
Registers the milter name with current callbacks, and flags.\n\ Registers the milter name with current callbacks, and flags.\n\
Required before main() is called."; Required before main() is called.";
@@ -1036,7 +1056,7 @@ milter_register(PyObject *self, PyObject *args, PyObject *kwds) {
return _generic_return(smfi_register(description), "cannot register"); return _generic_return(smfi_register(description), "cannot register");
} }
static char milter_opensocket__doc__[] = static const char milter_opensocket__doc__[] =
"opensocket(rmsock) -> None\n\ "opensocket(rmsock) -> None\n\
Attempts to create and open the socket provided with setconn.\n\ Attempts to create and open the socket provided with setconn.\n\
Removes the socket first if rmsock is True."; Removes the socket first if rmsock is True.";
@@ -1049,7 +1069,7 @@ milter_opensocket(PyObject *self, PyObject *args) {
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket"); return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
} }
static char milter_main__doc__[] = static const char milter_main__doc__[] =
"main() -> None\n\ "main() -> None\n\
Main milter routine. Set any callbacks, and flags desired, then call\n\ Main milter routine. Set any callbacks, and flags desired, then call\n\
setconn(), then call register(name), and finally call main()."; setconn(), then call register(name), and finally call main().";
@@ -1073,7 +1093,7 @@ milter_main(PyObject *self, PyObject *args) {
return o; return o;
} }
static char milter_setdbg__doc__[] = static const char milter_setdbg__doc__[] =
"setdbg(int) -> None\n\ "setdbg(int) -> None\n\
Sets debug level in sendmail/libmilter source. Dubious usefulness."; Sets debug level in sendmail/libmilter source. Dubious usefulness.";
@@ -1084,7 +1104,7 @@ milter_setdbg(PyObject *self, PyObject *args) {
return _generic_return(smfi_setdbg(val), "cannot set debug value"); return _generic_return(smfi_setdbg(val), "cannot set debug value");
} }
static char milter_setbacklog__doc__[] = static const char milter_setbacklog__doc__[] =
"setbacklog(int) -> None\n\ "setbacklog(int) -> None\n\
Set the TCP connection queue size for the milter socket."; Set the TCP connection queue size for the milter socket.";
@@ -1096,7 +1116,7 @@ milter_setbacklog(PyObject *self, PyObject *args) {
return _generic_return(smfi_setbacklog(val), "cannot set backlog"); return _generic_return(smfi_setbacklog(val), "cannot set backlog");
} }
static char milter_settimeout__doc__[] = static const char milter_settimeout__doc__[] =
"settimeout(int) -> None\n\ "settimeout(int) -> None\n\
Set the time (in seconds) that sendmail will wait before\n\ Set the time (in seconds) that sendmail will wait before\n\
considering this filter dead."; considering this filter dead.";
@@ -1109,7 +1129,7 @@ milter_settimeout(PyObject *self, PyObject *args) {
return _generic_return(smfi_settimeout(val), "cannot set timeout"); return _generic_return(smfi_settimeout(val), "cannot set timeout");
} }
static char milter_setconn__doc__[] = static const char milter_setconn__doc__[] =
"setconn(filename) -> None\n\ "setconn(filename) -> None\n\
Sets the pathname to the unix, inet, or inet6 socket that\n\ Sets the pathname to the unix, inet, or inet6 socket that\n\
sendmail will use to communicate with this filter. By default,\n\ sendmail will use to communicate with this filter. By default,\n\
@@ -1129,7 +1149,7 @@ milter_setconn(PyObject *self, PyObject *args) {
return _generic_return(smfi_setconn(str), "cannot set connection"); return _generic_return(smfi_setconn(str), "cannot set connection");
} }
static char milter_stop__doc__[] = static const char milter_stop__doc__[] =
"stop() -> None\n\ "stop() -> None\n\
This function appears to be a controlled method to tell sendmail to\n\ This function appears to be a controlled method to tell sendmail to\n\
stop using this filter. It will close the socket."; stop using this filter. It will close the socket.";
@@ -1142,7 +1162,7 @@ milter_stop(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_stop(), "cannot stop"); return _thread_return(t,smfi_stop(), "cannot stop");
} }
static char milter_getdiag__doc__[] = static const char milter_getdiag__doc__[] =
"getdiag() -> tuple\n\ "getdiag() -> tuple\n\
Return a tuple of diagnostic data. The first two items are context new\n\ Return a tuple of diagnostic data. The first two items are context new\n\
count and context del count. The rest are yet to be defined."; count and context del count. The rest are yet to be defined.";
@@ -1152,7 +1172,7 @@ milter_getdiag(PyObject *self, PyObject *args) {
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel); return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
} }
static char milter_getversion__doc__[] = static const char milter_getversion__doc__[] =
"getversion() -> tuple\n\ "getversion() -> tuple\n\
Return runtime libmilter version as a tuple of major,minor,patchlevel."; Return runtime libmilter version as a tuple of major,minor,patchlevel.";
static PyObject * static PyObject *
@@ -1166,7 +1186,7 @@ milter_getversion(PyObject *self, PyObject *args) {
return Py_BuildValue("(kkk)", major,minor,patch); return Py_BuildValue("(kkk)", major,minor,patch);
} }
static char milter_getsymval__doc__[] = static const char milter_getsymval__doc__[] =
"getsymval(String) -> String\n\ "getsymval(String) -> String\n\
Returns a symbol's value. Context-dependent, and unclear from the dox."; Returns a symbol's value. Context-dependent, and unclear from the dox.";
@@ -1181,7 +1201,7 @@ milter_getsymval(PyObject *self, PyObject *args) {
return Py_BuildValue("s", smfi_getsymval(ctx, str)); return Py_BuildValue("s", smfi_getsymval(ctx, str));
} }
static char milter_setreply__doc__[] = static const char milter_setreply__doc__[] =
"setreply(rcode, xcode, message) -> None\n\ "setreply(rcode, xcode, message) -> None\n\
Sets the specific reply code to be used in response\n\ Sets the specific reply code to be used in response\n\
to the active command.\n\ to the active command.\n\
@@ -1245,7 +1265,7 @@ milter_setreply(PyObject *self, PyObject *args) {
"cannot set reply"); "cannot set reply");
} }
static char milter_addheader__doc__[] = static const char milter_addheader__doc__[] =
"addheader(field, value, idx=-1) -> None\n\ "addheader(field, value, idx=-1) -> None\n\
Add a header to the message. This header is not passed to other\n\ Add a header to the message. This header is not passed to other\n\
filters. It is not checked for standards compliance;\n\ filters. It is not checked for standards compliance;\n\
@@ -1282,7 +1302,7 @@ milter_addheader(PyObject *self, PyObject *args) {
} }
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
static char milter_chgfrom__doc__[] = static const char milter_chgfrom__doc__[] =
"chgfrom(sender,params) -> None\n\ "chgfrom(sender,params) -> None\n\
Change the envelope sender (MAIL From) of the current message.\n\ Change the envelope sender (MAIL From) of the current message.\n\
A filter which calls smfi_chgfrom must have set the CHGFROM flag\n\ A filter which calls smfi_chgfrom must have set the CHGFROM flag\n\
@@ -1305,7 +1325,7 @@ milter_chgfrom(PyObject *self, PyObject *args) {
} }
#endif #endif
static char milter_chgheader__doc__[] = static const char milter_chgheader__doc__[] =
"chgheader(field, int, value) -> None\n\ "chgheader(field, int, value) -> None\n\
Change/delete a header in the message. \n\ Change/delete a header in the message. \n\
It is not checked for standards compliance; the mail filter\n\ It is not checked for standards compliance; the mail filter\n\
@@ -1333,7 +1353,7 @@ milter_chgheader(PyObject *self, PyObject *args) {
"cannot change header"); "cannot change header");
} }
static char milter_addrcpt__doc__[] = static const char milter_addrcpt__doc__[] =
"addrcpt(string,params=None) -> None\n\ "addrcpt(string,params=None) -> None\n\
Add a recipient to the envelope. It must be in the same format\n\ Add a recipient to the envelope. It must be in the same format\n\
as is passed to the envrcpt callback in the first tuple element.\n\ as is passed to the envrcpt callback in the first tuple element.\n\
@@ -1363,7 +1383,7 @@ milter_addrcpt(PyObject *self, PyObject *args) {
return _thread_return(t,rc, "cannot add recipient"); return _thread_return(t,rc, "cannot add recipient");
} }
static char milter_delrcpt__doc__[] = static const char milter_delrcpt__doc__[] =
"delrcpt(string) -> None\n\ "delrcpt(string) -> None\n\
Delete a recipient from the envelope.\n\ Delete a recipient from the envelope.\n\
This function can only be called from the EOM callback."; This function can only be called from the EOM callback.";
@@ -1381,7 +1401,7 @@ milter_delrcpt(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient"); return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
} }
static char milter_replacebody__doc__[] = static const char milter_replacebody__doc__[] =
"replacebody(string) -> None\n\ "replacebody(string) -> None\n\
Replace the body of the message. This routine may be called multiple\n\ Replace the body of the message. This routine may be called multiple\n\
times if the body is longer than convenient to send in one call. End of\n\ times if the body is longer than convenient to send in one call. End of\n\
@@ -1403,7 +1423,7 @@ milter_replacebody(PyObject *self, PyObject *args) {
(unsigned char *)bodyp, bodylen), "cannot replace message body"); (unsigned char *)bodyp, bodylen), "cannot replace message body");
} }
static char milter_setpriv__doc__[] = static const char milter_setpriv__doc__[] =
"setpriv(object) -> object\n\ "setpriv(object) -> object\n\
Associates any Python object with this context, and returns\n\ Associates any Python object with this context, and returns\n\
the old value or None. Use this to\n\ the old value or None. Use this to\n\
@@ -1429,7 +1449,7 @@ milter_setpriv(PyObject *self, PyObject *args) {
return old; return old;
} }
static char milter_getpriv__doc__[] = static const char milter_getpriv__doc__[] =
"getpriv() -> None\n\ "getpriv() -> None\n\
Returns the Python object associated with the current context (if any).\n\ Returns the Python object associated with the current context (if any).\n\
Use this in conjunction with setpriv to keep track of data in a thread-safe\n\ Use this in conjunction with setpriv to keep track of data in a thread-safe\n\
@@ -1447,7 +1467,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
} }
#ifdef SMFIF_QUARANTINE #ifdef SMFIF_QUARANTINE
static char milter_quarantine__doc__[] = static const char milter_quarantine__doc__[] =
"quarantine(string) -> None\n\ "quarantine(string) -> None\n\
Place the message in quarantine. A string with a description of the reason\n\ Place the message in quarantine. A string with a description of the reason\n\
is the only argument."; is the only argument.";
@@ -1468,7 +1488,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
#endif #endif
#ifdef SMFIR_PROGRESS #ifdef SMFIR_PROGRESS
static char milter_progress__doc__[] = static const char milter_progress__doc__[] =
"progress() -> None\n\ "progress() -> None\n\
Notify the MTA that we are working on a message so it will reset timeouts."; Notify the MTA that we are working on a message so it will reset timeouts.";
@@ -1485,23 +1505,23 @@ milter_progress(PyObject *self, PyObject *args) {
} }
#endif #endif
#ifdef SMFIF_SETSMLIST #ifdef SMFIF_SETSYMLIST
static 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
@@ -1525,12 +1545,17 @@ 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 }
}; };
static PyObject *
milter_Context_getattr(PyObject *self, char *name) {
return Py_FindMethod(context_methods, self, name);
}
static struct smfiDesc description = { /* Set some reasonable defaults */ static struct smfiDesc description = { /* Set some reasonable defaults */
"pythonfilter", "pythonfilter",
SMFI_VERSION, SMFI_VERSION,
@@ -1579,13 +1604,14 @@ static PyMethodDef milter_methods[] = {
}; };
static PyTypeObject milter_ContextType = { static PyTypeObject milter_ContextType = {
PyVarObject_HEAD_INIT(&PyType_Type,0) PyObject_HEAD_INIT(&PyType_Type)
0,
"milterContext", "milterContext",
sizeof(milter_ContextObject), sizeof(milter_ContextObject),
0, 0,
milter_Context_dealloc, /* tp_dealloc */ milter_Context_dealloc, /* tp_dealloc */
0, /* tp_print */ 0, /* tp_print */
0, /* tp_getattr */ milter_Context_getattr, /* tp_getattr */
0, /* tp_setattr */ 0, /* tp_setattr */
0, /* tp_compare */ 0, /* tp_compare */
0, /* tp_repr */ 0, /* tp_repr */
@@ -1599,43 +1625,26 @@ static PyTypeObject milter_ContextType = {
0, /* tp_setattro */ 0, /* tp_setattro */
0, /* tp_as_buffer */ 0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */ Py_TPFLAGS_DEFAULT, /* tp_flags */
NULL, /* Documentation string */
0, /* call function for all accessible objects */
0, /* delete references to contained objects */
0, /* rich comparisons */
0, /* weak reference enabler */
0, 0, /* Iterators */
context_methods, /* Attribute descriptor and subclassing stuff */
}; };
static char milter_documentation[] = static const char milter_documentation[] =
"This module interfaces with Sendmail's libmilter functionality,\n\ "This module interfaces with Sendmail's libmilter functionality,\n\
allowing one to write email filters directly in Python.\n\ allowing one to write email filters directly in Python.\n\
Libmilter is currently marked FFR, and needs to be explicitly installed.\n\ Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
See <sendmailsource>/libmilter/README for details on setting it up.\n"; See <sendmailsource>/libmilter/README for details on setting it up.\n";
static void setitem(PyObject *d,const char *name,long val) { static void setitem(PyObject *d,const char *name,long val) {
PyObject *v = PyLong_FromLong(val); PyObject *v = PyInt_FromLong(val);
PyDict_SetItemString(d,name,v); PyDict_SetItemString(d,name,v);
Py_DECREF(v); Py_DECREF(v);
} }
static struct PyModuleDef moduledef = { void
PyModuleDef_HEAD_INIT, initmilter(void) {
"milter", /* m_name */
milter_documentation,/* m_doc */
-1, /* m_size */
milter_methods, /* m_methods */
NULL, /* m_reload */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL, /* m_free */
};
PyMODINIT_FUNC PyInit_milter(void) {
PyObject *m, *d; PyObject *m, *d;
m = PyModule_Create(&moduledef); m = Py_InitModule4("milter", milter_methods, milter_documentation,
(PyObject*)NULL, PYTHON_API_VERSION);
d = PyModule_GetDict(m); d = PyModule_GetDict(m);
MilterError = PyErr_NewException("milter.error", NULL, NULL); MilterError = PyErr_NewException("milter.error", NULL, NULL);
PyDict_SetItemString(d,"error", MilterError); PyDict_SetItemString(d,"error", MilterError);
@@ -1660,15 +1669,15 @@ PyMODINIT_FUNC PyInit_milter(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 */
setitem(d,"M_ENVRCPT",SMFIM_ENVRCPT);/* RCPT To */ setitem(d,"M_ENVRCPT",SMFIM_ENVRCPT);/* RCPT To */
setitem(d,"M_DATA",SMFIM_DATA); /* DATA */ setitem(d,"M_DATA",SMFIM_DATA); /* DATA */
setitem(d,"M_EOM",SMFIM_EOM); /* end of message (final dot) */ setitem(d,"M_EOM",SMFIM_EOM); /* end of message (final dot) */
setitem(d,"M_EOH",SMFIM_EOH); /* end of header */ setitem(d,"M_EOH",SMFIM_EOH); /* end of header */
#endif #endif
#ifdef SMFIS_ALL_OPTS #ifdef SMFIS_ALL_OPTS
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ); setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
@@ -1701,5 +1710,4 @@ PyMODINIT_FUNC PyInit_milter(void) {
setitem(d,"DISCARD", SMFIS_DISCARD); setitem(d,"DISCARD", SMFIS_DISCARD);
setitem(d,"ACCEPT", SMFIS_ACCEPT); setitem(d,"ACCEPT", SMFIS_ACCEPT);
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL); setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
return m;
} }
+51 -48
View File
@@ -1,4 +1,7 @@
# $Log$ # $Log$
# Revision 1.8 2011/11/05 15:51:03 customdesigned
# New example
#
# Revision 1.7 2009/06/13 21:15:12 customdesigned # Revision 1.7 2009/06/13 21:15:12 customdesigned
# Doxygen updates. # Doxygen updates.
# #
@@ -127,24 +130,24 @@ class MimeGenerator(Generator):
# full MIME type, then dispatch to self._handle_<maintype>(). If # full MIME type, then dispatch to self._handle_<maintype>(). If
# that's missing too, then dispatch to self._writeBody(). # that's missing too, then dispatch to self._writeBody().
main = msg.get_content_maintype() main = msg.get_content_maintype()
if msg.is_multipart() and main.lower() != 'multipart': if msg.is_multipart() and main.lower() != 'multipart':
self._handle_multipart(msg) self._handle_multipart(msg)
else: else:
Generator._dispatch(self,msg) Generator._dispatch(self,msg)
def unquote(s): def unquote(s):
"""Remove quotes from a string.""" """Remove quotes from a string."""
if len(s) > 1: if len(s) > 1:
if s.startswith('"'): if s.startswith('"'):
if s.endswith('"'): if s.endswith('"'):
s = s[1:-1] s = s[1:-1]
else: # remove garbage after trailing quote else: # remove garbage after trailing quote
try: s = s[1:s[1:].index('"')+1] try: s = s[1:s[1:].index('"')+1]
except: except:
return s return s
return s.replace('\\\\', '\\').replace('\\"', '"') return s.replace('\\\\', '\\').replace('\\"', '"')
if s.startswith('<') and s.endswith('>'): if s.startswith('<') and s.endswith('>'):
return s[1:-1] return s[1:-1]
return s return s
from types import TupleType from types import TupleType
@@ -202,21 +205,21 @@ class MimeMessage(Message):
for attr,val in self._get_params_preserve([],'content-type'): for attr,val in self._get_params_preserve([],'content-type'):
if isinstance(val, TupleType): if isinstance(val, TupleType):
# It's an RFC 2231 encoded parameter # It's an RFC 2231 encoded parameter
newvalue = _unquotevalue(val) newvalue = _unquotevalue(val)
if val[0]: if val[0]:
val = unicode(newvalue[2], newvalue[0]) val = unicode(newvalue[2], newvalue[0])
else: else:
val = unicode(newvalue[2]) val = unicode(newvalue[2])
else: else:
val = _unquotevalue(val.strip()) val = _unquotevalue(val.strip())
names.append((attr,val)) names.append((attr,val))
names += [("filename",self.get_filename())] names += [("filename",self.get_filename())]
if scan_zip: if scan_zip:
for key,name in tuple(names): # copy by converting to tuple for key,name in tuple(names): # copy by converting to tuple
if name and name.lower().endswith('.zip'): if name and name.lower().endswith('.zip'):
txt = self.get_payload(decode=True) txt = self.get_payload(decode=True)
if txt.strip(): if txt.strip():
names += zipnames(txt) names += zipnames(txt)
return names return names
def ismodified(self): def ismodified(self):
@@ -287,13 +290,13 @@ class MimeMessage(Message):
if t == 'message/rfc822' or t.startswith('multipart/'): if t == 'message/rfc822' or t.startswith('multipart/'):
if not self.submsg: if not self.submsg:
txt = self.get_payload() txt = self.get_payload()
if type(txt) == str: if type(txt) == str:
txt = self.get_payload(decode=True) txt = self.get_payload(decode=True)
self.submsg = email.message_from_string(txt,MimeMessage) self.submsg = email.message_from_string(txt,MimeMessage)
for part in self.submsg.walk(): for part in self.submsg.walk():
part.modified = False part.modified = False
else: else:
self.submsg = txt[0] self.submsg = txt[0]
return self.submsg return self.submsg
return None return None
@@ -333,7 +336,7 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
if badname: if badname:
if key == 'zipname': if key == 'zipname':
badname = msg.get_filename() badname = msg.get_filename()
break break
else: else:
return Milter.CONTINUE return Milter.CONTINUE
except zipfile.BadZipfile: except zipfile.BadZipfile:
@@ -380,7 +383,7 @@ class _defang:
return rc return rc
def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True, def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True,
scan_zip=False): scan_zip=False):
"""Compatible entry point. """Compatible entry point.
Replace all attachments with dangerous names.""" Replace all attachments with dangerous names."""
self._savname = savname self._savname = savname
@@ -450,25 +453,25 @@ class SGMLFilter(sgmllib.SGMLParser):
n = len(rawdata) n = len(rawdata)
j = i + 2 j = i + 2
while j < n: while j < n:
c = rawdata[j] c = rawdata[j]
if c == ">": if c == ">":
# end of declaration syntax # end of declaration syntax
self.handle_special(rawdata[i+2:j]) self.handle_special(rawdata[i+2:j])
return j + 1 return j + 1
if c in "\"'": if c in "\"'":
m = declstringlit.match(rawdata, j) m = declstringlit.match(rawdata, j)
if not m: if not m:
# incomplete or an error? # incomplete or an error?
return -1 return -1
j = m.end() j = m.end()
elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
m = declname.match(rawdata, j) m = declname.match(rawdata, j)
if not m: if not m:
# incomplete or an error? # incomplete or an error?
return -1 return -1
j = m.end() j = m.end()
else: else:
j += 1 j += 1
# end of buffer between tokens # end of buffer between tokens
return -1 return -1
@@ -497,7 +500,7 @@ def check_html(msg,savname=None):
if msgtype == 'application/octet-stream': if msgtype == 'application/octet-stream':
for (attr,name) in msg.getnames(): for (attr,name) in msg.getnames():
if name and name.lower().endswith(".htm"): if name and name.lower().endswith(".htm"):
msgtype = 'text/html' msgtype = 'text/html'
if msgtype == 'text/html': if msgtype == 'text/html':
out = StringIO.StringIO() out = StringIO.StringIO()
htmlfilter = HTMLScriptFilter(out) htmlfilter = HTMLScriptFilter(out)
+51 -27
View File
@@ -1,14 +1,15 @@
%define __python python2.6 %define __python python2.6
%define pythonbase python26 %define pythonbase python
%define libdir %{_libdir}/pymilter %define libdir %{_libdir}/pymilter
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} %{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
Summary: Python interface to sendmail milter API Summary: Python interface to sendmail milter API
Name: %{pythonbase}-pymilter Name: %{pythonbase}-pymilter
Version: 0.9.6 Version: 1.0
Release: 1%{dist} Release: 1%{dist}
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
Source1: pymilter.te
License: GPLv2+ License: GPLv2+
Group: Development/Libraries Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
@@ -26,11 +27,23 @@ attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV. DSNs, and doing CBV.
%package selinux
Summary: SELinux policy module for pymilter
Group: System Environment/Base
Requires: policycoreutils, selinux-policy, %{name}
BuildRequires: policycoreutils, checkpolicy
%description selinux
SELinux policy module for using pymilter with sendmail with selinux enforcing
%prep %prep
%setup -q -n pymilter-%{version} %setup -q -n pymilter-%{version}
cp %{SOURCE1} pymilter.te
%build %build
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
checkmodule -m -M -o pymilter.mod pymilter.te
semodule_package -o pymilter.pp -m pymilter.mod
%install %install
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
@@ -38,31 +51,11 @@ rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
mkdir -p $RPM_BUILD_ROOT%{libdir} mkdir -p $RPM_BUILD_ROOT%{libdir}
cp start.sh $RPM_BUILD_ROOT%{libdir}
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^datadir=/
c
datadir="%{_localstatedir}/log/milter"
.
/^piddir=/
c
piddir="%{_localstatedir}/run/milter"
.
/^libdir=/
c
libdir="%{libdir}"
.
/^python=/
c
python="%{__python}"
.
w
q
EOF
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
# start.sh is used by spfmilter, srsmilter, and milter, and could be used by # install selinux modules
# other milters using pymilter. mkdir -p %{buildroot}%{_datadir}/selinux/targeted
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%files %files
%defattr(-,root,root,-) %defattr(-,root,root,-)
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py %doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
@@ -71,13 +64,44 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter %dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter %dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%files selinux
%doc pymilter.te
%{_datadir}/selinux/targeted/*
%clean %clean
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%post selinux
/usr/sbin/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
&>/dev/null || :
%postun selinux
if [ $1 -eq 0 ] ; then
/usr/sbin/semodule -s targeted -r pymilter &> /dev/null || :
fi
%changelog %changelog
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1 * Sat Mar 1 2014 Stuart Gathman <stuart@gathman.org> 1.0-2
- Remove start.sh to track EPEL repository, suggest daemonize as replacement
- Selinux subpackage should not care about pymilter version
* Wed Jun 26 2013 Stuart Gathman <stuart@gathman.org> 1.0-1
- Allow ACCEPT as untrapped exception policy
- Optional dir for getaddrset and getaddrdict in Milter.config
- Show registered milter name in untrapped exception message.
- Include selinux subpackage
- Provide Milter.greylist export and Milter.greylist import to migrate data
* 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"
* Thu Apr 12 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
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1 * Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
- Raise ValueError on unescaped '%' passed to setreply - Raise ValueError on unescaped '%' passed to setreply
+13
View File
@@ -0,0 +1,13 @@
module pymilter 1.0;
require {
type sendmail_t;
type var_run_t;
type initrc_t;
class sock_file { write getattr };
class unix_stream_socket connectto;
}
#============= sendmail_t ==============
allow sendmail_t initrc_t:unix_stream_socket connectto;
allow sendmail_t var_run_t:sock_file { write getattr };
+12 -12
View File
@@ -1,4 +1,4 @@
from __future__ import print_function
# A simple milter. # A simple milter.
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
@@ -21,9 +21,9 @@ class sampleMilter(Milter.Milter):
"Milter to replace attachments poisonous to Windows with a WARNING message." "Milter to replace attachments poisonous to Windows with a WARNING message."
def log(self,*msg): def log(self,*msg):
print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id), end=' ') print "%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),
for i in msg: print(i, end=' ') for i in msg: print i,
print() print
def __init__(self): def __init__(self):
self.tempname = None self.tempname = None
@@ -61,20 +61,20 @@ class sampleMilter(Milter.Milter):
# (delete if you read chinese mail) # (delete if you read chinese mail)
if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'): if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'):
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
# check for common spam keywords # check for common spam keywords
if val.find("$$$") >= 0 or val.find("XXX") >= 0 \ if val.find("$$$") >= 0 or val.find("XXX") >= 0 \
or val.find("!!!") >= 0 or val.find("FREE") >= 0: or val.find("!!!") >= 0 or val.find("FREE") >= 0:
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
# check for spam that pretends to be legal # check for spam that pretends to be legal
lval = val.lower() lval = val.lower()
if lval.startswith("adv:") or lval.startswith("adv.") \ if lval.startswith("adv:") or lval.startswith("adv.") \
or lval.find('viagra') >= 0: or lval.find('viagra') >= 0:
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
return Milter.REJECT return Milter.REJECT
@@ -86,7 +86,7 @@ class sampleMilter(Milter.Milter):
# check for common bulk mailers # check for common bulk mailers
if lname == 'x-mailer' and \ if lname == 'x-mailer' and \
val.lower() in ('direct email','calypso','mail bomber'): val.lower() in ('direct email','calypso','mail bomber'):
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
@@ -171,13 +171,13 @@ if __name__ == "__main__":
socketname = os.getenv("HOME") + "/pythonsock" socketname = os.getenv("HOME") + "/pythonsock"
Milter.factory = sampleMilter Milter.factory = sampleMilter
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS) Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS)
print("""To use this with sendmail, add the following to sendmail.cf: print """To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:%s Xpythonfilter, S=local:%s
See the sendmail README for libmilter. See the sendmail README for libmilter.
sample milter startup""" % socketname) sample milter startup""" % socketname
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,240) Milter.runmilter("pythonfilter",socketname,240)
print("sample milter shutdown") print "sample milter shutdown"
+1 -1
View File
@@ -1,5 +1,5 @@
[bdist_rpm] [bdist_rpm]
python=python3 python=python2.6
doc_files=README NEWS TODO COPYING CREDITS doc_files=README NEWS TODO COPYING CREDITS
packager=Stuart D. Gathman <stuart@gathman.org> packager=Stuart D. Gathman <stuart@gathman.org>
release=1 release=1
+1 -1
View File
@@ -13,7 +13,7 @@ libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian libdirs = ["/usr/lib/libmilter"] # needed for Debian
# NOTE: importing Milter to obtain version fails when milter.so not built # NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "pymilter", version = '0.9.7', setup(name = "pymilter", version = '1.0',
description="Python interface to sendmail milter API", description="Python interface to sendmail milter API",
long_description="""\ long_description="""\
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
+2
View File
@@ -2,6 +2,7 @@ import unittest
import testmime import testmime
import testsample import testsample
import testutils import testutils
import testgrey
import os import os
def suite(): def suite():
@@ -9,6 +10,7 @@ def suite():
s.addTest(testmime.suite()) s.addTest(testmime.suite())
s.addTest(testsample.suite()) s.addTest(testsample.suite())
s.addTest(testutils.suite()) s.addTest(testutils.suite())
s.addTest(testgrey.suite())
return s return s
if __name__ == '__main__': if __name__ == '__main__':
+55
View File
@@ -0,0 +1,55 @@
import unittest
import doctest
import os
#from Milter.greylist import Greylist
from Milter.greysql import Greylist
class GreylistTestCase(unittest.TestCase):
def setUp(self):
self.fname = 'test.db'
os.remove(self.fname)
def tearDown(self):
#os.remove(self.fname)
pass
def testGrey(self):
grey = Greylist(self.fname)
# first time
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com')
self.assertEqual(rc,0)
# not in window yet
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*60)
self.assertEqual(rc,0)
# within window
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=15*60)
self.assertEqual(rc,1)
# new triple
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=15*60)
self.assertEqual(rc,0)
# seen again
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*3600)
self.assertEqual(rc,2)
# new one past expire
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=6*3600)
self.assertEqual(rc,0)
# original past retain
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=37*24*3600)
self.assertEqual(rc,0)
# new one for testing expire
rc = grey.check('1.2.3.5','flub@bar.com','baz@spat.com',timeinc=20*24*3600)
self.assertEqual(rc,0)
grey.close()
# test cleanup
grey = Greylist(self.fname)
rc = grey.clean(timeinc=37*24*3600)
self.assertEqual(rc,1)
grey.close()
def suite():
s = unittest.makeSuite(GreylistTestCase,'test')
return s
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())
+4 -4
View File
@@ -77,7 +77,7 @@ class MimeTestCase(unittest.TestCase):
pass pass
def testDefang(self,vname='virus1',part=1, def testDefang(self,vname='virus1',part=1,
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'): fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
msg = mime.message_from_file(open('test/'+vname,"r")) msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.failUnless(msg.ismodified(),"virus not removed") self.failUnless(msg.ismodified(),"virus not removed")
@@ -111,7 +111,7 @@ class MimeTestCase(unittest.TestCase):
self.failIf(msg.ismultipart()) self.failIf(msg.ismultipart())
txt2 = msg.get_payload() txt2 = msg.get_payload()
self.failUnless(txt2 == mime.virus_msg % \ self.failUnless(txt2 == mime.virus_msg % \
(fname,hostname,None),txt2) (fname,hostname,None),txt2)
# honey virus has a sneaky ASP payload which is parsed correctly # honey virus has a sneaky ASP payload which is parsed correctly
# by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1 # by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1
@@ -125,7 +125,7 @@ class MimeTestCase(unittest.TestCase):
txt2 = parts[1].get_payload() txt2 = parts[1].get_payload()
txt3 = parts[2].get_payload() txt3 = parts[2].get_payload()
self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \ self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \
(fname,hostname,None),txt2) (fname,hostname,None),txt2)
if txt3 != '': if txt3 != '':
self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \ self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \
('story[1].asp',hostname,None),txt3) ('story[1].asp',hostname,None),txt3)
@@ -201,4 +201,4 @@ if __name__ == '__main__':
fp = open(fname,'r') fp = open(fname,'r')
msg = mime.message_from_file(fp) msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
print(msg.as_string()) print msg.as_string()
+10 -95
View File
@@ -4,97 +4,12 @@ import sample
import mime import mime
import rfc822 import rfc822
import StringIO import StringIO
from Milter.test import TestBase
class TestMilter(sample.sampleMilter): class TestMilter(TestBase,sample.sampleMilter):
_protocol = 0
def __init__(self): def __init__(self):
self.logfp = open("test/milter.log","a") TestBase.__init__(self)
sample.sampleMilter.__init__(self)
def log(self,*msg):
for i in msg: print >>self.logfp, i,
print >>self.logfp
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self.bodyreplaced = True
else:
raise IOError,"replacebody not called from eom()"
# FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a milter
def chgheader(self,field,idx,value):
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '':
del self._msg[field]
else:
self._msg[field] = value
self.headerschanged = True
def addheader(self,field,value):
self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value
self.headerschanged = True
def feedMsg(self,fname):
self._body = None
self.bodyreplaced = False
self.headerschanged = 0
fp = open('test/'+fname,'r')
msg = rfc822.Message(fp)
rc = self.envfrom('<spam@advertisements.com>')
if rc != Milter.CONTINUE: return rc
rc = self.envrcpt('<victim@lamb.com>')
if rc != Milter.CONTINUE: return rc
line = None
for h in msg.headers:
if h[:1].isspace():
line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
rc = self.header(s[0],s[1].strip())
if rc != Milter.CONTINUE: return rc
line = h
if line:
s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
while 1:
buf = fp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = StringIO.StringIO()
rc = self.eom()
if self.bodyreplaced:
body = self._body.getvalue()
else:
msg.rewindbody()
body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body)
return rc
def connect(self,host='localhost'):
self._body = None
self.bodyreplaced = False
rc = sample.sampleMilter.connect(self,host,1,0)
if rc != Milter.CONTINUE and rc != Milter.ACCEPT:
self.close()
return rc
rc = self.hello('spamrelay')
if rc != Milter.CONTINUE:
self.close()
return rc
class BMSMilterTestCase(unittest.TestCase): class BMSMilterTestCase(unittest.TestCase):
@@ -104,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())
@@ -119,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()
@@ -129,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()
+1 -7
View File
@@ -11,8 +11,7 @@ class AddrCacheTestCase(unittest.TestCase):
self.fname = 'test.dat' self.fname = 'test.dat'
def tearDown(self): def tearDown(self):
if os.path.exists(self.fname): os.remove(self.fname)
os.remove(self.fname)
def testAdd(self): def testAdd(self):
cache = AddrCache(fname=self.fname) cache = AddrCache(fname=self.fname)
@@ -39,11 +38,6 @@ class AddrCacheTestCase(unittest.TestCase):
cache.load(self.fname,30) cache.load(self.fname,30)
self.failUnless('spammer.com' in cache) self.failUnless('spammer.com' in cache)
def testParseHeader(self):
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
h = Milter.utils.parse_header(s)
self.assertEqual(h,'Last Few Coldplay Album Artworks Available\x00')
def suite(): def suite():
s = unittest.makeSuite(AddrCacheTestCase,'test') s = unittest.makeSuite(AddrCacheTestCase,'test')
s.addTest(doctest.DocTestSuite(Milter.utils)) s.addTest(doctest.DocTestSuite(Milter.utils))