Compare commits

..

1 Commits

Author SHA1 Message Date
cvs2svn d71095dbac This commit was manufactured by cvs2svn to create tag 'pymilter-0_9_4'.
Sprout from master 2011-03-05 03:12:02 UTC Stuart Gathman <stuart@gathman.org> 'Release 1.0'
Cherrypick from bmsi 2005-05-31 18:23:49 UTC Stuart Gathman <stuart@gathman.org> 'Development changes since 0.7.2':
    sample.py
    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
2011-03-05 03:12:03 +00:00
23 changed files with 206 additions and 19372 deletions
+2 -2
View File
@@ -31,7 +31,7 @@ PROJECT_NAME = pymilter
# This could be handy for archiving the generated documentation or # This could be handy for archiving the generated documentation or
# if some version control system is used. # if some version control system is used.
PROJECT_NUMBER = 1.0 PROJECT_NUMBER = 0.9.3
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
# base path where the generated documentation will be put. # base path where the generated documentation will be put.
@@ -814,7 +814,7 @@ DOCSET_FEEDNAME = "Doxygen generated docs"
# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen # reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen
# will append .docset to the name. # will append .docset to the name.
DOCSET_BUNDLE_ID = com.bmsi.pymilter DOCSET_BUNDLE_ID = org.doxygen.Project
# If the GENERATE_HTMLHELP tag is set to YES, additional index files # If the GENERATE_HTMLHELP tag is set to YES, additional index files
# will be generated that can be used as input for tools like the # will be generated that can be used as input for tools like the
+47 -224
View File
@@ -8,26 +8,19 @@
# 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.3'
import os import os
import milter import milter
import thread import thread
from milter import * from milter import *
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 sequence number unique to this process.
""" """
global _seq global _seq
_seq_lock.acquire() _seq_lock.acquire()
@@ -35,7 +28,6 @@ def uniqueID():
_seq_lock.release() _seq_lock.release()
return seqno return seqno
## @private
OPTIONAL_CALLBACKS = { OPTIONAL_CALLBACKS = {
'connect':(P_NR_CONN,P_NOCONNECT), 'connect':(P_NR_CONN,P_NOCONNECT),
'hello':(P_NR_HELO,P_NOHELO), 'hello':(P_NR_HELO,P_NOHELO),
@@ -48,7 +40,6 @@ OPTIONAL_CALLBACKS = {
'header':(P_NR_HDR,P_NOHDRS) 'header':(P_NR_HDR,P_NOHDRS)
} }
## @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]
nms = [s for s,m in t if bits & m] nms = [s for s,m in t if bits & m]
@@ -58,17 +49,16 @@ def decode_mask(bits,names):
## Class decorator to enable optional protocol steps. ## Class decorator to enable optional protocol steps.
# P_SKIP is enabled by default when supported, but # P_SKIP is enabled by default when supported, but
# applications may wish to enable P_HDR_LEADSPC # milter applications may wish to enable P_HDR_LEADSPC
# to send and receive the leading space of header continuation # to send and receive the leading space of header continuation
# lines unchanged, and/or P_RCPT_REJ to have recipients # lines unchanged, and/or P_RCPT_REJ to have recipients
# detected as invalid by the MTA passed to the envcrpt callback. # detected as invalid by the MTA passed to the envcrpt callback.
# #
# Applications may want to check whether the protocol is actually # Applications may want to check whether the protocol is actually
# supported by the MTA in use. Base._protocol # supported by the MTA in use. The <code>_protocol</code>
# is a bitmask of protocol options negotiated. So, # member is a bitmask of protocol options negotiated. So,
# for instance, if <code>self._protocol & Milter.P_RCPT_REJ</code> # for instance, if <code>self._protocol & Milter.P_RCPT_REJ</code>
# is true, then that feature was successfully negotiated with the MTA # is true, then that feature was successfully negotiated with the MTA.
# and the application will see recipients the MTA has flagged as invalid.
# #
# Sample use: # Sample use:
# <pre> # <pre>
@@ -78,59 +68,21 @@ def decode_mask(bits,names):
# myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ) # myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ)
# </pre> # </pre>
# @since 0.9.3 # @since 0.9.3
# @param klass the %milter application class to modify # @param klass the milter application class to modify
# @param mask a bitmask of protocol steps to enable # @param mask a bitmask of protocol steps to enable
# @return the modified %milter class # @return the modified milter class
def enable_protocols(klass,mask): def enable_protocols(klass,mask):
klass._protocol_mask = klass.protocol_mask() & ~mask klass._protocol_mask = klass.protocol_mask() & ~mask
return klass return klass
## Milter rejected recipients. A class decorator that calls
# enable_protocols() with the P_RCPT_REJ flag. By default, the MTA
# does not pass recipients that it knows are invalid on to the milter.
# This decorator enables a %milter app to see all recipients if supported
# by the MTA. Use like this with python-2.6 and later:
# <pre>
# @@Milter.rejected_recipients
# class myMilter(Milter.Base):
# def envrcpt(self,to,*params):
# return Milter.CONTINUE
# </pre>
# @since 0.9.5
# @param klass the %milter application class to modify
# @return the modified %milter class
def rejected_recipients(klass):
return enable_protocols(klass,P_RCPT_REJ)
## Milter leading space on headers. A class decorator that calls
# enable_protocols() with the P_HDR_LEADSPC flag. By default,
# header continuation lines are collected and joined before getting
# sent to a milter. Headers modified or added by the milter are
# folded by the MTA as necessary according to its own standards.
# With this flag, header continuation lines are preserved
# with their newlines and leading space. In addition, header folding
# done by the milter is preserved as well.
# Use like this with python-2.6 and later:
# <pre>
# @@Milter.header_leading_space
# class myMilter(Milter.Base):
# def header(self,hname,value):
# return Milter.CONTINUE
# </pre>
# @since 0.9.5
# @param klass the %milter application class to modify
# @return the modified %milter class
def header_leading_space(klass):
return enable_protocols(klass,P_HDR_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 call this callback,
# increasing efficiency. All the callbacks (except negotiate) # increasing efficiency. All the callbacks (except negotiate)
# are disabled in Milter.Base, and overriding them reenables the # are disabled in Milter.Base, and overriding them reenables the
# callback. An application may need to use @@nocallback when it extends # callback. An application may need to use @@nocallback when it extends
# another %milter and wants to disable a callback again. # another milter and wants to disable a callback again.
# The disabled method should still return Milter.CONTINUE, in case the MTA does # The disabled method should still return Milter.CONTINUE, in case the MTA does
# not support protocol negotiation, and for when called from a test harness. # not support protocol negotiation.
# @since 0.9.2 # @since 0.9.2
def nocallback(func): def nocallback(func):
try: try:
@@ -138,12 +90,7 @@ def nocallback(func):
except KeyError: except KeyError:
raise ValueError( raise ValueError(
'@nocallback applied to non-optional method: '+func.__name__) '@nocallback applied to non-optional method: '+func.__name__)
def wrapper(self,*args): return func
if func(self,*args) != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
% func.__name__)
return CONTINUE
return wrapper
## Function decorator to disable callback reply. ## Function decorator to disable callback reply.
# If the MTA supports it, tells the MTA not to wait for a reply from # If the MTA supports it, tells the MTA not to wait for a reply from
@@ -158,14 +105,9 @@ def noreply(func):
except KeyError: except KeyError:
raise ValueError( raise ValueError(
'@noreply applied to non-optional method: '+func.__name__) '@noreply applied to non-optional method: '+func.__name__)
@wraps(func)
def wrapper(self,*args): def wrapper(self,*args):
rc = func(self,*args) rc = func(self,*args)
if self._protocol & nr_mask: if self._protocol & nr_mask: return NOREPLY
if rc != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @noreply'
% func.__name__)
return NOREPLY
return rc return rc
wrapper.milter_protocol = nr_mask wrapper.milter_protocol = nr_mask
return wrapper return wrapper
@@ -180,87 +122,49 @@ def noreply(func):
class DisabledAction(RuntimeError): class DisabledAction(RuntimeError):
pass pass
## A do "nothing" Milter base class representing an SMTP connection. ## A do "nothing" Milter base class.
#
# Python milters should derive from this class # Python milters should derive from this class
# unless they are using the low level milter module directly. # unless they are using the low lever milter module directly.
# # All optional callbacks are disabled, and automatically
# Most of the methods are either "actions" or "callbacks". Callbacks # reenabled when overridden.
# are invoked by the MTA at certain points in the SMTP protocol. For
# instance when the HELO command is seen, the MTA calls the helo
# callback before returning a response code. All callbacks must
# return one of these constants: CONTINUE, TEMPFAIL, REJECT, ACCEPT,
# DISCARD, SKIP. The NOREPLY response is supplied automatically by
# the @@noreply decorator if negotiation with the MTA is successful.
# @@noreply and @@nocallback methods should return CONTINUE for two reasons:
# the MTA may not support negotiation, and the class may be running in a test
# harness.
#
# Optional callbacks are disabled with the @@nocallback decorator, and
# automatically reenabled when overridden. Disabled callbacks should
# still return CONTINUE for testing and MTAs that do not support
# negotiation.
# Each SMTP connection to the MTA calls the factory method you provide to
# create an instance derived from this class. This is typically the
# constructor for a class derived from Base. The _setctx() method attaches
# the instance to the low level milter.milterContext object. When the SMTP
# connection terminates, the close callback is called, the low level connection
# object is destroyed, and this normally causes instances of this class to be
# garbage collected as well. The close() method should release any global
# resources held by instances.
# @since 0.9.2 # @since 0.9.2
class Base(object): class Base(object):
"The core class interface to the %milter module." "The core class interface to the milter module."
## Attach this Milter to the low level milter.milterContext object. ## Attach this Milter to the low level milter.milterContext object.
def _setctx(self,ctx): def _setctx(self,ctx):
## The low level @ref milter.milterContext object.
self._ctx = ctx self._ctx = ctx
## A bitmask of actions this connection has negotiated to use. self._actions = CURR_ACTS # all actions enabled by default
# By default, all actions are enabled. High throughput milters self._protocol = 0 # no protocol options by default
# may want to disable unused actions to increase efficiency. if ctx:
# Some optional actions may be disabled by calling milter.set_flags(), or ctx.setpriv(self)
# by overriding the negotiate callback. The bits include: ## @var _actions
# A bitmask of actions this milter has negotiated to use.
# By default, all actions are enabled. This may be changed
# by calling <code>milter.set_flags</code>, or 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,SETSMLIST</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
# which actions are available. This is especially useful in
# generic library code designed to work in multiple milters.
# @since 0.9.2 # @since 0.9.2
# #
self._actions = CURR_ACTS # all actions enabled by default
## A bitmask of protocol options this connection has negotiated. ## @var _protocol
# An application may inspect this # A bitmask of protocol options this milter has negotiated.
# variable to determine which protocol steps are supported. Options # The bits generally indicate that a particular step should be
# of interest to applications: the SKIP result code is allowed # skipped, since previous versions of the milter protocol had
# only if the P_SKIP bit is set, rejected recipients are passed to the # no provision for skipping steps.
# %milter application only if the P_RCPT_REJ bit is set, and
# header values are sent and received with leading spaces (in the
# continuation lines) intact if the P_HDR_LEADSPC bit is set (so
# that the application can customize indenting).
#
# The P_N* bits should be negotiated via the @@noreply and @@nocallback
# method decorators, and P_RCPT_REJ, P_HDR_LEADSPC should
# be enabled using the enable_protocols class decorator.
#
# The bits include: <code> # The bits include: <code>
# P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN # P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN
# P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT # P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT
# P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP # P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP
# </code> (all under the Milter namespace). # </code> (all under the Milter namespace).
# @since 0.9.2 # @since 0.9.2
self._protocol = 0 # no protocol options by default
if ctx:
ctx.setpriv(self)
## 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.
# <a href="milter_api/xxfi_connect.html">
# 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.
# The format of hostaddr depends on the socket family: # The format of hostaddr depends on the socket family:
@@ -273,17 +177,6 @@ class Base(object):
# <dt><code>socket.AF_UNIX</code> # <dt><code>socket.AF_UNIX</code>
# <dd>A string with the socketname # <dd>A string with the socketname
# </dl> # </dl>
# To vary behavior based on what port the client connected to,
# for example skipping blacklist checks for port 587 (which must
# be authenticated), use @link #getsymval getsymval('{daemon_port}') @endlink.
# The <code>{daemon_port}</code> macro must be enabled in sendmail.cf
# <pre>
# O Milter.macros.connect=j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}
# </pre>
# or sendmail.mc
# <pre>
# define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}'')dnl
# </pre>
# @param hostname the PTR name or bracketed IP of the SMTP client # @param hostname the PTR name or bracketed IP of the SMTP client
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>, # @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
# or <code>socket.AF_UNIX</code> # or <code>socket.AF_UNIX</code>
@@ -295,26 +188,12 @@ class Base(object):
# this almost always results in terminating the connection. # this almost always results in terminating the connection.
@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.
# <a href="milter_api/xxfi_envfrom.html">
# 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
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
# For the From: header (author) defined in
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
# see @link #header the header callback @endlink.
@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.
# <a href="milter_api/xxfi_envrcpt.html">
# 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
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
# For recipients defined in
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
# for example To: or Cc:, see @link #header the header callback @endlink.
@nocallback @nocallback
def envrcpt(self,to,*str): return CONTINUE def envrcpt(self,to,*str): return CONTINUE
## Called when the SMTP client says DATA. ## Called when the SMTP client says DATA.
@@ -348,7 +227,7 @@ class Base(object):
## Called when the connection is closed. ## Called when the connection is closed.
def close(self): return CONTINUE def close(self): return CONTINUE
## Return mask of SMFIP_N* protocol option bits to clear for this class ## Return mask of SMFIP_N.. protocol option bits to clear for this class
# The @@nocallback and @@noreply decorators set the # The @@nocallback and @@noreply decorators set the
# <code>milter_protocol</code> function attribute to the protocol mask bit to # <code>milter_protocol</code> function attribute to the protocol mask bit to
# pass to libmilter, causing that callback or its reply to be skipped. # pass to libmilter, causing that callback or its reply to be skipped.
@@ -372,20 +251,10 @@ class Base(object):
klass._protocol_mask = p klass._protocol_mask = p
return p return p
## Negotiate milter protocol options. Called by the ## Negotiate milter protocol options.
# <a href="milter_api/xxfi_negotiate.html">
# xffi_negotiate</a> callback. This is an advanced callback,
# do not override unless you know what you are doing. Most
# negotiation can be done simply by using the supplied
# class and function decorators.
# Options are passed as
# a list of 4 32-bit ints which can be modified and are passed
# back to libmilter on return.
# Default negotiation sets P_NO* and P_NR* for callbacks # Default negotiation sets P_NO* and P_NR* for callbacks
# marked @@nocallback and @@noreply respectively, leaves all # marked @@nocallback and @@noreply respectively, leaves all
# actions enabled, and enables Milter.SKIP. The @@enable_protocols # actions enabled, and enables Milter.SKIP.
# class decorator can customize which protocol steps are implemented.
# @param opts a modifiable list of 4 ints with negotiated options
# @since 0.9.2 # @since 0.9.2
def negotiate(self,opts): def negotiate(self,opts):
try: try:
@@ -404,33 +273,14 @@ 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">
# 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):
return self._ctx.getsymval(sym) return self._ctx.getsymval(sym)
## 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.
# 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
# head scratching. What will <i>really</i> irritate you, however,
# is that if you carefully double any '%%', your message will be
# sent - but with the '%%' still doubled!
# See <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> for more information.
# @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>.
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
# @param msg The text part of the SMTP reply. If msg is None,
# an empty message is used.
# @param ml Optional additional message lines.
def setreply(self,rcode,xcode=None,msg=None,*ml): def setreply(self,rcode,xcode=None,msg=None,*ml):
for m in (msg,)+ml:
if 1 in [len(s)&1 for s in R.findall(m)]:
raise ValueError("'%' must be doubled: "+m)
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.
@@ -449,36 +299,28 @@ class Base(object):
# 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">
# smfi_addheader</a>.
# The <code>Milter.ADDHDRS</code> action flag must be set. # The <code>Milter.ADDHDRS</code> action flag must be set.
# #
# May be called from eom callback only. # May be called from eom callback only.
# @param field the header field name # @param field the header field name
# @param value the header field value # @param value the header field value
# @param idx header field index from the top of the message to insert at # @param idx header field index from the top of the message to insert at
# @throws DisabledAction if ADDHDRS is not enabled
def addheader(self,field,value,idx=-1): def addheader(self,field,value,idx=-1):
if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS") if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
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">
# smfi_chgheader</a>.
# The <code>Milter.CHGHDRS</code> action flag must be set. # The <code>Milter.CHGHDRS</code> action flag must be set.
# #
# May be called from eom callback only. # May be called from eom callback only.
# @param field the name of the field to change # @param field the name of the field to change
# @param idx index of the field to change when there are multiple instances # @param idx index of the field to change when there are multiple instances
# @param value the new value of the field # @param value the new value of the field
# @throws DisabledAction if CHGHDRS is not enabled
def chgheader(self,field,idx,value): def chgheader(self,field,idx,value):
if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS") if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
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">
# 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
# RCPT TO command (and as delivered to the envrcpt callback), for example # RCPT TO command (and as delivered to the envrcpt callback), for example
@@ -490,42 +332,33 @@ class Base(object):
# May be called from eom callback only. # May be called from eom callback only.
# @param rcpt the message recipient # @param rcpt the message recipient
# @param params an optional list of ESMTP parameters # @param params an optional list of ESMTP parameters
# @throws DisabledAction if ADDRCPT or ADDRCPT_PAR is not enabled
def addrcpt(self,rcpt,params=None): def addrcpt(self,rcpt,params=None):
if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT") if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT")
if params and not self._actions & ADDRCPT_PAR: if params and not self._actions & ADDRCPT_PAR:
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">
# 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.
# #
# May be called from eom callback only. # May be called from eom callback only.
# @param rcpt the message recipient to delete # @param rcpt the message recipient to delete
# @throws DisabledAction if DELRCPT is not enabled
def delrcpt(self,rcpt): def delrcpt(self,rcpt):
if not self._actions & DELRCPT: raise DisabledAction("DELRCPT") if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
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">
# 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.
# The <code>Milter.MODBODY</code> action flag must be set. # The <code>Milter.MODBODY</code> action flag must be set.
# #
# May be called from eom callback only. # May be called from eom callback only.
# @param body a chunk of body data # @param body a chunk of body data
# @throws DisabledAction if MODBODY is not enabled
def replacebody(self,body): def replacebody(self,body):
if not self._actions & MODBODY: raise DisabledAction("MODBODY") if not self._actions & MODBODY: raise DisabledAction("MODBODY")
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">
# 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),
# for example <code>self.chgfrom('<bar@example.com>')</code>. # for example <code>self.chgfrom('<bar@example.com>')</code>.
@@ -535,28 +368,22 @@ class Base(object):
# @since 0.9.1 # @since 0.9.1
# @param sender the new sender address # @param sender the new sender address
# @param params an optional list of ESMTP parameters # @param params an optional list of ESMTP parameters
# @throws DisabledAction if CHGFROM is not enabled
def chgfrom(self,sender,params=None): def chgfrom(self,sender,params=None):
if not self._actions & CHGFROM: raise DisabledAction("CHGFROM") if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
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">
# 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.
# The <code>Milter.QUARANTINE</code> action flag must be set. # The <code>Milter.QUARANTINE</code> action flag must be set.
# #
# May be called from eom callback only. # May be called from eom callback only.
# @param reason a string describing the reason for quarantine # @param reason a string describing the reason for quarantine
# @throws DisabledAction if QUARANTINE is not enabled
def quarantine(self,reason): def quarantine(self,reason):
if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE") if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
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">
# 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):
return self._ctx.progress() return self._ctx.progress()
@@ -569,9 +396,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):
@@ -638,14 +465,12 @@ class Milter(Base):
factory = Milter factory = Milter
## @private ## @private
# @brief Connect context to connection instance and return enabled callbacks.
def negotiate_callback(ctx,opts): def negotiate_callback(ctx,opts):
m = factory() m = factory()
m._setctx(ctx) m._setctx(ctx)
return m.negotiate(opts) return m.negotiate(opts)
## @private ## @private
# @brief Connect context if needed and invoke connect method.
def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN): def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
m = ctx.getpriv() m = ctx.getpriv()
if not m: if not m:
@@ -656,7 +481,6 @@ def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
return m.connect(hostname,family,hostaddr) return m.connect(hostname,family,hostaddr)
## @private ## @private
# @brief Disconnect milterContext and call close method.
def close_callback(ctx): def close_callback(ctx):
m = ctx.getpriv() m = ctx.getpriv()
if not m: return CONTINUE if not m: return CONTINUE
@@ -703,11 +527,11 @@ def envcallback(c,args):
pargs.append(s) pargs.append(s)
return c(*pargs,**kw) return c(*pargs,**kw)
## Run the %milter. ## Run the milter.
# @param name the name of the %milter known to the MTA # @param name the name of the milter known by the MTA
# @param socketname the socket to be passed to milter.setconn() # @param socketname the socket to be passed to <code>milter.setconn</code>
# @param timeout the time in secs the MTA should wait for a response before # @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):
# This bit is here on the assumption that you will be starting this filter # 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, # before sendmail. If sendmail is not running and the socket already exists,
@@ -774,5 +598,4 @@ for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'):
__all__ = __all__.keys() __all__ = __all__.keys()
## @example milter-template.py ## @example milter-template.py
## @example milter-nomix.py
# #
+5 -26
View File
@@ -70,24 +70,15 @@ class Session(object):
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
post: isinstance(__return__, types.ListType) post: isinstance(__return__, types.ListType)
""" """
if name.endswith('.'): name = name[:-1]
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
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
cnamek = (name,'CNAME')
cname = self.cache.get( cnamek )
if cname: if not result:
cname = cname[0]
else:
safe2cache = Session.SAFE2CACHE safe2cache = Session.SAFE2CACHE
for k, v in DNSLookup(name, qtype): for k, v in DNSLookup(name, qtype):
if k == cnamek: if k == (name, 'CNAME'):
cname = v cname = v
if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache: if (qtype,k[1]) in safe2cache:
self.cache.setdefault(k, []).append(v) self.cache.setdefault(k, []).append(v)
result = self.cache.get( (name, qtype), []) result = self.cache.get( (name, qtype), [])
if not result and cname: if not result and cname:
@@ -97,23 +88,11 @@ 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:
self.cache[(name,qtype)] = result
return result return result
def dns_txt(self, domainname, enc='ascii'):
"Get a list of TXT records for a domain name."
if domainname:
try:
return [''.join(s.decode(enc) for s in a)
for a in self.dns(domainname, 'TXT')]
except UnicodeEncodeError:
raise DNSError('Non-ascii character in SPF TXT record.')
return []
DNS.DiscoverNameServers() DNS.DiscoverNameServers()
if __name__ == '__main__': if __name__ == '__main__':
+1 -4
View File
@@ -5,9 +5,6 @@
# 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.21 2011/03/03 05:11:58 customdesigned
# Release 0.9.4
#
# Revision 1.20 2010/10/11 00:29:47 customdesigned # Revision 1.20 2010/10/11 00:29:47 customdesigned
# Handle multiple recipients. For CBV or auto whitelist of multiple emails. # Handle multiple recipients. For CBV or auto whitelist of multiple emails.
# #
@@ -152,7 +149,7 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
pass # MX didn't accept connections, try next one pass # MX didn't accept connections, try next one
except socket.timeout: except socket.timeout:
pass # MX too slow, try next one pass # MX too slow, try next one
if hasattr(smtp,'sock'): smtp.close() smtp.close()
if time.time() > toolate: if time.time() > toolate:
return (450,'No MX response within %f minutes'%(timeout/60.0)) return (450,'No MX response within %f minutes'%(timeout/60.0))
return (450,'No MX servers available') # temp error return (450,'No MX servers available') # temp error
+1 -2
View File
@@ -48,10 +48,9 @@ def is_dynip(host,addr):
True True
""" """
if host.startswith('[') and host.endswith(']'): if host.startswith('[') and host.endswith(']'):
return True # no ptr return True
if addr: if addr:
if host.find(addr) >= 0: return True if host.find(addr) >= 0: return True
if addr.find(':') >= 0: return False # IP6
a = addr.split('.') a = addr.split('.')
ia = map(int,a) ia = map(int,a)
h = host h = host
+1 -1
View File
@@ -50,7 +50,7 @@ class Greylist(object):
# expired # expired
log.debug('Expired greylist: %s',key) log.debug('Expired greylist: %s',key)
r = Record() r = Record()
elif now < r.firstseen + self.greylist_time + 5: elif now < r.firstseen + self.greylist_time:
# still greylisted # still greylisted
log.debug('Early greylist: %s',key) log.debug('Early greylist: %s',key)
#r = Record() #r = Record()
+9 -44
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$'
@@ -31,28 +28,23 @@ ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
}, re.IGNORECASE) }, re.IGNORECASE)
# from spf.py # from spf.py
def addr2bin(s): def addr2bin(str):
"""Convert a string IPv4 address into an unsigned integer.""" """Convert a string IPv4 address into an unsigned integer."""
if s.find(':') >= 0:
try: try:
return bin2long6(inet_pton(s)) return struct.unpack("!L", socket.inet_aton(str))[0]
except:
raise socket.error("Invalid IP6 address: "+s)
try:
return struct.unpack("!L", socket.inet_aton(s))[0]
except socket.error: except socket.error:
raise socket.error("Invalid IP4 address: "+s) raise socket.error("Invalid IP4 address: "+str)
def bin2long6(s): def bin2long6(str):
"""Convert binary IP6 address into an unsigned Python long integer.""" """Convert binary IP6 address into an unsigned Python long integer."""
h, l = struct.unpack("!QQ", s) h, l = struct.unpack("!QQ", str)
return h << 64 | l return h << 64 | l
if hasattr(socket,'has_ipv6') and socket.has_ipv6: if hasattr(socket,'has_ipv6') and socket.has_ipv6:
def inet_ntop(s): def inet_ntop(s):
return socket.inet_ntop(socket.AF_INET6,s) return socket.inet_ntop(socket.AF_INET6,s)
def inet_pton(s): def inet_pton(s):
return socket.inet_pton(socket.AF_INET6,s.strip()) return socket.inet_pton(socket.AF_INET6,s)
else: else:
from pyip6 import inet_ntop, inet_pton from pyip6 import inet_ntop, inet_pton
@@ -70,12 +62,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 +70,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)
@@ -107,13 +91,6 @@ def iniplist(ipaddr,iplist):
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 +98,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.
@@ -160,16 +136,6 @@ def parseaddr(t):
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,18 +180,17 @@ 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))
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
except ValueError: pass
except email.Errors.HeaderParseError: pass except email.Errors.HeaderParseError: pass
return val return val
+29 -5
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:
@@ -68,7 +69,8 @@ Not-so-quick Installation
First install Sendmail. Make sure you read libmilter/README in the Sendmail First install Sendmail. Make sure you read libmilter/README in the Sendmail
source directory, and make sure you enable libmilter before you build. The source directory, and make sure you enable libmilter before you build. The
8.11 series had libmilter marked as FFR (For Future Release); 8.12 8.11 series had libmilter marked as FFR (For Future Release); 8.12
officially supports libmilter, but it's still not built by default. officially
supports libmilter, but it's still not built by default.
Install Python, and enable threading in Modules/Setup. Install Python, and enable threading in Modules/Setup.
@@ -109,6 +111,28 @@ _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
---------- ----------
+2 -52
View File
@@ -1,7 +1,8 @@
## @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
@@ -33,54 +34,3 @@
# The <code>mime</code> module provides a wrapper for the Python email package # The <code>mime</code> 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
#
# The libmilter library which pymilter wraps
# <a href="milter_overview#SignalHandling">handles
# 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
# <a href="milter_api/smfi_stop.html">smfi_stop</a>
# and the last to an internal ABORT.
#
# If you use python threads or threading modules, then signal handling gets
# confused. Threads may still be useful, but you may need to provide an
# alternate means of causing graceful shutdown.
#
# You may find the
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
# multiprocessing</a> module useful. It can be a drop-in
# replacement for threading as illustrated in
# <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.
#
+3 -86
View File
@@ -20,58 +20,23 @@
# 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>.
def getsymval(self,sym): pass def getsymval(self,sym): pass
## Calls <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> or
# <a href="milter_api/smfi_setmlreply.html">
# smfi_setmlreply</a>.
# @param rcode SMTP response code
# @param xcode extended SMTP response code
# @param msg one or more message lines. If the MTA does not support
# 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>.
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>.
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>.
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>.
def delrcpt(self,rcpt): pass def delrcpt(self,rcpt): pass
## Calls <a href="milter_api/smfi_replacebody.html">smfi_replacebody</a>.
def replacebody(self,data): pass def replacebody(self,data): pass
## Attach a Python object to this connection context.
# @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.
def getpriv(self): pass def getpriv(self): pass
## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>.
def quarantine(self,reason): pass def quarantine(self,reason): pass
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>.
def progress(self): pass def progress(self): pass
## Calls <a href="milter_api/smfi_chgfrom.html">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. def setsmlist(self,stage,macrolist): pass
# Of interest only when you need to squeeze a few more bytes of bandwidth.
# It may only be called from the negotiate callback.
# The protocol stages are
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>.
# @param stage protocol stage in which the macro list should be used
# @param macrolist a space separated list of macro names
def setsymlist(self,stage,macrolist): pass
class error(Exception): pass class error(Exception): pass
## Enable optional milter actions.
# Certain milter actions need to be enabled before calling main()
# or they throw an exception. Pymilter enables them all by
# default (since 0.9.2), but you may wish to disable unneeded
# actions as an optimization.
# @param flags Bit or mask of optional actions to enable
def set_flags(flags): pass def set_flags(flags): pass
def set_connect_callback(cb): pass def set_connect_callback(cb): pass
def set_helo_callback(cb): pass def set_helo_callback(cb): pass
def set_envfrom_callback(cb): pass def set_envfrom_callback(cb): pass
@@ -81,67 +46,19 @@ def set_eoh_callback(cb): pass
def set_body_callback(cb): pass def set_body_callback(cb): pass
def set_abort_callback(cb): pass 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.
# Must be one of TEMPFAIL,REJECT,CONTINUE
def set_exception_policy(code): pass def set_exception_policy(code): pass
## Register python milter with libmilter.
# 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
# registering the milter.
# Three additional callbacks are specified as keyword parameters. These
# 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
# it before registering. I may move all the callbacks
# in the future (perhaps keeping the set functions for compatibility).
# @param name the milter name by which the MTA finds us
# @param negotiate the
# <a href="milter_api/xxfi_negotiate.html">
# xxfi_negotiate</a> callback, called to negotiate supported
# actions, callbacks, and protocol steps.
# @param unknown the
# <a href="milter_api/xxfi_unknown.html">
# xxfi_unknown</a> callback, called when for SMTP commands
# not recognized by the MTA. (Extend SMTP in your milter!)
# @param data the
# <a href="milter_api/xxfi_data.html">
# xxfi_data</a> callback, called when the DATA
# SMTP command is received.
def register(name,negotiate=None,unknown=None,data=None): pass def register(name,negotiate=None,unknown=None,data=None): pass
## Attempt to create the socket used to communicate with the MTA.
# milter.opensocket() attempts to create the socket specified previously by a
# 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
# created. If this is not called, milter.main() will do so implicitly.
# Calls <a href="milter_api/smfi_opensocket.html">
# smfi_opensocket</a>. While not documented for libmilter, my experiments
# indicate that you must call register() before calling opensocket().
# @param rmsock Try to remove an existing unix domain socket if true.
def opensocket(rmsock): pass def opensocket(rmsock): pass
## Transfer control to libmilter.
# Calls <a href="milter_api/smfi_main.html">
# 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> # smfi_setdbg 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
# current, highest, useful value. Must be called before calling main(). # current, highest, useful value.
def setdbg(lev): pass def setdbg(lev): pass
## Set timeout for MTA communication.
# Calls <a href="milter_api/smfi_settimeout.html">
# smfi_settimeout</a>. Must be called before calling main().
def settimeout(secs): pass def settimeout(secs): pass
## Set socket backlog.
# Calls <a href="milter_api/smfi_setbacklog.html">
# smfi_setbacklog</a>. Must be called before calling main().
def setbacklog(n): pass def setbacklog(n): pass
## Set the socket used to communicate with the MTA. ## Set the socket used to communicate with the MTA.
+3 -5
View File
@@ -1,11 +1,9 @@
web: web:
doxygen doxygen
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-devel-* doc/html/milter_api rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
rsync -ravKk doc/html/ spidey2.bmsi.com:/Public/pymilter
cd doc/html; zip -r ../../doc .
VERSION=0.9.6 VERSION=0.9.4
CVSTAG=pymilter-0_9_6 CVSTAG=pymilter-0_9_4
PKG=pymilter-$(VERSION) PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz SRCTAR=$(PKG).tar.gz
-79
View File
@@ -1,79 +0,0 @@
## A very simple milter to prevent mixing of internal and external mail.
# Internal is defined as using one of a list of internal top level domains.
# This code is open-source on the same terms as Python.
import Milter
import time
import sys
from Milter.utils import parse_addr
internal_tlds = ["corp", "personal"]
## Determine if a hostname is internal or not.
# True if internal, False otherwise
def is_internal(hostname):
components = hostname.split(".")
return components.pop() in internal_tlds:
# Determine if internal and external hosts are mixed based on a list
# of hostnames
def are_mixed(hostnames):
hostnames_mapped = map(is_internal, hostnames)
# Num internals
num_internal_hosts = hostnames_mapped.count(True)
# Num externals
num_external_hosts = hostnames_mapped.count(False)
return num_external_hosts >= 1 and num_internal_hosts >= 1
class NoMixMilter(Milter.Base):
def __init__(self): # A new instance with each new connection.
self.id = Milter.uniqueID() # Integer incremented with each call.
## def envfrom(self,f,*str):
@Milter.noreply
def envfrom(self, mailfrom, *str):
self.mailfrom = mailfrom
self.domains = []
t = parse_addr(mailfrom)
if len(t) > 1:
self.domains.append(t[1])
else:
self.domains.append('local')
self.internal = False
return Milter.CONTINUE
## def envrcpt(self, to, *str):
def envrcpt(self, to, *str):
self.R.append(to)
t = parse_addr(to)
if len(t) > 1:
self.domains.append(t[1])
else:
self.domains.append('local')
if are_mixed(self.domains):
# FIXME: log recipients collected in self.mailfrom and self.R
self.setreply('550','5.7.1','Mixing internal and external TLDs')
return Milter.REJECT
return Milter.CONTINUE
def main():
socketname = "/var/run/nomixsock"
timeout = 600
# Register to have the Milter factory create instances of your class:
Milter.factory = NoMixMilter
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush()
Milter.runmilter("nomixfilter",socketname,timeout)
logq.put(None)
bt.join()
print "%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__":
main()
+11 -29
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.
@@ -11,16 +11,9 @@ import Milter
import StringIO import StringIO
import time import time
import email import email
import sys
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
from Milter.utils import parse_addr from Milter import parse_addr
if True:
from multiprocessing import Process as Thread, Queue
else:
from threading import Thread
from Queue import Queue
logq = Queue(maxsize=4)
class myMilter(Milter.Base): class myMilter(Milter.Base):
@@ -30,7 +23,7 @@ class myMilter(Milter.Base):
# each connection runs in its own thread and has its own myMilter # each connection runs in its own thread and has its own myMilter
# instance. Python code must be thread safe. This is trivial if only stuff # instance. Python code must be thread safe. This is trivial if only stuff
# in myMilter instances is referenced. # in myMilter instances is referenced.
@Milter.noreply @noreply
def connect(self, IPname, family, hostaddr): def connect(self, IPname, family, hostaddr):
# (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) ) # (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
# (self, 'ip6.mxout.example.com', AF_INET6, # (self, 'ip6.mxout.example.com', AF_INET6,
@@ -78,25 +71,25 @@ class myMilter(Milter.Base):
## def envrcpt(self, to, *str): ## def envrcpt(self, to, *str):
@Milter.noreply @noreply
def envrcpt(self, to, *str): def envrcpt(self, recipient, *str):
rcptinfo = to,Milter.dictfromlist(str) rcptinfo = to,Milter.dictfromlist(str)
self.R.append(rcptinfo) self.R.append(rcptinfo)
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @noreply
def header(self, name, hval): def header(self, name, hval):
self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @noreply
def eoh(self): def eoh(self):
self.fp.write("\n") # terminate headers self.fp.write("\n") # terminate headers
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @noreply
def body(self, chunk): def body(self, chunk):
self.fp.write(chunk) self.fp.write(chunk)
return Milter.CONTINUE return Milter.CONTINUE
@@ -110,6 +103,7 @@ class myMilter(Milter.Base):
self.addrcpt('<%s>' % 'spy@example.com') self.addrcpt('<%s>' % 'spy@example.com')
return Milter.ACCEPT return Milter.ACCEPT
def close(self): def close(self):
# always called, even when abort is called. Clean up # always called, even when abort is called. Clean up
# any external resources here. # any external resources here.
@@ -122,25 +116,15 @@ class myMilter(Milter.Base):
## === Support Functions === ## === Support Functions ===
def log(self,*msg): def log(self,*msg):
logq.put((msg,self.id,time.time())) print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
def background():
while True:
t = logq.get()
if not t: break
msg,id,ts = t
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ... # 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print i, for i in msg: print i,
print print
## === ## ===
def main(): def main():
bt = Thread(target=background)
bt.start()
socketname = "/home/stuart/pythonsock"
timeout = 600
# Register to have the Milter factory create instances of your class: # Register to have the Milter factory create instances of your class:
Milter.factory = myMilter Milter.factory = myMilter
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
@@ -150,8 +134,6 @@ def main():
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S') print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,timeout) Milter.runmilter("pythonfilter",socketname,timeout)
logq.put(None)
bt.join()
print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S') print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__": if __name__ == "__main__":
+40 -118
View File
@@ -35,22 +35,6 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* Revision 1.31 2012/04/12 23:32:50 customdesigned
* Replace redundant callback array with macros. If this doesn't break anything,
* macros can be eliminated with code changes.
*
* Revision 1.30 2012/04/12 23:08:06 customdesigned
* Support RFC2553 on BSD
*
* Revision 1.29 2011/06/09 15:45:27 customdesigned
* Print callback name for non-int return error.
*
* Revision 1.28 2011/06/08 23:13:48 customdesigned
* Generate special exception when callback return not int.
*
* Revision 1.27 2009/07/28 21:45:54 customdesigned
* Add getversion() to return runtime version.
*
* Revision 1.26 2009/07/28 21:08:20 customdesigned * Revision 1.26 2009/07/28 21:08:20 customdesigned
* Increment del count. * Increment del count.
* *
@@ -267,10 +251,10 @@ $ python setup.py help
#define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */ #define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */
/* Now see if it supports the RFC-2553 socket's API spec. Early /* Now see if it supports the RFC-2553 socket's API spec. Early
* IPv6 "prototype" implementations existed before the RFC was * IPv6 "prototype" implementations existed before the RFC was
* published. Unfortunately I know of no good way to do this * published. Unfortunately I know of now good way to do this
* other than with OS-specific tests. * other than with OS-specific tests.
*/ */
#if defined(__FreeBSD_kernel__) || defined(__linux__) #ifdef linux
#define HAVE_IPV6_RFC2553 #define HAVE_IPV6_RFC2553
#include <arpa/inet.h> #include <arpa/inet.h>
#endif #endif
@@ -284,54 +268,21 @@ $ python setup.py help
#endif #endif
#endif #endif
enum callbacks {
CONNECT,HELO,ENVFROM,ENVRCPT,HEADER,EOH,BODY,EOM,ABORT,CLOSE,
#ifdef SMFIS_ALL_OPTS
UNKNOWN,DATA,NEGOTIATE,
#endif
NUMCALLBACKS
};
#define connect_callback callback[CONNECT].cb /* Yes, these are static. If you need multiple different callbacks, */
#define helo_callback callback[HELO].cb /* it's cleaner to use multiple filters, or convert to OO method calls. */
#define envfrom_callback callback[ENVFROM].cb static PyObject *connect_callback = NULL;
#define envrcpt_callback callback[ENVRCPT].cb static PyObject *helo_callback = NULL;
#define header_callback callback[HEADER].cb static PyObject *envfrom_callback = NULL;
#define eoh_callback callback[EOH].cb static PyObject *envrcpt_callback = NULL;
#define body_callback callback[BODY].cb static PyObject *header_callback = NULL;
#define eom_callback callback[EOM].cb static PyObject *eoh_callback = NULL;
#define abort_callback callback[ABORT].cb static PyObject *body_callback = NULL;
#define close_callback callback[CLOSE].cb static PyObject *eom_callback = NULL;
#define unknown_callback callback[UNKNOWN].cb static PyObject *abort_callback = NULL;
#define data_callback callback[DATA].cb static PyObject *close_callback = NULL;
#define negotiate_callback callback[NEGOTIATE].cb
/* Yes, these are static. If you need multiple different callbacks, staticforward struct smfiDesc description; /* forward declaration */
it's cleaner to use multiple filters, or convert to OO method calls. */
static struct MilterCallback {
PyObject *cb;
const char *name;
} callback[NUMCALLBACKS+1] = {
{ NULL ,"connect" },
{ NULL ,"helo" },
{ NULL ,"envfrom" },
{ NULL ,"envrcpt" },
{ NULL ,"header" },
{ NULL ,"eoh" },
{ NULL ,"body" },
{ NULL ,"eom" },
{ NULL ,"abort" },
{ NULL ,"close" },
#ifdef SMFIS_ALL_OPTS
{ NULL ,"unknown" },
{ NULL ,"data" },
{ NULL ,"negotiate" },
#endif
{ NULL , NULL }
};
static 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 +294,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
@@ -680,23 +631,9 @@ _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)) { retval = PyInt_AsLong(result);
const struct MilterCallback *p;
const char *cbname = "milter";
char buf[40];
Py_DECREF(result);
for (p = callback; p->name; ++p) {
if (cb == p->cb) {
cbname = p->name;
break;
}
}
sprintf(buf,"The %s callback must return int",cbname);
PyErr_SetString(MilterError,buf);
return _report_exception(self);
}
retval = PyLong_AS_LONG(result);
Py_DECREF(result); Py_DECREF(result);
if (PyErr_Occurred()) return _report_exception(self);
_release_thread(self->t); _release_thread(self->t);
return retval; return retval;
} }
@@ -712,7 +649,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 +657,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 +749,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);
@@ -885,6 +822,10 @@ milter_wrap_abort(SMFICTX *ctx) {
} }
#ifdef SMFIS_ALL_OPTS #ifdef SMFIS_ALL_OPTS
static PyObject *unknown_callback = NULL;
static PyObject *data_callback = NULL;
static PyObject *negotiate_callback = NULL;
static int static int
milter_wrap_unknown(SMFICTX *ctx, const char *cmd) { milter_wrap_unknown(SMFICTX *ctx, const char *cmd) {
PyObject *arglist; PyObject *arglist;
@@ -943,7 +884,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()) {
@@ -1531,6 +1472,11 @@ static PyMethodDef context_methods[] = {
{ 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 +1525,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,13 +1546,6 @@ 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 char milter_documentation[] =
@@ -1615,27 +1555,17 @@ 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);
@@ -1662,13 +1592,6 @@ PyMODINIT_FUNC PyInit_milter(void) {
#endif #endif
#ifdef SMFIF_SETSMLIST #ifdef SMFIF_SETSMLIST
setitem(d,"SETSMLIST",SMFIF_SETSMLIST); setitem(d,"SETSMLIST",SMFIF_SETSMLIST);
setitem(d,"M_CONNECT",SMFIM_CONNECT);/* connect */
setitem(d,"M_HELO",SMFIM_HELO); /* HELO/EHLO */
setitem(d,"M_ENVFROM",SMFIM_ENVFROM);/* MAIL From */
setitem(d,"M_ENVRCPT",SMFIM_ENVRCPT);/* RCPT To */
setitem(d,"M_DATA",SMFIM_DATA); /* DATA */
setitem(d,"M_EOM",SMFIM_EOM); /* end of message (final dot) */
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 +1624,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;
} }
+2 -4
View File
@@ -1,7 +1,4 @@
# $Log$ # $Log$
# Revision 1.7 2009/06/13 21:15:12 customdesigned
# Doxygen updates.
#
# Revision 1.6 2009/06/09 03:13:13 customdesigned # Revision 1.6 2009/06/09 03:13:13 customdesigned
# More doxygen docs. # More doxygen docs.
# #
@@ -168,14 +165,15 @@ class MimeMessage(Message):
""" """
def __init__(self,fp=None,seekable=1): def __init__(self,fp=None,seekable=1):
Message.__init__(self) Message.__init__(self)
self.headerchange = None
self.submsg = None self.submsg = None
self.modified = False self.modified = False
## @var headerchange ## @var headerchange
# Provide a headerchange event for integration with Milter. # Provide a headerchange event for integration with Milter.
# The headerchange attribute can be assigned a function to be called when # The headerchange attribute can be assigned a function to be called when
# changing headers. The signature is: # changing headers. The signature is:
# headerchange(msg,name,value) -> None # headerchange(msg,name,value) -> None
self.headerchange = None
def get_param(self, param, failobj=None, header='content-type', unquote=True): def get_param(self, param, failobj=None, header='content-type', unquote=True):
val = Message.get_param(self,param,failobj,header,unquote) val = Message.get_param(self,param,failobj,header,unquote)
+4 -19
View File
@@ -1,21 +1,20 @@
%define __python python2.6 %define __python python2.6
%define pythonbase python26
%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)")}
%define pythonbase python26
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: 0.9.4
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
License: GPLv2+ License: GPLv2+
Group: Development/Libraries Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html Url: http://www.bmsi.com/python/milter.html
# python-2.6.4 gets RuntimeError: not holding the import lock Requires: %{pythonbase}, sendmail >= 8.13
Requires: %{pythonbase} >= 2.6.5, sendmail >= 8.13 # Need python2.4 specific pydns, not the version for system python
# Need python2.6 specific pydns, not the version for system python
Requires: %{pythonbase}-pydns Requires: %{pythonbase}-pydns
# Needed for callbacks, not a core function but highly useful for milters # Needed for callbacks, not a core function but highly useful for milters
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13 BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13
@@ -75,20 +74,6 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%changelog %changelog
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
- Remove redundant table in miltermodule
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
- Raise ValueError on unescaped '%' passed to setreply
- Grace time at end of Greylist window
* Fri Aug 19 2011 Stuart Gathman <stuart@bmsi.com> 0.9.5-1
- Print milter.error for invalid callback return type.
(Since stacktrace is empty, the TypeError exception is confusing.)
- Fix milter-template.py
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
* Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1 * Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
- Handle IP6 in Milter.utils.iniplist() - Handle IP6 in Milter.utils.iniplist()
- python-2.6 - python-2.6
+7 -9
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
@@ -35,9 +35,7 @@ class sampleMilter(Milter.Milter):
# multiple messages can be received on a single connection # multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message. # of each message.
@Milter.noreply
def envfrom(self,f,*str): def envfrom(self,f,*str):
"start of MAIL transaction"
self.log("mail from",f,str) self.log("mail from",f,str)
self.fp = StringIO.StringIO() self.fp = StringIO.StringIO()
self.tempname = None self.tempname = None
@@ -171,13 +169,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"
+3 -3
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
packager=Stuart D. Gathman <stuart@gathman.org> packager=Stuart D. Gathman <stuart@bmsi.com>
release=1 release=1
+8 -4
View File
@@ -2,9 +2,6 @@ import os
import sys import sys
from distutils.core import setup, Extension from distutils.core import setup, Extension
if sys.version < '2.6.5':
sys.exit('ERROR: Sorry, python 2.6.5 is required for this module.')
# FIXME: on some versions of sendmail, smutil is renamed to sm. # FIXME: on some versions of sendmail, smutil is renamed to sm.
# On slackware and debian, leave it out entirely. It depends # On slackware and debian, leave it out entirely. It depends
# on how libmilter was built by the sendmail package. # on how libmilter was built by the sendmail package.
@@ -12,8 +9,15 @@ if sys.version < '2.6.5':
libs = ["milter"] libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian libdirs = ["/usr/lib/libmilter"] # needed for Debian
# patch distutils if it can't cope with the "classifiers" or
# "download_url" keywords
if sys.version < '2.2.3':
from distutils.dist import DistributionMetadata
DistributionMetadata.classifiers = None
DistributionMetadata.download_url = None
# 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 = '0.9.4',
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
+3 -6
View File
@@ -1,16 +1,13 @@
#!/bin/sh #!/bin/sh
appname="$1" appname="$1"
script="${2:-${appname}}" script="${2:-${appname}}"
datadir="/var/lib/milter" datadir="/var/log/milter"
logdir="/var/log/milter"
piddir="/var/run/milter" piddir="/var/run/milter"
libdir="/usr/lib/pymilter" libdir="/usr/lib/pymilter"
python="python2.4" python="python2.4"
exec >>${logdir}/${appname}.log 2>&1 exec >>${datadir}/${appname}.log 2>&1
if test -s ${datadir}/${script}.py; then if test -s ${datadir}/${script}.py; then
cd ${datadir} # use version in data dir if it exists for debugging cd ${datadir} # use version in log dir if it exists for debugging
elif test -s ${logdir}/${script}.py; then
cd ${logdir} # use version in log dir if it exists for debugging
else else
cd ${libdir} cd ${libdir}
fi fi
-18587
View File
File diff suppressed because it is too large Load Diff
+1 -33
View File
@@ -1,10 +1,4 @@
# $Log$ # $Log$
# Revision 1.5 2011/06/09 17:27:42 customdesigned
# Documentation updates.
#
# Revision 1.4 2005/07/20 14:49:44 customdesigned
# Handle corrupt and empty ZIP files.
#
# Revision 1.3 2005/06/17 01:49:39 customdesigned # Revision 1.3 2005/06/17 01:49:39 customdesigned
# Handle zip within zip. # Handle zip within zip.
# #
@@ -32,7 +26,6 @@ import socket
import StringIO import StringIO
import email import email
import sys import sys
import Milter
from email import Errors from email import Errors
samp1_txt1 = """Dear Agent 1 samp1_txt1 = """Dear Agent 1
@@ -153,31 +146,6 @@ class MimeTestCase(unittest.TestCase):
# test zip within zip # test zip within zip
self.testDefang('ziploop',1,'stuart@bmsi.com.zip') self.testDefang('ziploop',1,'stuart@bmsi.com.zip')
def _chk_name(self,name):
self.filename = name
def _chk_attach(self,msg):
"Filter attachments by content."
# check for bad extensions
mime.check_name(msg,ckname=self._chk_name,scan_zip=True)
# remove scripts from HTML
mime.check_html(msg)
# don't let a tricky virus slip one past us
msg = msg.get_submsg()
if isinstance(msg,email.Message.Message):
return mime.check_attachments(msg,self._chk_attach)
return Milter.CONTINUE
def testCheckAttach(self,fname="test1"):
# test1 contains a very long filename
msg = mime.message_from_file(open('test/'+fname,'r'))
mime.defang(msg,scan_zip=True)
self.failIf(msg.ismodified())
msg = mime.message_from_file(open('test/test2','r'))
rc = mime.check_attachments(msg,self._chk_attach)
self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
self.assertEquals(rc,Milter.CONTINUE)
def testHTML(self,fname=""): def testHTML(self,fname=""):
result = StringIO.StringIO() result = StringIO.StringIO()
filter = mime.HTMLScriptFilter(result) filter = mime.HTMLScriptFilter(result)
@@ -201,4 +169,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()
-6
View File
@@ -11,7 +11,6 @@ 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):
@@ -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))