Compare commits

..

23 Commits

Author SHA1 Message Date
Stuart D. Gathman 255624ea80 print function 2016-07-26 09:36:23 -04:00
Stuart D. Gathman f5bd952f64 Pull Scott's changes 2016-07-26 07:58:40 -04:00
Stuart D. Gathman 687bebcd45 Merge fix test case from master 2016-07-26 07:56:52 -04:00
Scott Kitterman 5252278804 Fixup test bits missing from the python3 branch. 2016-07-26 01:21:47 -04:00
Stuart D. Gathman e5cff32526 Target python2.7 for master
Conflicts:
	setup.cfg
2016-07-26 01:09:35 -04:00
Stuart D. Gathman 702c6c126f Use unicode literal to join unicode strings.
Conflicts:
	Milter/utils.py
2016-07-26 01:07:13 -04:00
Stuart D. Gathman bf327f95e0 Add section to link projects using pymilter. 2016-07-26 01:04:19 -04:00
Stuart D. Gathman 883b19f131 Fix test case 2016-07-26 01:03:59 -04:00
Stuart D. Gathman 0863bb5602 Fix spurious cleanup error. 2016-07-26 01:03:26 -04:00
Scott Kitterman 67d974638a Import print_function from future so we still work with python2.7 2016-07-25 17:42:19 -04:00
Scott Kitterman 9058f1c2aa sample.py: print is a function in python3 2016-07-25 17:29:29 -04:00
Scott Kitterman 932216e1bf Convert tabs to spaces for sample.py 2016-07-25 17:27:34 -04:00
Stuart D. Gathman 91a70384ed Update README 2016-07-25 10:05:19 -04:00
Stuart Gathman bbd6771a74 Handle missing padding in encoded header 2016-07-25 10:04:55 -04:00
Stuart Gathman 8b36939747 Test case for missing padding. 2016-07-25 10:04:35 -04:00
Stuart Gathman 0c1726614d Link to related packages.
Conflicts:
	doc/mainpage.py
2016-07-25 10:04:06 -04:00
Stuart Gathman 74b8b1ae19 Copy sendmail-devel libmilter api into documention, since milter.org is gone.
Conflicts:
	Milter/__init__.py
	doc/milter.py
	makefile
2016-07-25 10:02:46 -04:00
Stuart Gathman d35ed40edf Fix header_leading_space, update doc version.
Conflicts:
	Doxyfile
	testgrey.py
2016-07-25 09:59:03 -04:00
Stuart Gathman 753c417f31 Fix bug from pyspf - caching server altering case of cached names. 2016-07-25 09:56:11 -04:00
Stuart Gathman 73bd1895cd Add dns name support for iniplist() 2016-07-25 09:55:47 -04:00
Stuart D. Gathman e01b7dabf2 Use python3 for build 2016-07-23 12:19:45 -04:00
Stuart Gathman 1b4903f905 Python3 changes 2013-01-13 04:26:30 +00:00
cvs2svn e1d29fdf6a This commit was manufactured by cvs2svn to create branch 'python3-branch'.
Sprout from master 2012-11-10 03:38:47 UTC Stuart Gathman <stuart@gathman.org> 'Update doc version to 0.9.6'
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
2012-11-10 03:38:48 +00:00
39 changed files with 1233 additions and 2830 deletions
-8
View File
@@ -1,8 +0,0 @@
*.pyc
build/
test/*.out
test/*.tstout
test/*.log
test.db
dist
MANIFEST
+3 -2
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@gathman.org) took that and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) 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,6 +7,7 @@ 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
@@ -43,4 +44,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@gathman.org If I have left anybody out, send me a reminder: stuart@bmsi.com
+39 -88
View File
@@ -1,4 +1,4 @@
# Doxyfile 1.6.1 # Doxyfile 1.5.7.1
# This file describes the settings to be used by the documentation system # This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project # doxygen (www.doxygen.org) for a project
@@ -54,11 +54,11 @@ CREATE_SUBDIRS = NO
# information to generate all constant output in the proper language. # information to generate all constant output in the proper language.
# The default language is English, other supported languages are: # The default language is English, other supported languages are:
# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, # Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional,
# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, # Croatian, Czech, Danish, Dutch, Farsi, Finnish, French, German, Greek,
# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English # Hungarian, Italian, Japanese, Japanese-en (Japanese with English messages),
# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, # Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, Polish,
# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, # Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, Slovene,
# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. # Spanish, Swedish, and Ukrainian.
OUTPUT_LANGUAGE = English OUTPUT_LANGUAGE = English
@@ -207,17 +207,6 @@ OPTIMIZE_FOR_FORTRAN = NO
OPTIMIZE_OUTPUT_VHDL = NO OPTIMIZE_OUTPUT_VHDL = NO
# Doxygen selects the parser to use depending on the extension of the files it parses.
# With this tag you can assign which parser to use for a given extension.
# Doxygen has a built-in mapping, but you can override or extend it using this tag.
# The format is ext=language, where ext is a file extension, and language is one of
# the parsers supported by doxygen: IDL, Java, Javascript, C#, C, C++, D, PHP,
# Objective-C, Python, Fortran, VHDL, C, C++. For instance to make doxygen treat
# .inc files as Fortran files (default is PHP), and .f files as C (default is Fortran),
# use: inc=Fortran f=C. Note that for custom extensions you also need to set FILE_PATTERNS otherwise the files are not read by doxygen.
EXTENSION_MAPPING =
# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
# to include (a tag file for) the STL sources as input, then you should # to include (a tag file for) the STL sources as input, then you should
# set this tag to YES in order to let doxygen match functions declarations and # set this tag to YES in order to let doxygen match functions declarations and
@@ -405,10 +394,6 @@ SORT_MEMBER_DOCS = YES
SORT_BRIEF_DOCS = NO SORT_BRIEF_DOCS = NO
# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the (brief and detailed) documentation of class members so that constructors and destructors are listed first. If set to NO (the default) the constructors will appear in the respective orders defined by SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO.
SORT_MEMBERS_CTORS_1ST = NO
# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the
# hierarchy of group names into alphabetical order. If set to NO (the default) # hierarchy of group names into alphabetical order. If set to NO (the default)
# the group names will appear in their defined order. # the group names will appear in their defined order.
@@ -483,8 +468,7 @@ SHOW_DIRECTORIES = YES
SHOW_FILES = YES SHOW_FILES = YES
# Set the SHOW_NAMESPACES tag to NO to disable the generation of the # Set the SHOW_NAMESPACES tag to NO to disable the generation of the
# Namespaces page. # Namespaces page. This will remove the Namespaces entry from the Quick Index
# This will remove the Namespaces entry from the Quick Index
# and from the Folder Tree View (if specified). The default is YES. # and from the Folder Tree View (if specified). The default is YES.
SHOW_NAMESPACES = YES SHOW_NAMESPACES = YES
@@ -527,7 +511,7 @@ WARNINGS = YES
# for undocumented members. If EXTRACT_ALL is set to YES then this flag will # for undocumented members. If EXTRACT_ALL is set to YES then this flag will
# automatically be disabled. # automatically be disabled.
WARN_IF_UNDOCUMENTED = NO WARN_IF_UNDOCUMENTED = YES
# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as not documenting some # potential errors in the documentation, such as not documenting some
@@ -568,10 +552,7 @@ WARN_LOGFILE =
# directories like "/usr/src/myproject". Separate the files or directories # directories like "/usr/src/myproject". Separate the files or directories
# with spaces. # with spaces.
INPUT = mime.py \ INPUT = mime.py doc/mainpage.py doc/milter.py Milter
doc/mainpage.py \
doc/milter.py \
Milter
# This tag can be used to specify the character encoding of the source files # This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
@@ -655,17 +636,14 @@ IMAGE_PATH =
# by executing (via popen()) the command <filter> <input-file>, where <filter> # by executing (via popen()) the command <filter> <input-file>, where <filter>
# is the value of the INPUT_FILTER tag, and <input-file> is the name of an # is the value of the INPUT_FILTER tag, and <input-file> is the name of an
# input file. Doxygen will then use the output that the filter program writes # input file. Doxygen will then use the output that the filter program writes
# to standard output. # to standard output. If FILTER_PATTERNS is specified, this tag will be
# If FILTER_PATTERNS is specified, this tag will be
# ignored. # ignored.
INPUT_FILTER = INPUT_FILTER =
# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
# basis. # basis. Doxygen will compare the file name with each pattern and apply the
# Doxygen will compare the file name with each pattern and apply the # filter if there is a match. The filters are a list of the form:
# filter if there is a match.
# The filters are a list of the form:
# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further # pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further
# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER # info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER
# is applied to all files. # is applied to all files.
@@ -715,8 +693,7 @@ REFERENCES_RELATION = YES
# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) # If the REFERENCES_LINK_SOURCE tag is set to YES (the default)
# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from # and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will # functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
# link to the source code. # link to the source code. Otherwise they will link to the documentstion.
# Otherwise they will link to the documentation.
REFERENCES_LINK_SOURCE = YES REFERENCES_LINK_SOURCE = YES
@@ -790,11 +767,6 @@ HTML_HEADER =
HTML_FOOTER = HTML_FOOTER =
# If the HTML_TIMESTAMP tag is set to YES then the generated HTML
# documentation will contain the timesstamp.
HTML_TIMESTAMP = NO
# The HTML_STYLESHEET tag can be used to specify a user-defined cascading # The HTML_STYLESHEET tag can be used to specify a user-defined cascading
# style sheet that is used by each HTML page. It can be used to # style sheet that is used by each HTML page. It can be used to
# fine-tune the look of the HTML output. If the tag is left blank doxygen # fine-tune the look of the HTML output. If the tag is left blank doxygen
@@ -903,37 +875,20 @@ QCH_FILE =
# The QHP_NAMESPACE tag specifies the namespace to use when generating # The QHP_NAMESPACE tag specifies the namespace to use when generating
# Qt Help Project output. For more information please see # Qt Help Project output. For more information please see
# http://doc.trolltech.com/qthelpproject.html#namespace # <a href="http://doc.trolltech.com/qthelpproject.html#namespace">Qt Help Project / Namespace</a>.
QHP_NAMESPACE = org.doxygen.Project QHP_NAMESPACE = org.doxygen.Project
# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating
# Qt Help Project output. For more information please see # Qt Help Project output. For more information please see
# http://doc.trolltech.com/qthelpproject.html#virtual-folders # <a href="http://doc.trolltech.com/qthelpproject.html#virtual-folders">Qt Help Project / Virtual Folders</a>.
QHP_VIRTUAL_FOLDER = doc QHP_VIRTUAL_FOLDER = doc
# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to add.
# For more information please see
# http://doc.trolltech.com/qthelpproject.html#custom-filters
QHP_CUST_FILTER_NAME =
# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the custom filter to add.For more information please see
# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters">Qt Help Project / Custom Filters</a>.
QHP_CUST_FILTER_ATTRS =
# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this project's
# filter section matches.
# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes">Qt Help Project / Filter Attributes</a>.
QHP_SECT_FILTER_ATTRS =
# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can # If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can
# be used to specify the location of Qt's qhelpgenerator. # be used to specify the location of Qt's qhelpgenerator.
# If non-empty doxygen will try to run qhelpgenerator on the generated # If non-empty doxygen will try to run qhelpgenerator on the generated
# .qhp file. # .qhp file .
QHG_LOCATION = QHG_LOCATION =
@@ -950,19 +905,21 @@ ENUM_VALUES_PER_LINE = 4
# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
# structure should be generated to display hierarchical information. # structure should be generated to display hierarchical information.
# If the tag value is set to YES, a side panel will be generated # If the tag value is set to FRAME, a side panel will be generated
# containing a tree-like index structure (just like the one that # containing a tree-like index structure (just like the one that
# is generated for HTML Help). For this to work a browser that supports # is generated for HTML Help). For this to work a browser that supports
# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). # JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+,
# Windows users are probably better off using the HTML help feature. # Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are
# probably better off using the HTML help feature. Other possible values
# for this tag are: HIERARCHIES, which will generate the Groups, Directories,
# and Class Hierarchy pages using a tree view instead of an ordered list;
# ALL, which combines the behavior of FRAME and HIERARCHIES; and NONE, which
# disables this behavior completely. For backwards compatibility with previous
# releases of Doxygen, the values YES and NO are equivalent to FRAME and NONE
# respectively.
GENERATE_TREEVIEW = NO GENERATE_TREEVIEW = NO
# By enabling USE_INLINE_TREES, doxygen will generate the Groups, Directories,
# and Class Hierarchy pages using a tree view instead of an ordered list.
USE_INLINE_TREES = NO
# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be
# used to set the initial width (in pixels) of the frame in which the tree # used to set the initial width (in pixels) of the frame in which the tree
# is shown. # is shown.
@@ -977,13 +934,6 @@ TREEVIEW_WIDTH = 250
FORMULA_FONTSIZE = 10 FORMULA_FONTSIZE = 10
# When the SEARCHENGINE tag is enable doxygen will generate a search box for the HTML output. The underlying search engine uses javascript
# and DHTML and should work on any modern browser. Note that when using HTML help (GENERATE_HTMLHELP) or Qt help (GENERATE_QHP)
# there is already a search function so this one should typically
# be disabled.
SEARCHENGINE = NO
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# configuration options related to the LaTeX output # configuration options related to the LaTeX output
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -1060,10 +1010,6 @@ LATEX_BATCHMODE = NO
LATEX_HIDE_INDICES = NO LATEX_HIDE_INDICES = NO
# If LATEX_SOURCE_CODE is set to YES then doxygen will include source code with syntax highlighting in the LaTeX output. Note that which sources are shown also depends on other settings such as SOURCE_BROWSER.
LATEX_SOURCE_CODE = NO
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# configuration options related to the RTF output # configuration options related to the RTF output
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -1200,10 +1146,8 @@ GENERATE_PERLMOD = NO
PERLMOD_LATEX = NO PERLMOD_LATEX = NO
# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be # If the PERLMOD_PRETTY tag is set to YES the Perl module output will be
# nicely formatted so it can be parsed by a human reader. # nicely formatted so it can be parsed by a human reader. This is useful
# This is useful # if you want to understand what is going on. On the other hand, if this
# if you want to understand what is going on.
# On the other hand, if this
# tag is set to NO the size of the Perl module output will be much smaller # tag is set to NO the size of the Perl module output will be much smaller
# and Perl will parse it just the same. # and Perl will parse it just the same.
@@ -1290,11 +1234,9 @@ SKIP_FUNCTION_MACROS = YES
# Optionally an initial location of the external documentation # Optionally an initial location of the external documentation
# can be added for each tagfile. The format of a tag file without # can be added for each tagfile. The format of a tag file without
# this location is as follows: # this location is as follows:
# # TAGFILES = file1 file2 ...
# TAGFILES = file1 file2 ...
# Adding location for the tag files is done as follows: # Adding location for the tag files is done as follows:
# # TAGFILES = file1=loc1 "file2 = loc2" ...
# TAGFILES = file1=loc1 "file2 = loc2" ...
# where "loc1" and "loc2" can be relative or absolute paths or # where "loc1" and "loc2" can be relative or absolute paths or
# URLs. If a location is present for each tag, the installdox tool # URLs. If a location is present for each tag, the installdox tool
# does not have to be run to correct the links. # does not have to be run to correct the links.
@@ -1520,3 +1462,12 @@ GENERATE_LEGEND = YES
# the various graphs. # the various graphs.
DOT_CLEANUP = YES DOT_CLEANUP = YES
#---------------------------------------------------------------------------
# Configuration::additions related to the search engine
#---------------------------------------------------------------------------
# The SEARCHENGINE tag specifies whether or not a search engine should be
# used. If set to NO the values of all tags below this one will be ignored.
SEARCHENGINE = NO
-1
View File
@@ -10,7 +10,6 @@ include testmime.py
include testutils.py include testutils.py
include test.py include test.py
include sample.py include sample.py
include sgmllib.py
include milter-template.py include milter-template.py
include test/* include test/*
include Milter/*.py include Milter/*.py
+43 -76
View File
@@ -8,17 +8,11 @@
# 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.
from __future__ import print_function __version__ = '0.9.7'
__version__ = '1.0.2'
import os import os
import re
import milter import milter
try: import thread
import thread
except:
# libmilter uses posix threads
import _thread as thread
from milter import * from milter import *
from functools import wraps from functools import wraps
@@ -26,6 +20,12 @@ 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,15 +48,6 @@ OPTIONAL_CALLBACKS = {
'header':(P_NR_HDR,P_NOHDRS) 'header':(P_NR_HDR,P_NOHDRS)
} }
MACRO_CALLBACKS = {
'connect': M_CONNECT,
'hello': M_HELO, 'envfrom': M_ENVFROM, 'envrcpt': M_ENVRCPT,
'data': M_DATA, 'eom': M_EOM, 'eoh': M_EOH
}
## @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]
@@ -147,7 +138,6 @@ 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__)
@wraps(func)
def wrapper(self,*args): def wrapper(self,*args):
if func(self,*args) != CONTINUE: if func(self,*args) != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @nocallback' raise RuntimeError('%s return code must be CONTINUE with @nocallback'
@@ -180,21 +170,6 @@ def noreply(func):
wrapper.milter_protocol = nr_mask wrapper.milter_protocol = nr_mask
return wrapper return wrapper
## Function decorator to set macros used in a callback.
# By default, the MTA sends all macros defined for a callback.
# If some or all of these are unused, the bandwidth can be saved
# by listing the ones that are used.
# @since 1.0.2
def symlist(*syms):
if len(syms) > 5:
raise ValueError('@symlist limited to 5 macros by MTA: '+func.__name__)
def setsyms(func):
if func.__name__ not in MACRO_CALLBACKS:
raise ValueError('@symlist applied to non-symlist method: '+func.__name__)
func._symlist = syms
return func
return setsyms
## Disabled action exception. ## Disabled action exception.
# set_flags() can tell the MTA that this application will not use certain # set_flags() can tell the MTA that this application will not use certain
# features (such as CHGFROM). This can also be negotiated for each # features (such as CHGFROM). This can also be negotiated for each
@@ -248,7 +223,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,SETSYMLIST</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 # Application code can also inspect this field to determine
@@ -392,7 +367,7 @@ class Base(object):
for func,(nr,nc) in OPTIONAL_CALLBACKS.items(): for func,(nr,nc) in OPTIONAL_CALLBACKS.items():
func = getattr(klass,func) func = getattr(klass,func)
ca = getattr(func,'milter_protocol',0) ca = getattr(func,'milter_protocol',0)
#print(func,hex(nr),hex(nc),hex(ca)) #print func,hex(nr),hex(nc),hex(ca)
p |= (nr|nc) & ~ca p |= (nr|nc) & ~ca
klass._protocol_mask = p klass._protocol_mask = p
return p return p
@@ -415,11 +390,6 @@ class Base(object):
def negotiate(self,opts): def negotiate(self,opts):
try: try:
self._actions,p,f1,f2 = opts self._actions,p,f1,f2 = opts
for func,stage in MACRO_CALLBACKS.items():
func = getattr(self,func)
syms = getattr(func,'_symlist',None)
if syms is not None:
self.setsymlist(stage,syms)
opts[1] = self._protocol = p & ~self.protocol_mask() opts[1] = self._protocol = p & ~self.protocol_mask()
opts[2] = 0 opts[2] = 0
opts[3] = 0 opts[3] = 0
@@ -442,7 +412,7 @@ 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,
@@ -464,33 +434,17 @@ 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.
# This information can reduce the size of messages received from sendmail, # The <code>Milter.SETSMLIST</code> action flag must be set.
# 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. Hence, this is an advanced # May only be called from negotiate callback.
# feature. Use the @@symlist function decorator to conviently set # @since 0.9.2
# the macros used by a callback. # @param stage the protocol stage to set to macro list for
# @since 0.9.8, previous version was misspelled! # @param macros a string with a space delimited list of macros
# @param stage the protocol stage to set to macro list for, def setsmlist(self,stage,macros):
# one of the M_* constants defined in Milter if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST")
# @param macros space separated and/or lists of strings if type(macros) in (list,tuple):
def setsymlist(self,stage,*macros): macros = ' '.join(macros)
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST") return self._ctx.setsmlist(stage,macros)
if len(macros) > 5:
raise ValueError('setsymlist limited to 5 macros by MTA')
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(b' ')
a += m
except: pass
return self._ctx.setsymlist(stage,b' '.join(a))
# Milter methods which can only be called from eom callback. # Milter methods which can only be called from eom callback.
@@ -754,7 +708,28 @@ 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,rmsock=True): def runmilter(name,socketname,timeout = 0):
# 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)
@@ -785,14 +760,6 @@ def runmilter(name,socketname,timeout = 0,rmsock=True):
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()
+14 -20
View File
@@ -10,9 +10,6 @@
# 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
@@ -46,9 +43,8 @@
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. # Copyright 2001,2002,2003,2004,2005 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.
from __future__ import print_function
import time import time
from Milter.plock import PLock from plock import PLock
class AddrCache(object): class AddrCache(object):
time_format = '%Y%b%d %H:%M:%S %Z' time_format = '%Y%b%d %H:%M:%S %Z'
@@ -76,8 +72,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)
@@ -88,11 +84,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:
@@ -130,13 +126,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 as 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
@@ -148,8 +144,7 @@ class AddrCache(object):
if not ts: return # already permanent if not ts: return # already permanent
self.cache[lsender] = (None,res) self.cache[lsender] = (None,res)
if not res: if not res:
with open(self.fname,'a') as fp: print >>open(self.fname,'a'),sender
print(sender,file=fp)
def __setitem__(self,sender,res): def __setitem__(self,sender,res):
lsender = sender.lower() lsender = sender.lower()
@@ -157,8 +152,7 @@ class AddrCache(object):
self.cache[lsender] = (now,res) self.cache[lsender] = (now,res)
if not res and self.fname: if not res and self.fname:
s = time.strftime(AddrCache.time_format,time.localtime(now)) s = time.strftime(AddrCache.time_format,time.localtime(now))
with open(self.fname,'a') as fp: print >>open(self.fname,'a'),sender,s # log refreshed senders
print(sender,s,file=fp) # log refreshed senders
def __len__(self): def __len__(self):
return len(self.cache) return len(self.cache)
+13 -15
View File
@@ -1,5 +1,4 @@
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
import os.path
class MilterConfigParser(ConfigParser): class MilterConfigParser(ConfigParser):
@@ -21,7 +20,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,dir=''): def getaddrset(self,sect,opt):
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)
@@ -30,14 +29,13 @@ 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()
fname = os.path.join(dir,domain) d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
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,dir=''): def getaddrdict(self,sect,opt):
if not self.has_option(sect,opt): if not self.has_option(sect,opt):
return {} return {}
d = {} d = {}
@@ -45,14 +43,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 = os.path.join(dir,addr[5:]) fname = 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):
+6 -7
View File
@@ -1,7 +1,6 @@
## @package Milter.dns ## @package Milter.dns
# Provide a higher level interface to pydns. # Provide a higher level interface to pydns.
from __future__ import print_function
import DNS import DNS
from DNS import DNSError from DNS import DNSError
@@ -15,9 +14,9 @@ MAX_CNAME = 10
# @return a list of ((name,type),data) tuples # @return a list of ((name,type),data) tuples
def DNSLookup(name, qtype): def DNSLookup(name, qtype):
try: try:
# To be thread safe, we create a fresh DnsRequest with # To be thread safe, we create a fresh DnsRequest with
# each call. It would be more efficient to reuse # each call. It would be more efficient to reuse
# a req object stored in a Session. # a req object stored in a Session.
req = DNS.DnsRequest(name, qtype=qtype) req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req() resp = req.req()
#resp.show() #resp.show()
@@ -98,7 +97,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.lower().rstrip('.') 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:
@@ -121,5 +120,5 @@ if __name__ == '__main__':
import sys import sys
s = Session() s = Session()
for n,t in zip(*[iter(sys.argv[1:])]*2): for n,t in zip(*[iter(sys.argv[1:])]*2):
print(n,t) print n,t
print(s.dns(n,t)) print s.dns(n,t)
+11 -15
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.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
# #
@@ -69,7 +66,6 @@
# a DSN or use a null MAIL FROM with an email address obtained from # a DSN or use a null MAIL FROM with an email address obtained from
# anywhere else. # anywhere else.
# #
from __future__ import print_function
import smtplib import smtplib
import socket import socket
from email.Message import Message from email.Message import Message
@@ -118,17 +114,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 = {}
@@ -136,7 +132,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:
@@ -231,6 +227,6 @@ Subject: Test
Test DSN template Test DSN template
""" """
) )
print(msg.as_string()) print msg.as_string()
# print(send_dsn(f,msg.as_string())) # print send_dsn(f,msg.as_string())
# print(send_dsn(q.s,'mail.example.com',msg.as_string())) # print send_dsn(q.s,'mail.example.com',msg.as_string())
+7 -8
View File
@@ -9,7 +9,6 @@
# wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810) # wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810)
# cbl-sd-02-79.aster.com.do at ('200.88.62.79', 4153) # cbl-sd-02-79.aster.com.do at ('200.88.62.79', 4153)
from __future__ import print_function
import re import re
ip3 = re.compile('[0-9]{1,3}') ip3 = re.compile('[0-9]{1,3}')
@@ -54,11 +53,11 @@ def is_dynip(host,addr):
if host.find(addr) >= 0: return True if host.find(addr) >= 0: return True
if addr.find(':') >= 0: return False # IP6 if addr.find(':') >= 0: return False # IP6
a = addr.split('.') a = addr.split('.')
ia = list(map(int,a)) ia = map(int,a)
h = host h = host
m = ip3.findall(host) m = ip3.findall(host)
if m: if m:
g = list(map(int,m))[:4] g = map(int,m)[:4]
ia3 = (ia[1:],ia[:3]) ia3 = (ia[1:],ia[:3])
if g[-3:] in ia3: return True if g[-3:] in ia3: return True
if g[0] == ia[3] and g[1:3] == ia[:2]: return True if g[0] == ia[3] and g[1:3] == ia[:2]: return True
@@ -69,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
@@ -87,11 +86,11 @@ 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)
if is_dynip(host,ip): if is_dynip(host,ip):
print('%s\t%s DYN' % (ip,host)) print '%s\t%s DYN' % (ip,host)
else: else:
print('%s\t%s' % (ip,host)) print '%s\t%s' % (ip,host)
+8 -55
View File
@@ -1,4 +1,3 @@
from __future__ import print_function
import time import time
import shelve import shelve
import thread import thread
@@ -19,19 +18,13 @@ def quoteAddress(s):
class Record(object): class Record(object):
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' ) __slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
def __init__(self,timeinc=0): def __init__(self):
now = time.time() + timeinc now = time.time()
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):
@@ -42,37 +35,7 @@ class Greylist(object):
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): def check(self,ip,sender,recipient):
"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,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)
@@ -82,15 +45,15 @@ class Greylist(object):
dbp = self.dbp dbp = self.dbp
try: try:
r = dbp[key] r = dbp[key]
now = time.time() + timeinc now = time.time()
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(timeinc) r = Record()
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(timeinc) #r = Record()
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
@@ -100,22 +63,12 @@ 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(timeinc) r = Record()
dbp[key] = r dbp[key] = r
except: except:
r = Record(timeinc) r = Record()
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()
-109
View File
@@ -1,109 +0,0 @@
import time
import logging
import urllib
import sqlite3
try:
import thread
except:
import _thread as 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()
+6 -6
View File
@@ -11,7 +11,7 @@ class PLock(object):
self.basename = basename self.basename = basename
self.fp = None self.fp = None
def lock(self,lockname=None,mode=0o660,strict_perms=False): def lock(self,lockname=None,mode=0660,strict_perms=False):
"Start an update transaction. Return FILE to write new version." "Start an update transaction. Return FILE to write new version."
self.unlock() self.unlock()
if not lockname: if not lockname:
@@ -21,7 +21,7 @@ class PLock(object):
st = os.stat(self.basename) st = os.stat(self.basename)
mode |= st.st_mode mode |= st.st_mode
except OSError: pass except OSError: pass
u = os.umask(0o2) u = os.umask(0002)
try: try:
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode) fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
finally: finally:
@@ -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):
@@ -46,12 +46,12 @@ class PLock(object):
def commit(self,backname=None): def commit(self,backname=None):
"Commit update transaction with optional backup file." "Commit update transaction with optional backup file."
if not self.fp: if not self.fp:
raise IOError("File not locked") raise IOError,"File not locked"
self.fp.close() self.fp.close()
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)
+24 -25
View File
@@ -6,7 +6,6 @@ This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form. and disclaimer are retained in their original form.
""" """
from __future__ import print_function
import struct import struct
#from spf import RE_IP4 #from spf import RE_IP4
import re import re
@@ -49,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):
@@ -81,38 +80,38 @@ def inet_pton(p):
(0, 0, 0, 0, 0, 65535, 258, 772) (0, 0, 0, 0, 0, 65535, 258, 772)
>>> try: inet_pton('::1.2.3.4.5') >>> try: inet_pton('::1.2.3.4.5')
... except ValueError as x: print(x) ... except ValueError,x: print x
::1.2.3.4.5 ::1.2.3.4.5
""" """
if p == '::': if p == '::':
return b'\0'*16 return '\0'*16
s = p s = 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
-238
View File
@@ -1,238 +0,0 @@
## @package Milter.test
# A test framework for milters
from __future__ import print_function
import mime
try:
from io import BytesIO
except:
from StringIO import StringIO as BytesIO
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")
## The MAIL FROM for the current email being fed to the %milter
self._sender = None
## 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
## True if the %milter changed the envelope from.
self._envfromchanged = 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
## The protocol stage for macros returned
self._stage = None
## The macros returned by protocol stage
self._symlist = [ None, None, None, None, None, None, None ]
def log(self,*msg):
for i in msg: print(i,file=self.logfp,end=None)
print(file=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):
stage = self._stage
if stage >= 0:
syms = self._symlist[stage]
if syms is not None and name not in syms:
return None
return self._macros.get(name,None)
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self._bodyreplaced = True
else:
raise IOError("replacebody not called from eom()")
def chgfrom(self,sender,params=None):
if not self._body:
raise IOError("chgfrom not called from eom()")
self.log('chgfrom: sender=%s' % (sender))
self._envfromchanged = True
self._sender = sender
# TODO: write implement quarantine()
def quarantine(self,reason):
raise NotImplemented
# TODO: measure time between milter calls
def progress(self):
pass
# 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 & Milter.SETSYMLIST:
raise DisabledAction("SETSYMLIST")
if self._stage != -1:
raise RuntimeError("setsymlist may only be called from negotiate")
# 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(b' ')
except: pass
a += m
if len(a) > 5:
raise ValueError('setsymlist limited to 5 macros by MTA')
if self._symlist[stage] is not None:
raise ValueError('setsymlist already called for stage:'+stage)
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
self._sender = '<%s>'%sender
msg = mime.message_from_file(fp)
# envfrom
self._stage = Milter.M_ENVFROM
rc = self.envfrom(self._sender)
self._stage = None
if rc != Milter.CONTINUE: return rc
# envrcpt
for rcpt in (rcpt,) + rcpts:
self._stage = Milter.M_ENVRCPT
rc = self.envrcpt('<%s>'%rcpt)
self._stage = None
if rc != Milter.CONTINUE: return rc
# data
self._stage = Milter.M_DATA
rc = self.data()
self._stage = None
if rc != Milter.CONTINUE: return rc
# header
for h,val in msg.items():
rc = self.header(h,val)
if rc != Milter.CONTINUE: return rc
# eoh
self._stage = Milter.M_EOH
rc = self.eoh()
self._stage = None
if rc != Milter.CONTINUE: return rc
# body
header,body = msg.as_bytes().split(b'\n\n',1)
bfp = BytesIO(body)
while 1:
buf = bfp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = BytesIO()
self._stage = Milter.M_EOM
rc = self.eom()
self._stage = None
if self._bodyreplaced:
body = self._body.getvalue()
self._body = BytesIO()
self._body.write(header)
self._body.write(b'\n\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,'rb') as fp:
return self.feedFile(fp,sender,*rcpts)
## Call the connect and helo callbacks.
# The helo callback is not called if connect does not return CONTINUE.
# @param host the hostname passed to the connect callback
# @param helo the hostname passed to the helo callback
# @param ip the IP address passed to the connect callback
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
self._body = None
self._bodyreplaced = False
self._setctx(None)
opts = [ Milter.CURR_ACTS,~0,0,0 ]
self._stage = -1
rc = self.negotiate(opts)
self._stage = Milter.M_CONNECT
rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE:
self._stage = None
self.close()
return rc
self._stage = Milter.M_HELO
rc = self.hello(helo)
self._stage = None
if rc != Milter.CONTINUE:
self.close()
return rc
+23 -19
View File
@@ -5,11 +5,13 @@
import re import re
import struct import struct
import socket import socket
import email.errors import email.Errors
from email.header import decode_header
import email.base64mime import email.base64mime
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from email.Header import decode_header
from binascii import a2b_base64 from binascii import a2b_base64
#import email.Utils
import rfc822
dnsre = re.compile(r'^[a-z][-a-z\d.]+$', re.IGNORECASE) 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)
@@ -54,8 +56,8 @@ if hasattr(socket,'has_ipv6') and socket.has_ipv6:
else: else:
from pyip6 import inet_ntop, inet_pton from pyip6 import inet_ntop, inet_pton
MASK = 0xFFFFFFFF MASK = 0xFFFFFFFFL
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
def cidr(i,n,mask=MASK): def cidr(i,n,mask=MASK):
return ~(mask >> n) & mask & i return ~(mask >> n) & mask & i
@@ -70,7 +72,7 @@ def iniplist(ipaddr,iplist):
True True
>>> iniplist('4.2.2.2',['b.resolvers.Level3.net']) >>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
True True
>>> iniplist('2606:2800:220:1::',['example.com/40']) >>> iniplist('2607:f8b0:4004:801::',['google.com/40'])
True True
>>> iniplist('4.2.2.2',['nothing.example.com']) >>> iniplist('4.2.2.2',['nothing.example.com'])
False False
@@ -93,14 +95,14 @@ 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):
@@ -117,7 +119,7 @@ def iniplist(ipaddr,iplist):
return False return False
## 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. # Additional tricky cases are still broken. Patches welcome.
# #
@@ -133,10 +135,12 @@ def parseaddr(t):
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>') >>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
('God@heaven', 'jeff@spec.org') ('God@heaven', 'jeff@spec.org')
>>> parseaddr('Real Name ((comment)) <addr...@example.com>') >>> parseaddr('Real Name ((comment)) <addr...@example.com>')
('Real Name (comment)', 'addr...@example.com') ('Real Name', 'addr...@example.com')
>>> parseaddr('a(WRONG)@b')
('WRONG', 'a@b')
""" """
#return email.utils.parseaddr(t) #return email.Utils.parseaddr(t)
res = email.utils.parseaddr(t) res = rfc822.parseaddr(t)
# dirty fix for some broken cases # dirty fix for some broken cases
if not res[0]: if not res[0]:
pos = t.find('<') pos = t.find('<')
@@ -145,7 +149,7 @@ def parseaddr(t):
pos1 = addrspec.rfind(':') pos1 = addrspec.rfind(':')
if pos1 > 0: if pos1 > 0:
addrspec = addrspec[pos1+1:] addrspec = addrspec[pos1+1:]
return email.utils.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec)) return rfc822.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
if not res[1]: if not res[1]:
pos = t.find('<') pos = t.find('<')
if pos > 0 and t[-1] == '>': if pos > 0 and t[-1] == '>':
@@ -153,7 +157,7 @@ def parseaddr(t):
pos1 = addrspec.rfind(':') pos1 = addrspec.rfind(':')
if pos1 > 0: if pos1 > 0:
addrspec = addrspec[pos1+1:] addrspec = addrspec[pos1+1:]
return email.utils.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 ## Fix email.base64mime.decode to add any missing padding
@@ -210,18 +214,18 @@ def parse_header(val):
for s,enc in h: for s,enc in h:
if enc: if enc:
try: try:
u.append(s.decode(enc,'replace')) u.append(unicode(s,enc,'replace'))
except LookupError: except LookupError:
u.append(s.decode()) u.append(unicode(s))
else: else:
u.append(s.decode()) u.append(unicode(s))
u = u''.join(u) u = u''.join(u)
for enc in ('us-ascii','iso-8859-1','utf-8'): for enc in ('us-ascii','iso-8859-1','utf-8'):
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 ValueError: pass
except email.errors.HeaderParseError: pass except email.Errors.HeaderParseError: pass
return val return val
+3 -1
View File
@@ -112,6 +112,8 @@ m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
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
@@ -169,7 +171,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@gathman.org) took that and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) 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,2 +1,6 @@
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.
+10 -12
View File
@@ -7,32 +7,30 @@
# 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 event callbacks. # must provide methods corresponding to the libmilter callback events.
# #
# Each callback method returns a code to tell sendmail whether to proceed with # Each event 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. The callback return codes are # earliest possible point.
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
# #
# The Milter.Base class provides default implementations for # The <code>Milter.Base</code> 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 Milter.Milter class provides an alternate default # The <code>Milter.Milter</code> class provides an alternate default
# implementation that logs the main milter callbacks, but otherwise does # implementation that logs the main milter events, but otherwise does nothing.
# nothing. It is provided for compatibility. # It is provided for compatibility.
# #
# The mime 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.
# #
+17 -85
View File
@@ -3,65 +3,10 @@
## @package milter ## @package milter
# #
# A thin wrapper around libmilter. Most users will not import # A thin wrapper around libmilter.
# milter directly, but will instead import Milter and subclass
# Milter.Base. This module gives you ultimate low level control
# from python.
# #
## Continue processing the current connection, message, or recipient. ## Hold context for a milter connection.
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>
@@ -119,8 +64,8 @@ class milterContext(object):
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.
@@ -138,27 +83,19 @@ 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.
# The default is TEMPFAIL. You should not depend on this handler. Your # Must be one of TEMPFAIL,REJECT,CONTINUE
# 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 in the future (perhaps # it before registering. I may move all the callbacks
# keeping the set functions for compatibility). Note that Milter.Base # in the future (perhaps keeping the set functions for compatibility).
# automatically maps all callbacks to member functions, and negotiates which # @param name the milter name by which the MTA finds us
# 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="milter_api/xxfi_negotiate.html">
# xxfi_negotiate</a> callback, called to negotiate supported # xxfi_negotiate</a> callback, called to negotiate supported
@@ -212,21 +149,16 @@ 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. milter.setconn() will not fail with # group or world writable directory.
# 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>
# milter.setconn('unix:/var/run/pythonfilter') # a named pipe # setconn('unix:/var/run/pythonfilter')
# milter.setconn('local:/var/run/pythonfilter') # a named pipe # setconn('inet:8800') # listen on ANY interface
# milter.setconn('inet:8800') # listen on ANY interface # setconn('inet:7871@@publichost') # listen on a specific interface
# milter.setconn('inet:7871@@publichost') # listen on a specific interface # setconn('inet6:8020')
# 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.
+6 -3
View File
@@ -4,11 +4,14 @@ web:
rsync -ravKk doc/html/ spidey2.bmsi.com:/Public/pymilter rsync -ravKk doc/html/ spidey2.bmsi.com:/Public/pymilter
cd doc/html; zip -r ../../doc . cd doc/html; zip -r ../../doc .
VERSION=1.0.2 VERSION=0.9.6
CVSTAG=pymilter-0_9_6
PKG=pymilter-$(VERSION) PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz SRCTAR=$(PKG).tar.gz
$(SRCTAR): $(SRCTAR):
git archive --format=tar.gz --prefix=$(PKG)/ -o $(SRCTAR) $(PKG) cvs export -r$(CVSTAG) -d $(PKG) pymilter
tar cvfz $(PKG).tar.gz $(PKG)
rm -r $(PKG)
gittar: $(SRCTAR) cvstar: $(SRCTAR)
+3 -4
View File
@@ -2,7 +2,6 @@
# Internal is defined as using one of a list of internal top level domains. # 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. # This code is open-source on the same terms as Python.
from __future__ import print_function
import Milter import Milter
import time import time
import sys import sys
@@ -14,7 +13,7 @@ internal_tlds = ["corp", "personal"]
# True if internal, False otherwise # True if internal, False otherwise
def is_internal(hostname): def is_internal(hostname):
components = hostname.split(".") components = hostname.split(".")
return components.pop() in internal_tlds return components.pop() in internal_tlds:
# Determine if internal and external hosts are mixed based on a list # Determine if internal and external hosts are mixed based on a list
# of hostnames # of hostnames
@@ -69,12 +68,12 @@ def main():
timeout = 600 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 = NoMixMilter Milter.factory = NoMixMilter
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("nomixfilter",socketname,timeout) Milter.runmilter("nomixfilter",socketname,timeout)
logq.put(None) logq.put(None)
bt.join() bt.join()
print("%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')) print "%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+8 -16
View File
@@ -7,12 +7,8 @@
## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message. ## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
## You can also add/del recipients, replacebody, add/del headers, etc. ## You can also add/del recipients, replacebody, add/del headers, etc.
from __future__ import print_function
import Milter import Milter
try: import StringIO
from StringIO import StringIO
except:
from io import StringIO
import time import time
import email import email
import sys import sys
@@ -75,10 +71,7 @@ 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 self.fp = StringIO.StringIO()
# must use addheader, chgheader, replacebody to change the message
# on the MTA.
self.fp = 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()))
return Milter.CONTINUE return Milter.CONTINUE
@@ -111,6 +104,7 @@ 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')
@@ -135,12 +129,10 @@ def background():
t = logq.get() t = logq.get()
if not t: break if not t: break
msg,id,ts = t msg,id,ts = t
print("%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id), print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
end=None)
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ... # 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print(i,end=None) for i in msg: print i,
print() print
sys.stdout.flush()
## === ## ===
@@ -155,12 +147,12 @@ def main():
flags += Milter.ADDRCPT flags += Milter.ADDRCPT
flags += Milter.DELRCPT flags += Milter.DELRCPT
Milter.set_flags(flags) # tell Sendmail which features we use Milter.set_flags(flags) # tell Sendmail which features we use
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) logq.put(None)
bt.join() 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__":
main() main()
-174
View File
@@ -1,174 +0,0 @@
diff --git a/miltermodule.c b/miltermodule.c
index aa10a08..4d5a93d 100644
--- a/miltermodule.c
+++ b/miltermodule.c
@@ -343,7 +343,7 @@ static struct MilterCallback {
{ NULL , NULL }
};
-staticforward struct smfiDesc description; /* forward declaration */
+static struct smfiDesc description; /* forward declaration */
static PyObject *MilterError;
/* The interpreter instance that called milter.main */
@@ -355,7 +355,7 @@ typedef struct {
static milter_Diag diag;
-staticforward PyTypeObject milter_ContextType;
+static PyTypeObject milter_ContextType;
typedef struct {
PyObject_HEAD
@@ -700,7 +700,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
result = PyEval_CallObject(cb, arglist);
Py_DECREF(arglist);
if (result == NULL) return _report_exception(self);
- if (!PyInt_Check(result)) {
+ if (!PyLong_Check(result)) {
const struct MilterCallback *p;
const char *cbname = "milter";
char buf[40];
@@ -715,7 +715,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
PyErr_SetString(MilterError,buf);
return _report_exception(self);
}
- retval = PyInt_AS_LONG(result);
+ retval = PyLong_AS_LONG(result);
Py_DECREF(result);
_release_thread(self->t);
return retval;
@@ -732,7 +732,7 @@ makeipaddr(struct sockaddr_in *addr) {
sprintf(buf, "%d.%d.%d.%d",
(int) (x>>24) & 0xff, (int) (x>>16) & 0xff,
(int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff);
- return PyString_FromString(buf);
+ return PyUnicode_FromString(buf);
}
#ifdef HAVE_IPV6_SUPPORT
@@ -740,8 +740,8 @@ static PyObject *
makeip6addr(struct sockaddr_in6 *addr) {
char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */
const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf);
- if (s) return PyString_FromString(s);
- return PyString_FromString("inet6:unknown");
+ if (s) return PyUnicode_FromString(s);
+ return PyUnicode_FromString("inet6:unknown");
}
#endif
@@ -832,7 +832,7 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
for (i=0;i<count;i++) {
/* There's some error checking performed in do_mkvalue() for a string */
/* that's not currently done here - it probably should be */
- PyObject *o = PyString_FromStringAndSize(argv[i], strlen(argv[i]));
+ PyObject *o = PyUnicode_FromStringAndSize(argv[i], strlen(argv[i]));
if (o == NULL) { /* out of memory */
Py_DECREF(arglist);
return _report_exception(self);
@@ -889,7 +889,7 @@ milter_wrap_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) {
c = _get_context(ctx);
if (!c) return SMFIS_TEMPFAIL;
/* Unclear whether this should be s#, z#, or t# */
- arglist = Py_BuildValue("(Os#)", c, bodyp, bodylen);
+ arglist = Py_BuildValue("(Oy#)", c, bodyp, bodylen);
return _generic_wrapper(c, body_callback, arglist);
}
@@ -963,7 +963,7 @@ milter_wrap_negotiate(SMFICTX *ctx,
int i;
for (i = 0; i < 4; ++i) {
*pa[i] = (i <= len)
- ? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
+ ? PyLong_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
: fa[i];
}
if (PyErr_Occurred()) {
@@ -1551,11 +1551,6 @@ static PyMethodDef context_methods[] = {
{ 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 */
"pythonfilter",
SMFI_VERSION,
@@ -1604,14 +1599,13 @@ static PyMethodDef milter_methods[] = {
};
static PyTypeObject milter_ContextType = {
- PyObject_HEAD_INIT(&PyType_Type)
- 0,
- "milterContext",
+ PyVarObject_HEAD_INIT(&PyType_Type,0)
+ "milter.Context",
sizeof(milter_ContextObject),
0,
milter_Context_dealloc, /* tp_dealloc */
0, /* tp_print */
- milter_Context_getattr, /* tp_getattr */
+ 0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
0, /* tp_repr */
@@ -1625,6 +1619,13 @@ static PyTypeObject milter_ContextType = {
0, /* tp_setattro */
0, /* tp_as_buffer */
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 const char milter_documentation[] =
@@ -1634,17 +1635,31 @@ Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
See <sendmailsource>/libmilter/README for details on setting it up.\n";
static void setitem(PyObject *d,const char *name,long val) {
- PyObject *v = PyInt_FromLong(val);
+ PyObject *v = PyLong_FromLong(val);
PyDict_SetItemString(d,name,v);
Py_DECREF(v);
}
-void
-initmilter(void) {
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "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;
- m = Py_InitModule4("milter", milter_methods, milter_documentation,
- (PyObject*)NULL, PYTHON_API_VERSION);
+ if (PyType_Ready(&milter_ContextType) < 0)
+ return NULL;
+
+ m = PyModule_Create(&moduledef);
+ if (m == NULL) return NULL;
d = PyModule_GetDict(m);
MilterError = PyErr_NewException("milter.error", NULL, NULL);
PyDict_SetItemString(d,"error", MilterError);
@@ -1710,4 +1725,5 @@ initmilter(void) {
setitem(d,"DISCARD", SMFIS_DISCARD);
setitem(d,"ACCEPT", SMFIS_ACCEPT);
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
+ return m;
}
+89 -97
View File
@@ -1,6 +1,6 @@
/* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org) /* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org)
* Portions Copyright (C) 2001,2002,2003,2004,2005,2006,2007 * Portions Copyright (C) 2001,2002,2003,2004,2005,2006,2007
* Stuart Gathman (stuart@gathman.org) * Stuart Gathman (stuart@bmsi.com)
* *
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the * under the terms of the GNU General Public License as published by the
@@ -35,18 +35,6 @@ $ 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.
@@ -282,7 +270,7 @@ $ python setup.py help
* published. Unfortunately I know of no good way to do this * published. Unfortunately I know of no good way to do this
* other than with OS-specific tests. * other than with OS-specific tests.
*/ */
#if defined(__FreeBSD__) || defined(__linux__) #if defined(__FreeBSD_kernel__) || defined(__linux__)
#define HAVE_IPV6_RFC2553 #define HAVE_IPV6_RFC2553
#include <arpa/inet.h> #include <arpa/inet.h>
#endif #endif
@@ -343,7 +331,7 @@ static struct MilterCallback {
{ NULL , NULL } { NULL , NULL }
}; };
staticforward struct smfiDesc description; /* forward declaration */ 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 */
@@ -355,7 +343,7 @@ typedef struct {
static milter_Diag diag; static milter_Diag diag;
staticforward PyTypeObject milter_ContextType; static PyTypeObject milter_ContextType;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
@@ -452,7 +440,7 @@ _thread_return(PyThreadState *t,int val,char *errstr) {
return _generic_return(val,errstr); return _generic_return(val,errstr);
} }
static const char milter_set_flags__doc__[] = static 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\
@@ -493,7 +481,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
return Py_None; return Py_None;
} }
static const char milter_set_connect_callback__doc__[] = static 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\
@@ -520,7 +508,7 @@ milter_set_connect_callback(PyObject *self, PyObject *args) {
"O:set_connect_callback", &connect_callback); "O:set_connect_callback", &connect_callback);
} }
static const char milter_set_helo_callback__doc__[] = static 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\
@@ -531,7 +519,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 const char milter_set_envfrom_callback__doc__[] = static 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\
@@ -544,7 +532,7 @@ milter_set_envfrom_callback(PyObject *self, PyObject *args) {
&envfrom_callback); &envfrom_callback);
} }
static const char milter_set_envrcpt_callback__doc__[] = static 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\
@@ -557,7 +545,7 @@ milter_set_envrcpt_callback(PyObject *self, PyObject *args) {
&envrcpt_callback); &envrcpt_callback);
} }
static const char milter_set_header_callback__doc__[] = static 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\
@@ -570,7 +558,7 @@ milter_set_header_callback(PyObject *self, PyObject *args) {
&header_callback); &header_callback);
} }
static const char milter_set_eoh_callback__doc__[] = static 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";
@@ -580,7 +568,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 const char milter_set_body_callback__doc__[] = static 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\
@@ -593,7 +581,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 const char milter_set_eom_callback__doc__[] = static 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\
@@ -606,7 +594,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 const char milter_set_abort_callback__doc__[] = static 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\
@@ -620,7 +608,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 const char milter_set_close_callback__doc__[] = static 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\
@@ -633,7 +621,7 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
static int exception_policy = SMFIS_TEMPFAIL; static int exception_policy = SMFIS_TEMPFAIL;
static const char milter_set_exception_policy__doc__[] = static 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";
@@ -644,8 +632,7 @@ 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_REJECT: case SMFIS_TEMPFAIL: case SMFIS_CONTINUE:
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;
@@ -660,30 +647,23 @@ _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", untrapped_msg); smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure");
return SMFIS_REJECT; return SMFIS_REJECT;
case SMFIS_TEMPFAIL: case SMFIS_TEMPFAIL:
smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg); smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure");
return SMFIS_TEMPFAIL; return SMFIS_TEMPFAIL;
} }
return exception_policy; return SMFIS_CONTINUE;
} }
/* 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;
} }
@@ -700,7 +680,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 (!PyInt_Check(result)) { if (!PyLong_Check(result)) {
const struct MilterCallback *p; const struct MilterCallback *p;
const char *cbname = "milter"; const char *cbname = "milter";
char buf[40]; char buf[40];
@@ -715,7 +695,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 = PyInt_AS_LONG(result); retval = PyLong_AS_LONG(result);
Py_DECREF(result); Py_DECREF(result);
_release_thread(self->t); _release_thread(self->t);
return retval; return retval;
@@ -732,7 +712,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 PyString_FromString(buf); return PyUnicode_FromString(buf);
} }
#ifdef HAVE_IPV6_SUPPORT #ifdef HAVE_IPV6_SUPPORT
@@ -740,8 +720,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 PyString_FromString(s); if (s) return PyUnicode_FromString(s);
return PyString_FromString("inet6:unknown"); return PyUnicode_FromString("inet6:unknown");
} }
#endif #endif
@@ -832,7 +812,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 = PyString_FromStringAndSize(argv[i], strlen(argv[i])); PyObject *o = PyUnicode_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);
@@ -963,7 +943,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)
? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i)) ? PyLong_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
: fa[i]; : fa[i];
} }
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
@@ -1011,7 +991,7 @@ milter_wrap_close(SMFICTX *ctx) {
return r; return r;
} }
static const char milter_register__doc__[] = static 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.";
@@ -1056,7 +1036,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 const char milter_opensocket__doc__[] = static 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.";
@@ -1069,7 +1049,7 @@ milter_opensocket(PyObject *self, PyObject *args) {
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket"); return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
} }
static const char milter_main__doc__[] = static 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().";
@@ -1093,7 +1073,7 @@ milter_main(PyObject *self, PyObject *args) {
return o; return o;
} }
static const char milter_setdbg__doc__[] = static 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.";
@@ -1104,7 +1084,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 const char milter_setbacklog__doc__[] = static 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.";
@@ -1116,7 +1096,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 const char milter_settimeout__doc__[] = static 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.";
@@ -1129,7 +1109,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 const char milter_setconn__doc__[] = static 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\
@@ -1149,7 +1129,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 const char milter_stop__doc__[] = static 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.";
@@ -1162,7 +1142,7 @@ milter_stop(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_stop(), "cannot stop"); return _thread_return(t,smfi_stop(), "cannot stop");
} }
static const char milter_getdiag__doc__[] = static 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.";
@@ -1172,7 +1152,7 @@ milter_getdiag(PyObject *self, PyObject *args) {
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel); return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
} }
static const char milter_getversion__doc__[] = static 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 *
@@ -1186,7 +1166,7 @@ milter_getversion(PyObject *self, PyObject *args) {
return Py_BuildValue("(kkk)", major,minor,patch); return Py_BuildValue("(kkk)", major,minor,patch);
} }
static const char milter_getsymval__doc__[] = static 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.";
@@ -1201,7 +1181,7 @@ milter_getsymval(PyObject *self, PyObject *args) {
return Py_BuildValue("s", smfi_getsymval(ctx, str)); return Py_BuildValue("s", smfi_getsymval(ctx, str));
} }
static const char milter_setreply__doc__[] = static 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\
@@ -1265,7 +1245,7 @@ milter_setreply(PyObject *self, PyObject *args) {
"cannot set reply"); "cannot set reply");
} }
static const char milter_addheader__doc__[] = static 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\
@@ -1302,7 +1282,7 @@ milter_addheader(PyObject *self, PyObject *args) {
} }
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
static const char milter_chgfrom__doc__[] = static 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\
@@ -1325,7 +1305,7 @@ milter_chgfrom(PyObject *self, PyObject *args) {
} }
#endif #endif
static const char milter_chgheader__doc__[] = static 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\
@@ -1353,7 +1333,7 @@ milter_chgheader(PyObject *self, PyObject *args) {
"cannot change header"); "cannot change header");
} }
static const char milter_addrcpt__doc__[] = static 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\
@@ -1383,7 +1363,7 @@ milter_addrcpt(PyObject *self, PyObject *args) {
return _thread_return(t,rc, "cannot add recipient"); return _thread_return(t,rc, "cannot add recipient");
} }
static const char milter_delrcpt__doc__[] = static 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.";
@@ -1401,7 +1381,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 const char milter_replacebody__doc__[] = static 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\
@@ -1423,7 +1403,7 @@ milter_replacebody(PyObject *self, PyObject *args) {
(unsigned char *)bodyp, bodylen), "cannot replace message body"); (unsigned char *)bodyp, bodylen), "cannot replace message body");
} }
static const char milter_setpriv__doc__[] = static 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\
@@ -1449,7 +1429,7 @@ milter_setpriv(PyObject *self, PyObject *args) {
return old; return old;
} }
static const char milter_getpriv__doc__[] = static 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\
@@ -1467,7 +1447,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
} }
#ifdef SMFIF_QUARANTINE #ifdef SMFIF_QUARANTINE
static const char milter_quarantine__doc__[] = static 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.";
@@ -1488,7 +1468,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
#endif #endif
#ifdef SMFIR_PROGRESS #ifdef SMFIR_PROGRESS
static const char milter_progress__doc__[] = static 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.";
@@ -1505,23 +1485,23 @@ milter_progress(PyObject *self, PyObject *args) {
} }
#endif #endif
#ifdef SMFIF_SETSYMLIST #ifdef SMFIF_SETSMLIST
static const char milter_setsymlist__doc__[] = static char milter_setsmlist__doc__[] =
"setsymlist(stage,macrolist) -> None\n\ "setsmlist(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_setsymlist(PyObject *self, PyObject *args) { milter_setsmlist(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:setsymlist",&stage, &smlist)) return NULL; if (!PyArg_ParseTuple(args, "is:setsmlist",&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_setsymlist(ctx,stage,smlist), return _thread_return(t,smfi_setsmlist(ctx,stage,smlist),
"cannot set macro list"); "cannot set macro list");
} }
#endif #endif
@@ -1545,17 +1525,12 @@ 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_SETSYMLIST #ifdef SMFIF_SETSMLIST
{ "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__doc__}, { "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__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,
@@ -1604,14 +1579,13 @@ static PyMethodDef milter_methods[] = {
}; };
static PyTypeObject milter_ContextType = { static PyTypeObject milter_ContextType = {
PyObject_HEAD_INIT(&PyType_Type) PyVarObject_HEAD_INIT(&PyType_Type,0)
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 */
milter_Context_getattr, /* tp_getattr */ 0, /* tp_getattr */
0, /* tp_setattr */ 0, /* tp_setattr */
0, /* tp_compare */ 0, /* tp_compare */
0, /* tp_repr */ 0, /* tp_repr */
@@ -1625,26 +1599,43 @@ 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 const char milter_documentation[] = static 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 = PyInt_FromLong(val); PyObject *v = PyLong_FromLong(val);
PyDict_SetItemString(d,name,v); PyDict_SetItemString(d,name,v);
Py_DECREF(v); Py_DECREF(v);
} }
void static struct PyModuleDef moduledef = {
initmilter(void) { PyModuleDef_HEAD_INIT,
"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 = Py_InitModule4("milter", milter_methods, milter_documentation, m = PyModule_Create(&moduledef);
(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);
@@ -1669,15 +1660,15 @@ initmilter(void) {
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
setitem(d,"CHGFROM",SMFIF_CHGFROM); setitem(d,"CHGFROM",SMFIF_CHGFROM);
#endif #endif
#ifdef SMFIF_SETSYMLIST #ifdef SMFIF_SETSMLIST
setitem(d,"SETSYMLIST",SMFIF_SETSYMLIST); setitem(d,"SETSMLIST",SMFIF_SETSMLIST);
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);
@@ -1710,4 +1701,5 @@ initmilter(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;
} }
+83 -92
View File
@@ -1,7 +1,4 @@
# $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.
# #
@@ -93,33 +90,26 @@
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. # Copyright 2001,2002,2003,2004,2005 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.
from __future__ import print_function import StringIO
try:
from io import BytesIO, StringIO
except:
from StringIO import StringIO
BytesIO = StringIO
import socket import socket
import Milter import Milter
import zipfile import zipfile
import email import email
from email.message import Message import email.Message
try: from email.Message import Message
from email.generator import BytesGenerator from email.Generator import Generator
from email import message_from_binary_file from email.Utils import quote
except: from email import Utils
from email.generator import Generator as BytesGenerator from email.Parser import Parser
from email import message_from_file as message_from_binary_file from email import Errors
from email.utils import quote
if not getattr(Message,'as_bytes',None): from types import ListType,StringType
Message.as_bytes = Message.as_string
## Return a list of filenames in a zip file. ## Return a list of filenames in a zip file.
# Embedded zip files are recursively expanded. # Embedded zip files are recursively expanded.
def zipnames(txt): def zipnames(txt):
fp = BytesIO(txt) fp = StringIO.StringIO(txt)
zipf = zipfile.ZipFile(fp,'r') zipf = zipfile.ZipFile(fp,'r')
names = [] names = []
for nm in zipf.namelist(): for nm in zipf.namelist():
@@ -130,44 +120,46 @@ def zipnames(txt):
## Fix multipart handling in email.Generator. ## Fix multipart handling in email.Generator.
# #
class MimeGenerator(BytesGenerator): class MimeGenerator(Generator):
def _dispatch(self, msg): def _dispatch(self, msg):
# Get the Content-Type: for the message, then try to dispatch to # Get the Content-Type: for the message, then try to dispatch to
# self._handle_<maintype>_<subtype>(). If there's no handler for the # self._handle_<maintype>_<subtype>(). If there's no handler for the
# 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:
BytesGenerator._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
def _unquotevalue(value): def _unquotevalue(value):
if isinstance(value, tuple): if isinstance(value, TupleType):
return value[0], value[1], unquote(value[2]) return value[0], value[1], unquote(value[2])
else: else:
return unquote(value) return unquote(value)
#email.Message._unquotevalue = _unquotevalue #email.Message._unquotevalue = _unquotevalue
from email.message import _parseparam from email.Message import _parseparam
## Enhance email.message.Message ## Enhance email.Message
# #
# Tracks modifications to headers of body or any part independently. # Tracks modifications to headers of body or any part independently.
@@ -208,23 +200,23 @@ class MimeMessage(Message):
interpret as a name - and hence decide to execute this message.""" interpret as a name - and hence decide to execute this message."""
names = [] names = []
for attr,val in self._get_params_preserve([],'content-type'): for attr,val in self._get_params_preserve([],'content-type'):
if isinstance(val, tuple): 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):
@@ -243,9 +235,9 @@ class MimeMessage(Message):
g = MimeGenerator(file) g = MimeGenerator(file)
g.flatten(self,unixfrom=unixfrom) g.flatten(self,unixfrom=unixfrom)
def as_bytes(self, unixfrom=False): def as_string(self, unixfrom=False):
"Return the entire formatted message as a string." "Return the entire formatted message as a string."
fp = BytesIO() fp = StringIO.StringIO()
self.dump(fp,unixfrom=unixfrom) self.dump(fp,unixfrom=unixfrom)
return fp.getvalue() return fp.getvalue()
@@ -295,18 +287,18 @@ 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
def message_from_file(fp): def message_from_file(fp):
msg = message_from_binary_file(fp,MimeMessage) msg = email.message_from_file(fp,MimeMessage)
for part in msg.walk(): for part in msg.walk():
part.modified = False part.modified = False
assert not msg.ismodified() assert not msg.ismodified()
@@ -317,7 +309,7 @@ ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,inf,ins,isp,js,
jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc, jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc,
wsf,wsh wsf,wsh
""".split()) """.split())
bad_extensions = ['.' + x for x in extlist.split(',')] bad_extensions = map(lambda x:'.' + x,extlist.split(','))
def check_ext(name): def check_ext(name):
"Check a name for dangerous Winblows extensions." "Check a name for dangerous Winblows extensions."
@@ -341,7 +333,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:
@@ -356,6 +348,8 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
msg["Content-Type"] = "text/plain; name="+name msg["Content-Type"] = "text/plain; name="+name
return Milter.CONTINUE return Milter.CONTINUE
import email.Iterators
def check_attachments(msg,check): def check_attachments(msg,check):
"""Scan attachments. """Scan attachments.
msg MimeMessage msg MimeMessage
@@ -386,7 +380,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
@@ -401,18 +395,18 @@ class _defang:
# emulate old defang function # emulate old defang function
defang = _defang() defang = _defang()
from sgmllib import SGMLParser as HTMLParser import sgmllib
import re import re
declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*') declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*')
declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*') declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*')
class SGMLFilter(HTMLParser): class SGMLFilter(sgmllib.SGMLParser):
"""Parse HTML and pass through all constructs unchanged. It is intended for """Parse HTML and pass through all constructs unchanged. It is intended for
derived classes to implement exceptional processing for selected cases. derived classes to implement exceptional processing for selected cases.
""" """
def __init__(self,out): def __init__(self,out):
HTMLParser.__init__(self) sgmllib.SGMLParser.__init__(self)
self.out = out self.out = out
def handle_comment(self,comment): def handle_comment(self,comment):
@@ -443,7 +437,7 @@ class SGMLFilter(HTMLParser):
self.out.write("<!%s>" % data) self.out.write("<!%s>" % data)
def write(self,buf): def write(self,buf):
"Act like a writer. Why doesn't HTMLParser do this by default?" "Act like a writer. Why doesn't SGMLParser do this by default?"
self.feed(buf) self.feed(buf)
# Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft # Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft
@@ -456,25 +450,25 @@ class SGMLFilter(HTMLParser):
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
@@ -486,14 +480,11 @@ class HTMLScriptFilter(SGMLFilter):
self.modified = False self.modified = False
self.msg = "<!-- WARNING: embedded script removed -->" self.msg = "<!-- WARNING: embedded script removed -->"
def start_script(self,unused): def start_script(self,unused):
#print('beg script',unused)
self.ignoring += 1 self.ignoring += 1
self.modified = True self.modified = True
self.out.write(self.msg)
def end_script(self): def end_script(self):
#print('end script')
self.ignoring -= 1 self.ignoring -= 1
if not self.ignoring:
self.out.write(self.msg)
def handle_data(self,data): def handle_data(self,data):
if not self.ignoring: SGMLFilter.handle_data(self,data) if not self.ignoring: SGMLFilter.handle_data(self,data)
def handle_comment(self,comment): def handle_comment(self,comment):
@@ -506,16 +497,16 @@ 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() out = StringIO.StringIO()
htmlfilter = HTMLScriptFilter(out) htmlfilter = HTMLScriptFilter(out)
try: try:
htmlfilter.write(msg.get_payload(decode=True).decode()) htmlfilter.write(msg.get_payload(decode=True))
htmlfilter.close() htmlfilter.close()
#except sgmllib.SGMLParseError: #except sgmllib.SGMLParseError:
except: except:
mimetools.copyliteral(msg.get_payload(),open('debug.out','wb')) #mimetools.copyliteral(msg.get_payload(),open('debug.out','w')
htmlfilter.close() htmlfilter.close()
hostname = socket.gethostname() hostname = socket.gethostname()
msg.set_payload( msg.set_payload(
@@ -538,14 +529,14 @@ if __name__ == '__main__':
def _list_attach(msg): def _list_attach(msg):
t = msg.get_content_type() t = msg.get_content_type()
p = msg.get_payload(decode=True) p = msg.get_payload(decode=True)
print(msg.get_filename(),msg.get_content_type(),type(p)) print msg.get_filename(),msg.get_content_type(),type(p)
msg = msg.get_submsg() msg = msg.get_submsg()
if isinstance(msg,Message): if isinstance(msg,Message):
return check_attachments(msg,_list_attach) return check_attachments(msg,_list_attach)
return Milter.CONTINUE return Milter.CONTINUE
for fname in sys.argv[1:]: for fname in sys.argv[1:]:
fp = open(fname,'rb') fp = open(fname)
msg = message_from_file(fp) msg = message_from_file(fp)
email.iterators._structure(msg) email.Iterators._structure(msg)
check_attachments(msg,_list_attach) check_attachments(msg,_list_attach)
-6
View File
@@ -1,6 +0,0 @@
Check Description Justification
E111 req indent 4 Creates more continuation lines
E114 req indent 4 cmnt Same
E231 req space after , makes calls like print() harder to read
E266 no ## Required by Doxygen
W291 trailing spaces in cmnt Needed for space preserving para reformat
-5
View File
@@ -1,5 +0,0 @@
#!/bin/sh
ignore=`awk -F\\\\t '{ print $1 }' pep8.dat | tail -n +2`
a=(${ignore})
list=$(echo "${a[@]}"|tr '[ ]' '[,]')
echo python3 -m pep8 --ignore="$list" $@
-197
View File
@@ -1,197 +0,0 @@
%if 0%{?rhel} == 7
%define pythonbase python34
%else
%define pythonbase python3
%endif
%define __python python3
%define libdir %{_libdir}/pymilter
%{!?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
Name: %{pythonbase}-pymilter
Version: 1.0.2
Release: 1%{dist}
Source: https://github.com/sdgathman/pymilter/archive/pymilter-%{version}.tar.gz
Source1: pymilter.te
# Patch miltermodule to python3
# FIXME: replace with reverse patch at some point (make py3 the default)
Patch: milter.patch
License: GPLv2+
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html
# python-2.6.4 gets RuntimeError: not holding the import lock
Requires: %{pythonbase} >= 2.6.5, sendmail-milter >= 8.13
%if 0%{?fedora} >= 23
# Need python2.6 specific pydns, not the version for system python
Recommends: %{pythonbase}-pydns
%endif
# Needed for callbacks, not a core function but highly useful for milters
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13
%description
This is a python extension module to enable python scripts to
attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV.
%package selinux
Summary: SELinux policy module for pymilter
Group: System Environment/Base
Requires: policycoreutils, selinux-policy, %{name}
BuildRequires: policycoreutils, checkpolicy
%if 0%{?epel} >= 6
BuildRequires: policycoreutils-python
%else
BuildRequires: policycoreutils-python-utils
%endif
%description selinux
SELinux policy module for using pymilter with sendmail with selinux enforcing
%prep
%setup -q -n pymilter-%{version}
%patch -p1 -b .py3
cp %{SOURCE1} pymilter.te
%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
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install --root=$RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
mkdir -p $RPM_BUILD_ROOT%{libdir}
# install selinux modules
mkdir -p %{buildroot}%{_datadir}/selinux/targeted
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%files
%defattr(-,root,root,-)
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%{python_sitearch}/*
%{libdir}
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%files selinux
%doc pymilter.te
%{_datadir}/selinux/targeted/*
%clean
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
* Tue Dec 13 2016 Stuart Gathman <stuart@gathman.org> 1.0.2-1
- Fix the last setsymlist misspelling. Support in test framework and tests.
- Add @symlist decorator.
- Change body callback and a few other APIs to use bytes instead of str.
* Tue Sep 20 2016 Stuart Gathman <stuart@gathman.org> 1.0.1-1
- Support python3
* 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
- 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
- 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
* Tue Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
- Handle IP6 in Milter.utils.iniplist()
- python-2.6
* Thu Jul 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.3-1
- Handle source route in Milter.utils.parse_addr()
- Fix default arg in chgfrom.
- Disable negotiate callback for libmilter < 8.14.3 (1,0,1)
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-3
- Change result of @noreply callbacks to NOREPLY when so negotiated.
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-2
- Cache callback negotiation
* Thu May 28 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-1
- Add new callback support: data,negotiate,unknown
- Auto-negotiate protocol steps
* Thu Feb 05 2009 Stuart Gathman <stuart@bmsi.com> 0.9.1-1
- Fix missing address of optional param to addrcpt
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-4
- Stop using INSTALLED_FILES to make Fedora happy
- Remove config flag from start.sh glue
- Own /var/log/milter
- Use _localstatedir
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-2
- Changes to meet Fedora standards
* Mon Nov 24 2008 Stuart Gathman <stuart@bmsi.com> 0.9.0-1
- Split pymilter into its own CVS module
- Support chgfrom and addrcpt_par
- Support NS records in Milter.dns
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
- /var/run/milter directory must be owned by mail
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
- improved parsing into email and fullname (still 2 self test failures)
- implement no-DSN CBV, reduce full DSNs
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
- Use ifarch hack to build milter and milter-spf packages as noarch
- Remove spf dependency from dsn.py, add dns.py
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
- move AddrCache, parse_addr, iniplist to Milter package
- move parse_header to Milter.utils
- fix plock for missing source and can't change owner/group
- split out pymilter and pymilter-spf packages
- move milter apps to /usr/lib/pymilter
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
- SPF moved to pyspf RPM
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
- Support CBV timeout
+32 -78
View File
@@ -1,32 +1,22 @@
%define __python python2 %define __python python2.6
%if 0%{?rhel} == 6 %define pythonbase python26
%define pythonbase python
%else
%define pythonbase python2
%endif
%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: 1.0.2 Version: 0.9.6
Release: 1%{dist} Release: 1%{dist}
Source: https://github.com/sdgathman/pymilter/archive/pymilter-%{version}.tar.gz Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
Source1: pymilter.te
# Patch miltermodule to python3
# FIXME: replace with reverse patch at some point (make py3 the default)
Patch: milter.patch
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 # python-2.6.4 gets RuntimeError: not holding the import lock
Requires: %{pythonbase} >= 2.6.5, sendmail-milter >= 8.13 Requires: %{pythonbase} >= 2.6.5, sendmail >= 8.13
%if 0%{?fedora} >= 23
# Need python2.6 specific pydns, not the version for system python # Need python2.6 specific pydns, not the version for system python
Recommends: %{pythonbase}-pydns Requires: %{pythonbase}-pydns
%endif
# 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
@@ -36,28 +26,11 @@ 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
%if 0%{?epel} >= 6
BuildRequires: policycoreutils-python
%else
BuildRequires: policycoreutils-python-utils
%endif
%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
@@ -65,11 +38,31 @@ 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
# install selinux modules # start.sh is used by spfmilter, srsmilter, and milter, and could be used by
mkdir -p %{buildroot}%{_datadir}/selinux/targeted # other milters using pymilter.
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
@@ -78,52 +71,13 @@ cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%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
* Tue Dec 13 2016 Stuart Gathman <stuart@gathman.org> 1.0.2-1 * Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Fix the last setsymlist misspelling. Support in test framework and tests.
- Add @symlist decorator.
- Change body callback and a few other APIs to use bytes instead of str.
* Tue Sep 20 2016 Stuart Gathman <stuart@gathman.org> 1.0.1-1
- Support python3
* 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
@@ -135,7 +89,7 @@ fi
- Fix milter-template.py - Fix milter-template.py
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6 - Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
* Tue 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
-13
View File
@@ -1,13 +0,0 @@
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 };
+20 -26
View File
@@ -7,10 +7,8 @@ from __future__ import print_function
import sys import sys
import os import os
try: import StringIO
from io import BytesIO import rfc822
except:
from StringIO import StringIO as BytesIO
import mime import mime
import Milter import Milter
import tempfile import tempfile
@@ -23,8 +21,8 @@ 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=None) print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id), end=' ')
for i in msg: print(i,end=None) for i in msg: print(i, end=' ')
print() print()
def __init__(self): def __init__(self):
@@ -33,25 +31,18 @@ class sampleMilter(Milter.Milter):
self.fp = None self.fp = None
self.bodysize = 0 self.bodysize = 0
self.id = Milter.uniqueID() self.id = Milter.uniqueID()
self.user = None
# 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.symlist('{auth_authen}')
@Milter.noreply @Milter.noreply
def envfrom(self,f,*str): def envfrom(self,f,*str):
"start of MAIL transaction" "start of MAIL transaction"
self.fp = BytesIO() self.log("mail from",f,str)
self.fp = StringIO.StringIO()
self.tempname = None self.tempname = None
self.mailfrom = f self.mailfrom = f
self.bodysize = 0 self.bodysize = 0
self.user = self.getsymval('{auth_authen}')
self.auth_type = self.getsymval('{auth_type}')
if self.user:
self.log("user",self.user,"sent mail from",f,str)
else:
self.log("mail from",f,str)
return Milter.CONTINUE return Milter.CONTINUE
def envrcpt(self,to,*str): def envrcpt(self,to,*str):
@@ -70,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
@@ -95,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
@@ -104,12 +95,12 @@ class sampleMilter(Milter.Milter):
if lname in ('subject','x-mailer'): if lname in ('subject','x-mailer'):
self.log('%s: %s' % (name,val)) self.log('%s: %s' % (name,val))
if self.fp: if self.fp:
self.fp.write(("%s: %s\n" % (name,val)).encode()) # add header to buffer self.fp.write("%s: %s\n" % (name,val)) # add header to buffer
return Milter.CONTINUE return Milter.CONTINUE
def eoh(self): def eoh(self):
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
self.fp.write(b'\n') self.fp.write("\n")
self.fp.seek(0) self.fp.seek(0)
# copy headers to a temp file for scanning the body # copy headers to a temp file for scanning the body
headers = self.fp.getvalue() headers = self.fp.getvalue()
@@ -147,16 +138,19 @@ class sampleMilter(Milter.Milter):
self.log("Temp file:",self.tempname) self.log("Temp file:",self.tempname)
self.tempname = None # prevent removal of original message copy self.tempname = None # prevent removal of original message copy
# copy defanged message to a temp file # copy defanged message to a temp file
with tempfile.TemporaryFile() as out: out = tempfile.TemporaryFile()
try:
msg.dump(out) msg.dump(out)
out.seek(0) out.seek(0)
msg = mime.message_from_file(out) msg = rfc822.Message(out)
fp = BytesIO(msg.as_bytes().split(b'\n\n',1)[1]) msg.rewindbody()
while 1: while 1:
buf = fp.read(8192) buf = out.read(8192)
if len(buf) == 0: break if len(buf) == 0: break
self.replacebody(buf) # feed modified message to sendmail self.replacebody(buf) # feed modified message to sendmail
return Milter.ACCEPT # ACCEPT modified message return Milter.ACCEPT # ACCEPT modified message
finally:
out.close()
return Milter.TEMPFAIL return Milter.TEMPFAIL
def close(self): def close(self):
+4 -8
View File
@@ -11,13 +11,9 @@ if sys.version < '2.6.5':
#libs = ["milter", "smutil"] #libs = ["milter", "smutil"]
libs = ["milter"] libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian libdirs = ["/usr/lib/libmilter"] # needed for Debian
modules = ["mime"]
if sys.version >= '3':
modules.append("sgmllib")
print("modules=",modules)
# 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 = '1.0.2', setup(name = "pymilter", version = '0.9.7',
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
@@ -28,10 +24,10 @@ sending DSNs or doing CBVs.
author="Jim Niemira", author="Jim Niemira",
author_email="urmane@urmane.org", author_email="urmane@urmane.org",
maintainer="Stuart D. Gathman", maintainer="Stuart D. Gathman",
maintainer_email="stuart@gathman.org", maintainer_email="stuart@bmsi.com",
license="GPL", license="GPL",
url="https://pythonhosted.org/milter/", url="http://www.bmsi.com/python/milter.html",
py_modules=modules, py_modules=["mime"],
packages = ['Milter'], packages = ['Milter'],
ext_modules=[ ext_modules=[
Extension("milter", ["miltermodule.c"], Extension("milter", ["miltermodule.c"],
-553
View File
@@ -1,553 +0,0 @@
"""A parser for SGML, using the derived class as a static DTD."""
# XXX This only supports those SGML features used by HTML.
# XXX There should be a way to distinguish between PCDATA (parsed
# character data -- the normal case), RCDATA (replaceable character
# data -- only char and entity references and end tags are special)
# and CDATA (character data -- only end tags are special). RCDATA is
# not supported at all.
from __future__ import print_function
try:
import _markupbase
except:
import markupbase as _markupbase
import re
__all__ = ["SGMLParser", "SGMLParseError"]
# Regular expressions used for parsing
interesting = re.compile('[&<]')
incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|'
'<([a-zA-Z][^<>]*|'
'/([a-zA-Z][^<>]*)?|'
'![^<>]*)?')
entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
charref = re.compile('&#([0-9]+)[^0-9]')
starttagopen = re.compile('<[>a-zA-Z]')
shorttagopen = re.compile('<[a-zA-Z][-.a-zA-Z0-9]*/')
shorttag = re.compile('<([a-zA-Z][-.a-zA-Z0-9]*)/([^/]*)/')
piclose = re.compile('>')
endbracket = re.compile('[<>]')
tagfind = re.compile('[a-zA-Z][-_.a-zA-Z0-9]*')
attrfind = re.compile(
r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*'
r'(\'[^\']*\'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?')
class SGMLParseError(RuntimeError):
"""Exception raised for all parse errors."""
pass
# SGML parser base class -- find tags and call handler functions.
# Usage: p = SGMLParser(); p.feed(data); ...; p.close().
# The dtd is defined by deriving a class which defines methods
# with special names to handle tags: start_foo and end_foo to handle
# <foo> and </foo>, respectively, or do_foo to handle <foo> by itself.
# (Tags are converted to lower case for this purpose.) The data
# between tags is passed to the parser by calling self.handle_data()
# with some data as argument (the data may be split up in arbitrary
# chunks). Entity references are passed by calling
# self.handle_entityref() with the entity reference as argument.
class SGMLParser(_markupbase.ParserBase):
# Definition of entities -- derived classes may override
entity_or_charref = re.compile('&(?:'
'([a-zA-Z][-.a-zA-Z0-9]*)|#([0-9]+)'
')(;?)')
def __init__(self, verbose=0):
"""Initialize and reset this instance."""
self.verbose = verbose
self.reset()
def reset(self):
"""Reset this instance. Loses all unprocessed data."""
self.__starttag_text = None
self.rawdata = ''
self.stack = []
self.lasttag = '???'
self.nomoretags = 0
self.literal = 0
_markupbase.ParserBase.reset(self)
def setnomoretags(self):
"""Enter literal mode (CDATA) till EOF.
Intended for derived classes only.
"""
self.nomoretags = self.literal = 1
def setliteral(self, *args):
"""Enter literal mode (CDATA).
Intended for derived classes only.
"""
self.literal = 1
def feed(self, data):
"""Feed some data to the parser.
Call this as often as you want, with as little or as much text
as you want (may include '\n'). (This just saves the text,
all the processing is done by goahead().)
"""
self.rawdata = self.rawdata + data
self.goahead(0)
def close(self):
"""Handle the remaining data."""
self.goahead(1)
def error(self, message):
raise SGMLParseError(message)
# Internal -- handle data as far as reasonable. May leave state
# and data to be processed by a subsequent call. If 'end' is
# true, force handling all data as if followed by EOF marker.
def goahead(self, end):
rawdata = self.rawdata
i = 0
n = len(rawdata)
while i < n:
if self.nomoretags:
self.handle_data(rawdata[i:n])
i = n
break
match = interesting.search(rawdata, i)
if match: j = match.start()
else: j = n
if i < j:
self.handle_data(rawdata[i:j])
i = j
if i == n: break
if rawdata[i] == '<':
if starttagopen.match(rawdata, i):
if self.literal:
self.handle_data(rawdata[i])
i = i+1
continue
k = self.parse_starttag(i)
if k < 0: break
i = k
continue
if rawdata.startswith("</", i):
k = self.parse_endtag(i)
if k < 0: break
i = k
self.literal = 0
continue
if self.literal:
if n > (i + 1):
self.handle_data("<")
i = i+1
else:
# incomplete
break
continue
if rawdata.startswith("<!--", i):
# Strictly speaking, a comment is --.*--
# within a declaration tag <!...>.
# This should be removed,
# and comments handled only in parse_declaration.
k = self.parse_comment(i)
if k < 0: break
i = k
continue
if rawdata.startswith("<?", i):
k = self.parse_pi(i)
if k < 0: break
i = i+k
continue
if rawdata.startswith("<!", i):
# This is some sort of declaration; in "HTML as
# deployed," this should only be the document type
# declaration ("<!DOCTYPE html...>").
k = self.parse_declaration(i)
if k < 0: break
i = k
continue
elif rawdata[i] == '&':
if self.literal:
self.handle_data(rawdata[i])
i = i+1
continue
match = charref.match(rawdata, i)
if match:
name = match.group(1)
self.handle_charref(name)
i = match.end(0)
if rawdata[i-1] != ';': i = i-1
continue
match = entityref.match(rawdata, i)
if match:
name = match.group(1)
self.handle_entityref(name)
i = match.end(0)
if rawdata[i-1] != ';': i = i-1
continue
else:
self.error('neither < nor & ??')
# We get here only if incomplete matches but
# nothing else
match = incomplete.match(rawdata, i)
if not match:
self.handle_data(rawdata[i])
i = i+1
continue
j = match.end(0)
if j == n:
break # Really incomplete
self.handle_data(rawdata[i:j])
i = j
# end while
if end and i < n:
self.handle_data(rawdata[i:n])
i = n
self.rawdata = rawdata[i:]
# XXX if end: check for empty stack
# Extensions for the DOCTYPE scanner:
_decl_otherchars = '='
# Internal -- parse processing instr, return length or -1 if not terminated
def parse_pi(self, i):
rawdata = self.rawdata
if rawdata[i:i+2] != '<?':
self.error('unexpected call to parse_pi()')
match = piclose.search(rawdata, i+2)
if not match:
return -1
j = match.start(0)
self.handle_pi(rawdata[i+2: j])
j = match.end(0)
return j-i
def get_starttag_text(self):
return self.__starttag_text
# Internal -- handle starttag, return length or -1 if not terminated
def parse_starttag(self, i):
self.__starttag_text = None
start_pos = i
rawdata = self.rawdata
if shorttagopen.match(rawdata, i):
# SGML shorthand: <tag/data/ == <tag>data</tag>
# XXX Can data contain &... (entity or char refs)?
# XXX Can data contain < or > (tag characters)?
# XXX Can there be whitespace before the first /?
match = shorttag.match(rawdata, i)
if not match:
return -1
tag, data = match.group(1, 2)
self.__starttag_text = '<%s/' % tag
tag = tag.lower()
k = match.end(0)
self.finish_shorttag(tag, data)
self.__starttag_text = rawdata[start_pos:match.end(1) + 1]
return k
# XXX The following should skip matching quotes (' or ")
# As a shortcut way to exit, this isn't so bad, but shouldn't
# be used to locate the actual end of the start tag since the
# < or > characters may be embedded in an attribute value.
match = endbracket.search(rawdata, i+1)
if not match:
return -1
j = match.start(0)
# Now parse the data between i+1 and j into a tag and attrs
attrs = []
if rawdata[i:i+2] == '<>':
# SGML shorthand: <> == <last open tag seen>
k = j
tag = self.lasttag
else:
match = tagfind.match(rawdata, i+1)
if not match:
self.error('unexpected call to parse_starttag')
k = match.end(0)
tag = rawdata[i+1:k].lower()
self.lasttag = tag
while k < j:
match = attrfind.match(rawdata, k)
if not match: break
attrname, rest, attrvalue = match.group(1, 2, 3)
if not rest:
attrvalue = attrname
else:
if (attrvalue[:1] == "'" == attrvalue[-1:] or
attrvalue[:1] == '"' == attrvalue[-1:]):
# strip quotes
attrvalue = attrvalue[1:-1]
attrvalue = self.entity_or_charref.sub(
self._convert_ref, attrvalue)
attrs.append((attrname.lower(), attrvalue))
k = match.end(0)
if rawdata[j] == '>':
j = j+1
self.__starttag_text = rawdata[start_pos:j]
self.finish_starttag(tag, attrs)
return j
# Internal -- convert entity or character reference
def _convert_ref(self, match):
if match.group(2):
return self.convert_charref(match.group(2)) or \
'&#%s%s' % match.groups()[1:]
elif match.group(3):
return self.convert_entityref(match.group(1)) or \
'&%s;' % match.group(1)
else:
return '&%s' % match.group(1)
# Internal -- parse endtag
def parse_endtag(self, i):
rawdata = self.rawdata
match = endbracket.search(rawdata, i+1)
if not match:
return -1
j = match.start(0)
tag = rawdata[i+2:j].strip().lower()
if rawdata[j] == '>':
j = j+1
self.finish_endtag(tag)
return j
# Internal -- finish parsing of <tag/data/ (same as <tag>data</tag>)
def finish_shorttag(self, tag, data):
self.finish_starttag(tag, [])
self.handle_data(data)
self.finish_endtag(tag)
# Internal -- finish processing of start tag
# Return -1 for unknown tag, 0 for open-only tag, 1 for balanced tag
def finish_starttag(self, tag, attrs):
try:
method = getattr(self, 'start_' + tag)
except AttributeError:
try:
method = getattr(self, 'do_' + tag)
except AttributeError:
self.unknown_starttag(tag, attrs)
return -1
else:
self.handle_starttag(tag, method, attrs)
return 0
else:
self.stack.append(tag)
self.handle_starttag(tag, method, attrs)
return 1
# Internal -- finish processing of end tag
def finish_endtag(self, tag):
if not tag:
found = len(self.stack) - 1
if found < 0:
self.unknown_endtag(tag)
return
else:
if tag not in self.stack:
try:
method = getattr(self, 'end_' + tag)
except AttributeError:
self.unknown_endtag(tag)
else:
self.report_unbalanced(tag)
return
found = len(self.stack)
for i in range(found):
if self.stack[i] == tag: found = i
while len(self.stack) > found:
tag = self.stack[-1]
try:
method = getattr(self, 'end_' + tag)
except AttributeError:
method = None
if method:
self.handle_endtag(tag, method)
else:
self.unknown_endtag(tag)
del self.stack[-1]
# Overridable -- handle start tag
def handle_starttag(self, tag, method, attrs):
method(attrs)
# Overridable -- handle end tag
def handle_endtag(self, tag, method):
method()
# Example -- report an unbalanced </...> tag.
def report_unbalanced(self, tag):
if self.verbose:
print('*** Unbalanced </' + tag + '>')
print('*** Stack:', self.stack)
def convert_charref(self, name):
"""Convert character reference, may be overridden."""
try:
n = int(name)
except ValueError:
return
if not 0 <= n <= 127:
return
return self.convert_codepoint(n)
def convert_codepoint(self, codepoint):
return chr(codepoint)
def handle_charref(self, name):
"""Handle character reference, no need to override."""
replacement = self.convert_charref(name)
if replacement is None:
self.unknown_charref(name)
else:
self.handle_data(replacement)
# Definition of entities -- derived classes may override
entitydefs = \
{'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\''}
def convert_entityref(self, name):
"""Convert entity references.
As an alternative to overriding this method; one can tailor the
results by setting up the self.entitydefs mapping appropriately.
"""
table = self.entitydefs
if name in table:
return table[name]
else:
return
def handle_entityref(self, name):
"""Handle entity references, no need to override."""
replacement = self.convert_entityref(name)
if replacement is None:
self.unknown_entityref(name)
else:
self.handle_data(replacement)
# Example -- handle data, should be overridden
def handle_data(self, data):
pass
# Example -- handle comment, could be overridden
def handle_comment(self, data):
pass
# Example -- handle declaration, could be overridden
def handle_decl(self, decl):
pass
# Example -- handle processing instruction, could be overridden
def handle_pi(self, data):
pass
# To be overridden -- handlers for unknown objects
def unknown_starttag(self, tag, attrs): pass
def unknown_endtag(self, tag): pass
def unknown_charref(self, ref): pass
def unknown_entityref(self, ref): pass
class TestSGMLParser(SGMLParser):
def __init__(self, verbose=0):
self.testdata = ""
SGMLParser.__init__(self, verbose)
def handle_data(self, data):
self.testdata = self.testdata + data
if len(repr(self.testdata)) >= 70:
self.flush()
def flush(self):
data = self.testdata
if data:
self.testdata = ""
print('data:', repr(data))
def handle_comment(self, data):
self.flush()
r = repr(data)
if len(r) > 68:
r = r[:32] + '...' + r[-32:]
print('comment:', r)
def unknown_starttag(self, tag, attrs):
self.flush()
if not attrs:
print('start tag: <' + tag + '>')
else:
print('start tag: <' + tag, end=' ')
for name, value in attrs:
print(name + '=' + '"' + value + '"', end=' ')
print('>')
def unknown_endtag(self, tag):
self.flush()
print('end tag: </' + tag + '>')
def unknown_entityref(self, ref):
self.flush()
print('*** unknown entity ref: &' + ref + ';')
def unknown_charref(self, ref):
self.flush()
print('*** unknown char ref: &#' + ref + ';')
def unknown_decl(self, data):
self.flush()
print('*** unknown decl: [' + data + ']')
def close(self):
SGMLParser.close(self)
self.flush()
def test(args = None):
import sys
if args is None:
args = sys.argv[1:]
if args and args[0] == '-s':
args = args[1:]
klass = SGMLParser
else:
klass = TestSGMLParser
if args:
file = args[0]
else:
file = 'test.html'
if file == '-':
f = sys.stdin
else:
try:
f = open(file, 'r')
except IOError as msg:
print(file, ":", msg)
sys.exit(1)
data = f.read()
if f is not sys.stdin:
f.close()
x = klass()
for c in data:
x.feed(c)
x.close()
if __name__ == '__main__':
test()
-2
View File
@@ -2,7 +2,6 @@ import unittest
import testmime import testmime
import testsample import testsample
import testutils import testutils
import testgrey
import os import os
def suite(): def suite():
@@ -10,7 +9,6 @@ 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__':
-56
View File
@@ -1,56 +0,0 @@
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'
if os.path.isfile(self.fname):
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())
+53 -81
View File
@@ -26,21 +26,14 @@
# Revision 1.20 2004/11/20 16:38:17 stuart # Revision 1.20 2004/11/20 16:38:17 stuart
# Add rcs log # Add rcs log
# #
from __future__ import print_function
import unittest import unittest
import mime import mime
import socket import socket
try: import StringIO
from StringIO import StringIO
except:
from io import StringIO
import email import email
import sys import sys
import Milter import Milter
try: from email import Errors
from email import Errors as errors
except:
from email import errors
samp1_txt1 = """Dear Agent 1 samp1_txt1 = """Dear Agent 1
I hope you can read this. Whenever you write label it P.B.S kids. I hope you can read this. Whenever you write label it P.B.S kids.
@@ -53,56 +46,48 @@ class MimeTestCase(unittest.TestCase):
# test mime parameter parsing # test mime parameter parsing
def testParam(self): def testParam(self):
plist = mime._parseparam('; boundary="----=_NextPart_000_4e56_490d_48e3"') plist = mime._parseparam(
plist = [ x for x in plist if x ] # py2 doesn't include empty params '; boundary="----=_NextPart_000_4e56_490d_48e3"')
self.assertEqual(1,len(plist)) self.failUnless(len(plist)==1)
self.assertTrue(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"') self.failUnless(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"')
plist = mime._parseparam('; name="Jim&amp;amp;Girlz.jpg"') plist = mime._parseparam('; name="Jim&amp;amp;Girlz.jpg"')
plist = [ x for x in plist if x ] # py2 doesn't include empty params self.failUnless(len(plist)==1)
self.assertEqual(1,len(plist)) self.failUnless(plist[0] == 'name="Jim&amp;amp;Girlz.jpg"')
self.assertTrue(plist[0] == 'name="Jim&amp;amp;Girlz.jpg"')
def testParse(self,fname='samp1'): def testParse(self,fname='samp1'):
with open('test/'+fname,"rb") as fp: msg = mime.message_from_file(open('test/'+fname,"r"))
msg = mime.message_from_file(fp) self.failUnless(msg.ismultipart())
self.assertTrue(msg.ismultipart())
parts = msg.get_payload() parts = msg.get_payload()
self.assertTrue(len(parts) == 2) self.failUnless(len(parts) == 2)
txt1 = parts[0].get_payload() txt1 = parts[0].get_payload()
self.assertTrue(txt1.rstrip() == samp1_txt1,txt1) self.failUnless(txt1.rstrip() == samp1_txt1,txt1)
with open('test/missingboundary',"rb") as fp: msg = mime.message_from_file(open('test/missingboundary',"r"))
msg = mime.message_from_file(fp)
# should get no exception as long as we don't try to parse # should get no exception as long as we don't try to parse
# message attachments # message attachments
mime.defang(msg,scan_rfc822=False) mime.defang(msg,scan_rfc822=False)
with open('test/missingboundary.out','wb') as fp: msg.dump(open('test/missingboundary.out','w'))
msg.dump(fp) msg = mime.message_from_file(open('test/missingboundary',"r"))
with open('test/missingboundary',"rb") as fp:
msg = mime.message_from_file(fp)
try: try:
mime.defang(msg) mime.defang(msg)
# python 2.4 doesn't get exceptions on missing boundaries, and # python 2.4 doesn't get exceptions on missing boundaries, and
# if message is modified, output is readable by mail clients # if message is modified, output is readable by mail clients
if sys.hexversion < 0x02040000: if sys.hexversion < 0x02040000:
self.fail('should get boundary error parsing bad rfc822 attachment') self.fail('should get boundary error parsing bad rfc822 attachment')
except errors.BoundaryError: except Errors.BoundaryError:
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'):
with open('test/'+vname,"rb") as fp: msg = mime.message_from_file(open('test/'+vname,"r"))
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.assertTrue(msg.ismodified(),"virus not removed") self.failUnless(msg.ismodified(),"virus not removed")
oname = vname + '.out' oname = vname + '.out'
with open('test/'+oname,"wb") as fp: msg.dump(open('test/'+oname,"w"))
msg.dump(fp) msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"rb") as fp:
msg = mime.message_from_file(fp)
txt2 = msg.get_payload() txt2 = msg.get_payload()
if type(txt2) == list: if type(txt2) == list:
txt2 = txt2[part].get_payload() txt2 = txt2[part].get_payload()
self.assertTrue( self.failUnless(
txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2) txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
def testDefang3(self): def testDefang3(self):
@@ -118,60 +103,51 @@ class MimeTestCase(unittest.TestCase):
# virus6 has no parts - the virus is directly inline # virus6 has no parts - the virus is directly inline
def testDefang6(self,vname="virus6",fname='FAX20.exe'): def testDefang6(self,vname="virus6",fname='FAX20.exe'):
with open('test/'+vname,"rb") as fp: msg = mime.message_from_file(open('test/'+vname,"r"))
msg = mime.message_from_file(fp)
mime.defang(msg) mime.defang(msg)
oname = vname + '.out' oname = vname + '.out'
with open('test/'+oname,"wb") as fp: msg.dump(open('test/'+oname,"w"))
msg.dump(fp) msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"rb") as fp: self.failIf(msg.ismultipart())
msg = mime.message_from_file(fp)
self.assertFalse(msg.ismultipart())
txt2 = msg.get_payload() txt2 = msg.get_payload()
self.assertTrue(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
def testDefang7(self,vname="honey",fname='story[1].scr'): def testDefang7(self,vname="honey",fname='story[1].scr'):
with open('test/'+vname,"rb") as fp: msg = mime.message_from_file(open('test/'+vname,"r"))
msg = mime.message_from_file(fp)
mime.defang(msg) mime.defang(msg)
oname = vname + '.out' oname = vname + '.out'
with open('test/'+oname,"wb") as fp: msg.dump(open('test/'+oname,"w"))
msg.dump(fp) msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"rb") as fp:
msg = mime.message_from_file(fp)
parts = msg.get_payload() parts = msg.get_payload()
txt2 = parts[1].get_payload() txt2 = parts[1].get_payload()
txt3 = parts[2].get_payload() txt3 = parts[2].get_payload()
self.assertTrue(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.assertTrue(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)
def testParse2(self,fname="spam7"): def testParse2(self,fname="spam7"):
with open('test/'+fname,"rb") as fp: msg = mime.message_from_file(open('test/'+fname,"r"))
msg = mime.message_from_file(fp) self.failUnless(msg.ismultipart())
self.assertTrue(msg.ismultipart())
parts = msg.get_payload() parts = msg.get_payload()
self.assertTrue(len(parts) == 2) self.failUnless(len(parts) == 2)
name = parts[1].getname() name = parts[1].getname()
self.assertTrue(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name) self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name)
def testZip(self,vname="zip1",fname='zip.zip'): def testZip(self,vname="zip1",fname='zip.zip'):
self.testDefang(vname,1,'zip.zip') self.testDefang(vname,1,'zip.zip')
# test scan_zip flag # test scan_zip flag
with open('test/'+vname,"rb") as fp: msg = mime.message_from_file(open('test/'+vname,"r"))
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=False) mime.defang(msg,scan_zip=False)
self.assertFalse(msg.ismodified()) self.failIf(msg.ismodified())
# test ignoring empty zip (often found in DSNs) # test ignoring empty zip (often found in DSNs)
with open('test/zip2','rb') as fp: msg = mime.message_from_file(open('test/zip2','r'))
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.assertFalse(msg.ismodified()) self.failIf(msg.ismodified())
# test corrupt zip (often an EXE named as a ZIP) # test corrupt zip (often an EXE named as a ZIP)
self.testDefang('zip3',1,'zip.zip') self.testDefang('zip3',1,'zip.zip')
# test zip within zip # test zip within zip
@@ -188,24 +164,22 @@ class MimeTestCase(unittest.TestCase):
mime.check_html(msg) mime.check_html(msg)
# don't let a tricky virus slip one past us # don't let a tricky virus slip one past us
msg = msg.get_submsg() msg = msg.get_submsg()
if isinstance(msg,email.message.Message): if isinstance(msg,email.Message.Message):
return mime.check_attachments(msg,self._chk_attach) return mime.check_attachments(msg,self._chk_attach)
return Milter.CONTINUE return Milter.CONTINUE
def testCheckAttach(self,fname="test1"): def testCheckAttach(self,fname="test1"):
# test1 contains a very long filename # test1 contains a very long filename
with open('test/'+fname,'rb') as fp: msg = mime.message_from_file(open('test/'+fname,'r'))
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.assertFalse(msg.ismodified()) self.failIf(msg.ismodified())
with open('test/test2','rb') as fp: msg = mime.message_from_file(open('test/test2','r'))
msg = mime.message_from_file(fp)
rc = mime.check_attachments(msg,self._chk_attach) rc = mime.check_attachments(msg,self._chk_attach)
self.assertEqual(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF") self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
self.assertEqual(rc,Milter.CONTINUE) self.assertEquals(rc,Milter.CONTINUE)
def testHTML(self,fname=""): def testHTML(self,fname=""):
result = StringIO() result = StringIO.StringIO()
filter = mime.HTMLScriptFilter(result) filter = mime.HTMLScriptFilter(result)
msg = """<! Illegal declaration used as comment> msg = """<! Illegal declaration used as comment>
<![if conditional]> Optional SGML <![endif]> <![if conditional]> Optional SGML <![endif]>
@@ -214,10 +188,8 @@ class MimeTestCase(unittest.TestCase):
script = "<script lang=javascript> Dangerous script </script>" script = "<script lang=javascript> Dangerous script </script>"
filter.feed(msg + script) filter.feed(msg + script)
filter.close() filter.close()
#print(result.getvalue()) #print result.getvalue()
#print('---') self.failUnless(result.getvalue() == msg + filter.msg)
#print(msg + filter.msg)
self.assertTrue(result.getvalue() == msg + filter.msg)
def suite(): return unittest.makeSuite(MimeTestCase,'test') def suite(): return unittest.makeSuite(MimeTestCase,'test')
@@ -226,7 +198,7 @@ if __name__ == '__main__':
unittest.main() unittest.main()
else: else:
for fname in sys.argv[1:]: for fname in sys.argv[1:]:
with open(fname,'rb') as fp: 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())
+101 -19
View File
@@ -2,29 +2,111 @@ import unittest
import Milter import Milter
import sample import sample
import mime import mime
from Milter.test import TestBase import rfc822
import StringIO
class TestMilter(TestBase,sample.sampleMilter): class TestMilter(sample.sampleMilter):
_protocol = 0
def __init__(self): def __init__(self):
TestBase.__init__(self) self.logfp = open("test/milter.log","a")
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):
def testDefang(self,fname='virus1'): def testDefang(self,fname='virus1'):
milter = TestMilter() milter = TestMilter()
milter.setsymval('{auth_authen}','batman')
milter.setsymval('{auth_type}','batcomputer')
milter.setsymval('j','mailhost')
rc = milter.connect() rc = milter.connect()
self.failUnless(rc == Milter.CONTINUE) self.failUnless(rc == Milter.CONTINUE)
rc = milter.feedMsg(fname) rc = milter.feedMsg(fname)
self.failUnless(milter.user == 'batman',"getsymval failed")
self.failUnless(milter.auth_type != 'batcomputer',"setsymlist failed")
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","wb").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())
fp.seek(0) fp.seek(0)
msg = mime.message_from_file(fp) msg = mime.message_from_file(fp)
@@ -37,9 +119,9 @@ 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","wb").write(fp.getvalue()) open('test/'+fname+".tstout","w").write(fp.getvalue())
milter.close() milter.close()
def testDefang2(self): def testDefang2(self):
@@ -47,19 +129,19 @@ 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","wb").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","wb").write(fp.getvalue()) open("test/virus6.tstout","w").write(fp.getvalue())
milter.close() milter.close()
def suite(): return unittest.makeSuite(BMSMilterTestCase,'test') def suite(): return unittest.makeSuite(BMSMilterTestCase,'test')
+4 -11
View File
@@ -1,11 +1,9 @@
from __future__ import print_function
import unittest import unittest
import doctest import doctest
import os import os
import Milter.utils import Milter.utils
from Milter.cache import AddrCache from Milter.cache import AddrCache
from Milter.dynip import is_dynip from Milter.dynip import is_dynip
from Milter.pyip6 import inet_ntop
class AddrCacheTestCase(unittest.TestCase): class AddrCacheTestCase(unittest.TestCase):
@@ -34,8 +32,9 @@ class AddrCacheTestCase(unittest.TestCase):
self.failUnless(not cache['temp@bar.com']) self.failUnless(not cache['temp@bar.com'])
def testDomain(self): def testDomain(self):
with open(self.fname,'w') as fp: fp = open(self.fname,'w')
print('spammer.com',file=fp) print >>fp,'spammer.com'
fp.close()
cache = AddrCache(fname=self.fname) cache = AddrCache(fname=self.fname)
cache.load(self.fname,30) cache.load(self.fname,30)
self.failUnless('spammer.com' in cache) self.failUnless('spammer.com' in cache)
@@ -43,18 +42,12 @@ class AddrCacheTestCase(unittest.TestCase):
def testParseHeader(self): def testParseHeader(self):
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?=' s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
h = Milter.utils.parse_header(s) h = Milter.utils.parse_header(s)
self.assertEqual(h,b'Last Few Coldplay Album Artworks Available\x00') self.assertEqual(h,'Last Few Coldplay Album Artworks Available\x00')
@unittest.expectedFailure
def testParseAddress(self):
s = Milter.utils.parseaddr('a(WRONG)@b')
self.assertEqual(s,('WRONG', 'a@b'))
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))
s.addTest(doctest.DocTestSuite(Milter.dynip)) s.addTest(doctest.DocTestSuite(Milter.dynip))
s.addTest(doctest.DocTestSuite(Milter.pyip6))
return s return s
if __name__ == '__main__': if __name__ == '__main__':