Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b5db35ace |
@@ -1,4 +0,0 @@
|
|||||||
*.pyc
|
|
||||||
build/
|
|
||||||
test/*.out
|
|
||||||
test/*.tstout
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -31,7 +31,7 @@ PROJECT_NAME = pymilter
|
|||||||
# This could be handy for archiving the generated documentation or
|
# This could be handy for archiving the generated documentation or
|
||||||
# if some version control system is used.
|
# if some version control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 1.0
|
PROJECT_NUMBER = 0.9.6
|
||||||
|
|
||||||
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
|
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
|
||||||
# base path where the generated documentation will be put.
|
# base path where the generated documentation will be put.
|
||||||
@@ -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,33 +875,16 @@ 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
|
||||||
@@ -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,10 +1234,8 @@ 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+49
-35
@@ -8,17 +8,12 @@
|
|||||||
# 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.8'
|
||||||
__version__ = '1.0.1'
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
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 +21,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).
|
||||||
"""
|
"""
|
||||||
@@ -106,7 +107,7 @@ def rejected_recipients(klass):
|
|||||||
return enable_protocols(klass,P_RCPT_REJ)
|
return enable_protocols(klass,P_RCPT_REJ)
|
||||||
|
|
||||||
## Milter leading space on headers. A class decorator that calls
|
## Milter leading space on headers. A class decorator that calls
|
||||||
# enable_protocols() with the P_HDR_LEADSPC flag. By default,
|
# enable_protocols() with the P_HEAD_LEADSPC flag. By default,
|
||||||
# header continuation lines are collected and joined before getting
|
# header continuation lines are collected and joined before getting
|
||||||
# sent to a milter. Headers modified or added by the milter are
|
# sent to a milter. Headers modified or added by the milter are
|
||||||
# folded by the MTA as necessary according to its own standards.
|
# folded by the MTA as necessary according to its own standards.
|
||||||
@@ -124,7 +125,7 @@ def rejected_recipients(klass):
|
|||||||
# @param klass the %milter application class to modify
|
# @param klass the %milter application class to modify
|
||||||
# @return the modified %milter class
|
# @return the modified %milter class
|
||||||
def header_leading_space(klass):
|
def header_leading_space(klass):
|
||||||
return enable_protocols(klass,P_HDR_LEADSPC)
|
return enable_protocols(klass,P_HEAD_LEADSPC)
|
||||||
|
|
||||||
## Function decorator to disable callback methods.
|
## Function decorator to disable callback methods.
|
||||||
# If the MTA supports it, tells the MTA not to invoke this callback,
|
# If the MTA supports it, tells the MTA not to invoke this callback,
|
||||||
@@ -262,7 +263,7 @@ class Base(object):
|
|||||||
## Defined by subclasses to write log messages.
|
## Defined by subclasses to write log messages.
|
||||||
def log(self,*msg): pass
|
def log(self,*msg): pass
|
||||||
## Called for each connection to the MTA. Called by the
|
## Called for each connection to the MTA. Called by the
|
||||||
# <a href="milter_api/xxfi_connect.html">
|
# <a href="https://www.milter.org/developers/api/xxfi_connect">
|
||||||
# xxfi_connect</a> callback.
|
# xxfi_connect</a> callback.
|
||||||
# The <code>hostname</code> provided by the local MTA is either
|
# The <code>hostname</code> provided by the local MTA is either
|
||||||
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
|
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
|
||||||
@@ -299,7 +300,7 @@ class Base(object):
|
|||||||
@nocallback
|
@nocallback
|
||||||
def hello(self,hostname): return CONTINUE
|
def hello(self,hostname): return CONTINUE
|
||||||
## Called when the SMTP client says MAIL FROM. Called by the
|
## Called when the SMTP client says MAIL FROM. Called by the
|
||||||
# <a href="milter_api/xxfi_envfrom.html">
|
# <a href="https://www.milter.org/developers/api/xxfi_envfrom">
|
||||||
# xxfi_envfrom</a> callback.
|
# xxfi_envfrom</a> callback.
|
||||||
# Returning REJECT rejects the message, but not the connection.
|
# Returning REJECT rejects the message, but not the connection.
|
||||||
# The sender is the "envelope" from as defined by
|
# The sender is the "envelope" from as defined by
|
||||||
@@ -310,7 +311,7 @@ class Base(object):
|
|||||||
@nocallback
|
@nocallback
|
||||||
def envfrom(self,f,*str): return CONTINUE
|
def envfrom(self,f,*str): return CONTINUE
|
||||||
## Called when the SMTP client says RCPT TO. Called by the
|
## Called when the SMTP client says RCPT TO. Called by the
|
||||||
# <a href="milter_api/xxfi_envrcpt.html">
|
# <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
|
||||||
# xxfi_envrcpt</a> callback.
|
# xxfi_envrcpt</a> callback.
|
||||||
# Returning REJECT rejects the current recipient, not the entire message.
|
# Returning REJECT rejects the current recipient, not the entire message.
|
||||||
# The recipient is the "envelope" recipient as defined by
|
# The recipient is the "envelope" recipient as defined by
|
||||||
@@ -370,13 +371,13 @@ 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
|
||||||
|
|
||||||
## Negotiate milter protocol options. Called by the
|
## Negotiate milter protocol options. Called by the
|
||||||
# <a href="milter_api/xxfi_negotiate.html">
|
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
|
||||||
# xffi_negotiate</a> callback. This is an advanced callback,
|
# xffi_negotiate</a> callback. This is an advanced callback,
|
||||||
# do not override unless you know what you are doing. Most
|
# do not override unless you know what you are doing. Most
|
||||||
# negotiation can be done simply by using the supplied
|
# negotiation can be done simply by using the supplied
|
||||||
@@ -407,7 +408,7 @@ class Base(object):
|
|||||||
## Return the value of an MTA macro. Sendmail macro names
|
## Return the value of an MTA macro. Sendmail macro names
|
||||||
# are either single chars (e.g. "j") or multiple chars enclosed
|
# are either single chars (e.g. "j") or multiple chars enclosed
|
||||||
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
|
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
|
||||||
# See <a href="milter_api/smfi_getsymval.html">
|
# See <a href="https://www.milter.org/developers/api/smfi_getsymval">
|
||||||
# smfi_getsymval</a> for default sendmail macros.
|
# smfi_getsymval</a> for default sendmail macros.
|
||||||
# @param sym the macro name
|
# @param sym the macro name
|
||||||
def getsymval(self,sym):
|
def getsymval(self,sym):
|
||||||
@@ -421,7 +422,7 @@ class Base(object):
|
|||||||
# head scratching. What will <i>really</i> irritate you, however,
|
# head scratching. What will <i>really</i> irritate you, however,
|
||||||
# is that if you carefully double any '%%', your message will be
|
# is that if you carefully double any '%%', your message will be
|
||||||
# sent - but with the '%%' still doubled!
|
# sent - but with the '%%' still doubled!
|
||||||
# See <a href="milter_api/smfi_setreply.html">
|
# See <a href="https://www.milter.org/developers/api/smfi_setreply">
|
||||||
# smfi_setreply</a> for more information.
|
# smfi_setreply</a> for more information.
|
||||||
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
|
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
|
||||||
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
|
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
|
||||||
@@ -464,7 +465,7 @@ class Base(object):
|
|||||||
# Milter methods which can only be called from eom callback.
|
# Milter methods which can only be called from eom callback.
|
||||||
|
|
||||||
## Add a mail header field.
|
## Add a mail header field.
|
||||||
# Calls <a href="milter_api/smfi_addheader.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_addheader">
|
||||||
# smfi_addheader</a>.
|
# smfi_addheader</a>.
|
||||||
# The <code>Milter.ADDHDRS</code> action flag must be set.
|
# The <code>Milter.ADDHDRS</code> action flag must be set.
|
||||||
#
|
#
|
||||||
@@ -478,7 +479,7 @@ class Base(object):
|
|||||||
return self._ctx.addheader(field,value,idx)
|
return self._ctx.addheader(field,value,idx)
|
||||||
|
|
||||||
## Change the value of a mail header field.
|
## Change the value of a mail header field.
|
||||||
# Calls <a href="milter_api/smfi_chgheader.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">
|
||||||
# smfi_chgheader</a>.
|
# smfi_chgheader</a>.
|
||||||
# The <code>Milter.CHGHDRS</code> action flag must be set.
|
# The <code>Milter.CHGHDRS</code> action flag must be set.
|
||||||
#
|
#
|
||||||
@@ -492,7 +493,7 @@ class Base(object):
|
|||||||
return self._ctx.chgheader(field,idx,value)
|
return self._ctx.chgheader(field,idx,value)
|
||||||
|
|
||||||
## Add a recipient to the message.
|
## Add a recipient to the message.
|
||||||
# Calls <a href="milter_api/smfi_addrcpt.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">
|
||||||
# smfi_addrcpt</a>.
|
# smfi_addrcpt</a>.
|
||||||
# If no corresponding mail header is added, this is like a Bcc.
|
# If no corresponding mail header is added, this is like a Bcc.
|
||||||
# The syntax of the recipient is the same as used in the SMTP
|
# The syntax of the recipient is the same as used in the SMTP
|
||||||
@@ -512,7 +513,7 @@ class Base(object):
|
|||||||
raise DisabledAction("ADDRCPT_PAR")
|
raise DisabledAction("ADDRCPT_PAR")
|
||||||
return self._ctx.addrcpt(rcpt,params)
|
return self._ctx.addrcpt(rcpt,params)
|
||||||
## Delete a recipient from the message.
|
## Delete a recipient from the message.
|
||||||
# Calls <a href="milter_api/smfi_delrcpt.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">
|
||||||
# smfi_delrcpt</a>.
|
# smfi_delrcpt</a>.
|
||||||
# The recipient should match one passed to the envrcpt callback.
|
# The recipient should match one passed to the envrcpt callback.
|
||||||
# The <code>Milter.DELRCPT</code> action flag must be set.
|
# The <code>Milter.DELRCPT</code> action flag must be set.
|
||||||
@@ -525,7 +526,7 @@ class Base(object):
|
|||||||
return self._ctx.delrcpt(rcpt)
|
return self._ctx.delrcpt(rcpt)
|
||||||
|
|
||||||
## Replace the message body.
|
## Replace the message body.
|
||||||
# Calls <a href="milter_api/smfi_replacebody.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">
|
||||||
# smfi_replacebody</a>.
|
# smfi_replacebody</a>.
|
||||||
# The entire message body must be replaced.
|
# The entire message body must be replaced.
|
||||||
# Call repeatedly with blocks of data until the entire body is transferred.
|
# Call repeatedly with blocks of data until the entire body is transferred.
|
||||||
@@ -539,7 +540,7 @@ class Base(object):
|
|||||||
return self._ctx.replacebody(body)
|
return self._ctx.replacebody(body)
|
||||||
|
|
||||||
## Change the SMTP envelope sender address.
|
## Change the SMTP envelope sender address.
|
||||||
# Calls <a href="milter_api/smfi_chgfrom.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">
|
||||||
# smfi_chgfrom</a>.
|
# smfi_chgfrom</a>.
|
||||||
# The syntax of the sender is that same as used in the SMTP
|
# The syntax of the sender is that same as used in the SMTP
|
||||||
# MAIL FROM command (and as delivered to the envfrom callback),
|
# MAIL FROM command (and as delivered to the envfrom callback),
|
||||||
@@ -556,7 +557,7 @@ class Base(object):
|
|||||||
return self._ctx.chgfrom(sender,params)
|
return self._ctx.chgfrom(sender,params)
|
||||||
|
|
||||||
## Quarantine the message.
|
## Quarantine the message.
|
||||||
# Calls <a href="milter_api/smfi_quarantine.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">
|
||||||
# smfi_quarantine</a>.
|
# smfi_quarantine</a>.
|
||||||
# When quarantined, a message goes into the mailq as if to be delivered,
|
# When quarantined, a message goes into the mailq as if to be delivered,
|
||||||
# but delivery is deferred until the message is unquarantined.
|
# but delivery is deferred until the message is unquarantined.
|
||||||
@@ -570,7 +571,7 @@ class Base(object):
|
|||||||
return self._ctx.quarantine(reason)
|
return self._ctx.quarantine(reason)
|
||||||
|
|
||||||
## Tell the MTA to wait a bit longer.
|
## Tell the MTA to wait a bit longer.
|
||||||
# Calls <a href="milter_api/smfi_progress.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_progress">
|
||||||
# smfi_progress</a>.
|
# smfi_progress</a>.
|
||||||
# Resets timeouts in the MTA that detect a "hung" milter.
|
# Resets timeouts in the MTA that detect a "hung" milter.
|
||||||
def progress(self):
|
def progress(self):
|
||||||
@@ -584,9 +585,9 @@ class Milter(Base):
|
|||||||
|
|
||||||
## Provide simple logging to sys.stdout
|
## Provide simple logging to sys.stdout
|
||||||
def log(self,*msg):
|
def log(self,*msg):
|
||||||
print('Milter:',end=None)
|
print 'Milter:',
|
||||||
for i in msg: print(i,end=None)
|
for i in msg: print i,
|
||||||
print()
|
print
|
||||||
|
|
||||||
@noreply
|
@noreply
|
||||||
def connect(self,hostname,family,hostaddr):
|
def connect(self,hostname,family,hostaddr):
|
||||||
@@ -723,7 +724,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)
|
||||||
@@ -754,14 +776,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()
|
||||||
|
|||||||
+5
-8
@@ -46,9 +46,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'
|
||||||
@@ -132,8 +131,8 @@ class AddrCache(object):
|
|||||||
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)
|
||||||
@@ -148,8 +147,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 +155,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)
|
||||||
|
|||||||
+4
-6
@@ -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 = {}
|
||||||
@@ -48,7 +46,7 @@ class MilterConfigParser(ConfigParser):
|
|||||||
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:
|
||||||
|
|||||||
+3
-5
@@ -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
|
||||||
|
|
||||||
@@ -74,7 +73,6 @@ class Session(object):
|
|||||||
if name.endswith('.'): name = name[:-1]
|
if name.endswith('.'): name = name[:-1]
|
||||||
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
|
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
|
||||||
return [] # invalid DNS name (too long or empty)
|
return [] # invalid DNS name (too long or empty)
|
||||||
name = name.lower()
|
|
||||||
result = self.cache.get( (name, qtype) )
|
result = self.cache.get( (name, qtype) )
|
||||||
cname = None
|
cname = None
|
||||||
if result: return result
|
if result: return result
|
||||||
@@ -98,7 +96,7 @@ class Session(object):
|
|||||||
#return result # if too many == NX_DOMAIN
|
#return result # if too many == NX_DOMAIN
|
||||||
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
||||||
cnames[name] = cname
|
cnames[name] = cname
|
||||||
if cname.lower().rstrip('.') in cnames:
|
if cname in cnames:
|
||||||
raise DNSError('CNAME loop')
|
raise DNSError('CNAME loop')
|
||||||
result = self.dns(cname, qtype, cnames=cnames)
|
result = self.dns(cname, qtype, cnames=cnames)
|
||||||
if result:
|
if result:
|
||||||
@@ -121,5 +119,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)
|
||||||
|
|||||||
+3
-4
@@ -69,7 +69,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
|
||||||
@@ -231,6 +230,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())
|
||||||
|
|||||||
+4
-5
@@ -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
|
||||||
@@ -92,6 +91,6 @@ if __name__ == '__main__':
|
|||||||
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)
|
||||||
|
|||||||
+1
-20
@@ -1,4 +1,3 @@
|
|||||||
from __future__ import print_function
|
|
||||||
import time
|
import time
|
||||||
import shelve
|
import shelve
|
||||||
import thread
|
import thread
|
||||||
@@ -42,24 +41,13 @@ 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):
|
|
||||||
"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):
|
def clean(self,timeinc=0):
|
||||||
"Delete records past the retention limit."
|
"Delete records past the retention limit."
|
||||||
now = time.time() + timeinc
|
now = time.time() + timeinc
|
||||||
cnt = 0
|
cnt = 0
|
||||||
dbp = self.dbp
|
dbp = self.dbp
|
||||||
for key, r in dbp.iteritems():
|
for key, r in dbp.iteritems():
|
||||||
#print(key,r,time.ctime(now))
|
#print key,r,time.ctime(now)
|
||||||
if now > r.lastseen + self.greylist_retain:
|
if now > r.lastseen + self.greylist_retain:
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
try:
|
try:
|
||||||
@@ -112,10 +100,3 @@ class Greylist(object):
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.dbp.close()
|
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()
|
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import time
|
|||||||
import logging
|
import logging
|
||||||
import urllib
|
import urllib
|
||||||
import sqlite3
|
import sqlite3
|
||||||
try:
|
|
||||||
import thread
|
import thread
|
||||||
except:
|
|
||||||
import _thread as thread
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
log = logging.getLogger('milter.greylist')
|
log = logging.getLogger('milter.greylist')
|
||||||
@@ -28,19 +25,6 @@ class Greylist(object):
|
|||||||
primary key (ip,sender,recipient))''')
|
primary key (ip,sender,recipient))''')
|
||||||
except: pass
|
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):
|
def clean(self,timeinc=0):
|
||||||
"Delete records past the retention limit."
|
"Delete records past the retention limit."
|
||||||
now = time.time() + timeinc - self.greylist_retain
|
now = time.time() + timeinc - self.greylist_retain
|
||||||
@@ -100,10 +84,3 @@ class Greylist(object):
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
g = Greylist(sys.argv[1])
|
|
||||||
try:
|
|
||||||
g.import_csv(sys.stdin)
|
|
||||||
finally: g.close()
|
|
||||||
|
|||||||
+3
-3
@@ -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:
|
||||||
@@ -46,7 +46,7 @@ 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:
|
||||||
|
|||||||
+3
-4
@@ -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
|
||||||
@@ -81,11 +80,11 @@ 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:
|
||||||
@@ -115,4 +114,4 @@ def inet_pton(p):
|
|||||||
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
|
||||||
|
|||||||
+47
-55
@@ -1,17 +1,13 @@
|
|||||||
## @package Milter.test
|
## @package Milter.test
|
||||||
# A test framework for milters
|
# A test framework for milters
|
||||||
|
|
||||||
from __future__ import print_function
|
import rfc822
|
||||||
import mime
|
import StringIO
|
||||||
try:
|
|
||||||
from io import BytesIO
|
|
||||||
except:
|
|
||||||
from StringIO import StringIO as BytesIO
|
|
||||||
import Milter
|
import Milter
|
||||||
|
|
||||||
Milter.NOREPLY = Milter.CONTINUE
|
Milter.NOREPLY = Milter.CONTINUE
|
||||||
|
|
||||||
## Test mixin for unit testing %milter applications.
|
## Test mixin for unit testing milter applications.
|
||||||
# This mixin overrides many Milter.MilterBase methods
|
# This mixin overrides many Milter.MilterBase methods
|
||||||
# with stub versions that simply record what was done.
|
# with stub versions that simply record what was done.
|
||||||
# @since 0.9.8
|
# @since 0.9.8
|
||||||
@@ -20,8 +16,6 @@ class TestBase(object):
|
|||||||
def __init__(self,logfile='test/milter.log'):
|
def __init__(self,logfile='test/milter.log'):
|
||||||
self._protocol = 0
|
self._protocol = 0
|
||||||
self.logfp = open(logfile,"a")
|
self.logfp = open(logfile,"a")
|
||||||
## The MAIL FROM for the current email being fed to the %milter
|
|
||||||
self._sender = None
|
|
||||||
## List of recipients deleted
|
## List of recipients deleted
|
||||||
self._delrcpt = []
|
self._delrcpt = []
|
||||||
## List of recipients added
|
## List of recipients added
|
||||||
@@ -30,24 +24,22 @@ class TestBase(object):
|
|||||||
self._macros = { }
|
self._macros = { }
|
||||||
## The message body.
|
## The message body.
|
||||||
self._body = None
|
self._body = None
|
||||||
## True if the %milter replaced the message body.
|
## True if the milter replaced the message body.
|
||||||
self._bodyreplaced = False
|
self._bodyreplaced = False
|
||||||
## True if the %milter changed any headers.
|
## True if the milter changed any headers.
|
||||||
self._headerschanged = False
|
self._headerschanged = False
|
||||||
## True if the %milter changed the envelope from.
|
## Reply codes and messages set by milter
|
||||||
self._envfromchanged = False
|
|
||||||
## Reply codes and messages set by the %milter
|
|
||||||
self._reply = None
|
self._reply = None
|
||||||
## The rfc822 message object for the current email being fed to the %milter.
|
## The rfc822 message object for the current email being fed to the milter.
|
||||||
self._msg = None
|
self._msg = None
|
||||||
self._symlist = [ None, None, None, None, None, None, None ]
|
self._symlist = [ None, None, None, None, None, None, None ]
|
||||||
|
|
||||||
def log(self,*msg):
|
def log(self,*msg):
|
||||||
for i in msg: print(i,file=self.logfp,end=None)
|
for i in msg: print >>self.logfp, i,
|
||||||
print(file=self.logfp)
|
print >>self.logfp
|
||||||
|
|
||||||
## Set a macro value.
|
## Set a macro value.
|
||||||
# These are retrieved by the %milter with getsymval.
|
# These are retrieved by the milter with getsymval.
|
||||||
# @param name the macro name, as passed to getsymval
|
# @param name the macro name, as passed to getsymval
|
||||||
# @param val the macro value
|
# @param val the macro value
|
||||||
def setsymval(self,name,val):
|
def setsymval(self,name,val):
|
||||||
@@ -62,28 +54,13 @@ class TestBase(object):
|
|||||||
self._body.write(chunk)
|
self._body.write(chunk)
|
||||||
self._bodyreplaced = True
|
self._bodyreplaced = True
|
||||||
else:
|
else:
|
||||||
raise IOError("replacebody not called from eom()")
|
raise IOError,"replacebody not called from eom()"
|
||||||
|
|
||||||
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
|
# FIXME: rfc822 indexing does not really reflect the way chg/add header
|
||||||
# work for a %milter
|
# work for a milter
|
||||||
def chgheader(self,field,idx,value):
|
def chgheader(self,field,idx,value):
|
||||||
if not self._body:
|
if not self._body:
|
||||||
raise IOError("chgheader not called from eom()")
|
raise IOError,"chgheader not called from eom()"
|
||||||
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
|
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
|
||||||
if value == '':
|
if value == '':
|
||||||
del self._msg[field]
|
del self._msg[field]
|
||||||
@@ -93,19 +70,19 @@ class TestBase(object):
|
|||||||
|
|
||||||
def addheader(self,field,value,idx=-1):
|
def addheader(self,field,value,idx=-1):
|
||||||
if not self._body:
|
if not self._body:
|
||||||
raise IOError("addheader not called from eom()")
|
raise IOError,"addheader not called from eom()"
|
||||||
self.log('addheader: %s=%s' % (field,value))
|
self.log('addheader: %s=%s' % (field,value))
|
||||||
self._msg[field] = value
|
self._msg[field] = value
|
||||||
self._headerschanged = True
|
self._headerschanged = True
|
||||||
|
|
||||||
def delrcpt(self,rcpt):
|
def delrcpt(self,rcpt):
|
||||||
if not self._body:
|
if not self._body:
|
||||||
raise IOError("delrcpt not called from eom()")
|
raise IOError,"delrcpt not called from eom()"
|
||||||
self._delrcpt.append(rcpt)
|
self._delrcpt.append(rcpt)
|
||||||
|
|
||||||
def addrcpt(self,rcpt):
|
def addrcpt(self,rcpt):
|
||||||
if not self._body:
|
if not self._body:
|
||||||
raise IOError("addrcpt not called from eom()")
|
raise IOError,"addrcpt not called from eom()"
|
||||||
self._addrcpt.append(rcpt)
|
self._addrcpt.append(rcpt)
|
||||||
|
|
||||||
## Save the reply codes and messages in self._reply.
|
## Save the reply codes and messages in self._reply.
|
||||||
@@ -126,9 +103,9 @@ class TestBase(object):
|
|||||||
a += m
|
a += m
|
||||||
self._symlist[stage] = set(a)
|
self._symlist[stage] = set(a)
|
||||||
|
|
||||||
## Feed a file like object to the %milter. Calls envfrom, envrcpt for
|
## Feed a file like object to the milter. Calls envfrom, envrcpt for
|
||||||
# each recipient, header for each header field, body for each body
|
# each recipient, header for each header field, body for each body
|
||||||
# block, and finally eom. A return code from the %milter other than
|
# block, and finally eom. A return code from the milter other than
|
||||||
# CONTINUE returns immediately with that return code.
|
# CONTINUE returns immediately with that return code.
|
||||||
#
|
#
|
||||||
# This is a convenience method, a test could invoke the callbacks
|
# This is a convenience method, a test could invoke the callbacks
|
||||||
@@ -142,42 +119,57 @@ class TestBase(object):
|
|||||||
self._bodyreplaced = False
|
self._bodyreplaced = False
|
||||||
self._headerschanged = False
|
self._headerschanged = False
|
||||||
self._reply = None
|
self._reply = None
|
||||||
self._sender = '<%s>'%sender
|
msg = rfc822.Message(fp)
|
||||||
msg = mime.message_from_file(fp)
|
rc = self.envfrom('<%s>'%sender)
|
||||||
rc = self.envfrom(self._sender)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
if rc != Milter.CONTINUE: return rc
|
||||||
for rcpt in (rcpt,) + rcpts:
|
for rcpt in (rcpt,) + rcpts:
|
||||||
rc = self.envrcpt('<%s>'%rcpt)
|
rc = self.envrcpt('<%s>'%rcpt)
|
||||||
if rc != Milter.CONTINUE: return rc
|
if rc != Milter.CONTINUE: return rc
|
||||||
for h,val in msg.items():
|
line = None
|
||||||
rc = self.header(h,val)
|
for h in msg.headers:
|
||||||
|
if h[:1].isspace():
|
||||||
|
line = line + h
|
||||||
|
continue
|
||||||
|
if not line:
|
||||||
|
line = h
|
||||||
|
continue
|
||||||
|
s = line.split(': ',1)
|
||||||
|
if len(s) > 1: val = s[1].strip()
|
||||||
|
else: val = ''
|
||||||
|
rc = self.header(s[0],val)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
line = h
|
||||||
|
if line:
|
||||||
|
s = line.split(': ',1)
|
||||||
|
rc = self.header(s[0],s[1])
|
||||||
if rc != Milter.CONTINUE: return rc
|
if rc != Milter.CONTINUE: return rc
|
||||||
rc = self.eoh()
|
rc = self.eoh()
|
||||||
if rc != Milter.CONTINUE: return rc
|
if rc != Milter.CONTINUE: return rc
|
||||||
header,body = msg.as_bytes().split(b'\n\n',1)
|
|
||||||
bfp = BytesIO(body)
|
|
||||||
while 1:
|
while 1:
|
||||||
buf = bfp.read(8192)
|
buf = fp.read(8192)
|
||||||
if len(buf) == 0: break
|
if len(buf) == 0: break
|
||||||
rc = self.body(buf)
|
rc = self.body(buf)
|
||||||
if rc != Milter.CONTINUE: return rc
|
if rc != Milter.CONTINUE: return rc
|
||||||
self._msg = msg
|
self._msg = msg
|
||||||
self._body = BytesIO()
|
self._body = StringIO.StringIO()
|
||||||
rc = self.eom()
|
rc = self.eom()
|
||||||
if self._bodyreplaced:
|
if self._bodyreplaced:
|
||||||
body = self._body.getvalue()
|
body = self._body.getvalue()
|
||||||
self._body = BytesIO()
|
else:
|
||||||
self._body.write(header)
|
msg.rewindbody()
|
||||||
self._body.write(b'\n\n')
|
body = msg.fp.read()
|
||||||
|
self._body = StringIO.StringIO()
|
||||||
|
self._body.writelines(msg.headers)
|
||||||
|
self._body.write('\n')
|
||||||
self._body.write(body)
|
self._body.write(body)
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
## Feed an email contained in a file to the %milter.
|
## Feed an email contained in a file to the milter.
|
||||||
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
||||||
# @param sender MAIL FROM
|
# @param sender MAIL FROM
|
||||||
# @param rcpts RCPT TO, multiple recipients may be supplied
|
# @param rcpts RCPT TO, multiple recipients may be supplied
|
||||||
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
||||||
with open('test/'+fname,'rb') as fp:
|
with open('test/'+fname,'r') as fp:
|
||||||
return self.feedFile(fp,sender,*rcpts)
|
return self.feedFile(fp,sender,*rcpts)
|
||||||
|
|
||||||
## Call the connect and helo callbacks.
|
## Call the connect and helo callbacks.
|
||||||
|
|||||||
+18
-45
@@ -5,13 +5,12 @@
|
|||||||
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
|
|
||||||
from fnmatch import fnmatchcase
|
from fnmatch import fnmatchcase
|
||||||
from binascii import a2b_base64
|
from email.Header import decode_header
|
||||||
|
#import email.Utils
|
||||||
|
import rfc822
|
||||||
|
|
||||||
dnsre = re.compile(r'^[a-z][-a-z\d.]+$', re.IGNORECASE)
|
|
||||||
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
||||||
ip4re = re.compile(PAT_IP4+'$')
|
ip4re = re.compile(PAT_IP4+'$')
|
||||||
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
|
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
|
||||||
@@ -54,8 +53,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
|
||||||
@@ -68,12 +67,6 @@ def iniplist(ipaddr,iplist):
|
|||||||
True
|
True
|
||||||
>>> iniplist('192.168.0.45',['192.168.0.*'])
|
>>> iniplist('192.168.0.45',['192.168.0.*'])
|
||||||
True
|
True
|
||||||
>>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
|
|
||||||
True
|
|
||||||
>>> iniplist('2607:f8b0:4004:801::',['google.com/40'])
|
|
||||||
True
|
|
||||||
>>> iniplist('4.2.2.2',['nothing.example.com'])
|
|
||||||
False
|
|
||||||
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
||||||
True
|
True
|
||||||
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
||||||
@@ -82,10 +75,8 @@ def iniplist(ipaddr,iplist):
|
|||||||
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
|
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
|
||||||
"""
|
"""
|
||||||
if ip4re.match(ipaddr):
|
if ip4re.match(ipaddr):
|
||||||
fam = socket.AF_INET
|
|
||||||
ipnum = addr2bin(ipaddr)
|
ipnum = addr2bin(ipaddr)
|
||||||
elif ip6re.match(ipaddr):
|
elif ip6re.match(ipaddr):
|
||||||
fam = socket.AF_INET6
|
|
||||||
ipnum = bin2long6(inet_pton(ipaddr))
|
ipnum = bin2long6(inet_pton(ipaddr))
|
||||||
else:
|
else:
|
||||||
raise ValueError('Invalid ip syntax:'+ipaddr)
|
raise ValueError('Invalid ip syntax:'+ipaddr)
|
||||||
@@ -105,21 +96,13 @@ def iniplist(ipaddr,iplist):
|
|||||||
n = 128
|
n = 128
|
||||||
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
|
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
|
||||||
return True
|
return True
|
||||||
elif dnsre.match(p[0]):
|
|
||||||
try:
|
|
||||||
sfx = '/'.join(['']+p[1:])
|
|
||||||
addrlist = [r[4][0]+sfx for r in socket.getaddrinfo(p[0],25,fam)]
|
|
||||||
if iniplist(ipaddr,addrlist):
|
|
||||||
return True
|
|
||||||
except socket.gaierror: pass
|
|
||||||
elif fnmatchcase(ipaddr,pat):
|
elif fnmatchcase(ipaddr,pat):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Split email into Fullname and address.
|
## Split email into Fullname and address.
|
||||||
# This replaces <code>email.utils.parseaddr</code> but fixes
|
# This replaces <code>email.Utils.parseaddr</code> but fixes
|
||||||
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
|
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
|
||||||
# Additional tricky cases are still broken. Patches welcome.
|
|
||||||
#
|
#
|
||||||
def parseaddr(t):
|
def parseaddr(t):
|
||||||
"""Split email into Fullname and address.
|
"""Split email into Fullname and address.
|
||||||
@@ -133,12 +116,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')
|
>>> parseaddr('a(WRONG)@b')
|
||||||
('WRONG', 'a@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('<')
|
||||||
@@ -147,7 +130,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] == '>':
|
||||||
@@ -155,19 +138,9 @@ 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
|
|
||||||
def decode(s, convert_eols=None):
|
|
||||||
if not s: return s
|
|
||||||
while len(s) % 4: s += '=' # add missing padding
|
|
||||||
dec = a2b_base64(s)
|
|
||||||
if convert_eols:
|
|
||||||
return dec.replace(CRLF, convert_eols)
|
|
||||||
return dec
|
|
||||||
|
|
||||||
email.base64mime.decode = decode
|
|
||||||
|
|
||||||
def parse_addr(t):
|
def parse_addr(t):
|
||||||
"""Split email into user,domain.
|
"""Split email into user,domain.
|
||||||
@@ -212,18 +185,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 = ''.join(u)
|
||||||
for enc in ('us-ascii','iso-8859-1','utf-8'):
|
for enc in ('us-ascii','iso-8859-1','utf8'):
|
||||||
try:
|
try:
|
||||||
return u.encode(enc)
|
return u.encode(enc)
|
||||||
except UnicodeError: continue
|
except UnicodeError: continue
|
||||||
except UnicodeDecodeError: pass
|
except UnicodeDecodeError: pass
|
||||||
except LookupError: pass
|
except LookupError: pass
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
except email.errors.HeaderParseError: pass
|
except email.Errors.HeaderParseError: pass
|
||||||
return val
|
return val
|
||||||
|
|||||||
@@ -11,24 +11,25 @@ any point, tell Sendmail to reject, discard, or accept the message.
|
|||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Python milter extension: http://https://pypi.python.org/pypi/pymilter/
|
This python milter extension: http://www.bmsi.com/python/milter.html
|
||||||
Python: http://www.python.org
|
Python: http://www.python.org
|
||||||
Sendmail: http://www.sendmail.org
|
Sendmail: http://www.sendmail.org
|
||||||
|
|
||||||
NB: From Sendmail's libmilter/README:
|
NB: From Sendmail's libmilter/README:
|
||||||
|
|
||||||
libmilter requires pthread support in the operating system. Moreover, it
|
libmilter requires pthread support in the operating system. Moreover, it
|
||||||
requires that the library functions it uses are thread safe; which is true
|
requires that the library functions it uses are thread safe; which is true
|
||||||
for the operating systems libmilter has been developed and tested on. On
|
for the operating systems libmilter has been developed and tested on. On
|
||||||
some operating systems this requires special compile time options (e.g.,
|
some operating systems this requires special compile time options (e.g.,
|
||||||
not just -pthread). libmilter is currently known to work on (modulo problems
|
not just -pthread). libmilter is currently known to work on (modulo
|
||||||
in the pthread support of some specific versions):
|
problems in the pthread support of some specific versions):
|
||||||
|
|
||||||
FreeBSD 3.x, 4.x
|
FreeBSD 3.x, 4.x
|
||||||
SunOS 5.x (x >= 5)
|
SunOS 5.x (x >= 5)
|
||||||
AIX 4.3.x
|
AIX 4.3.x
|
||||||
HP UX 11.x
|
HP UX 11.x
|
||||||
Linux (recent versions/distributions)
|
Linux (recent versions/distributions)
|
||||||
|
OpenBSD
|
||||||
|
AIX 4.1.5
|
||||||
|
|
||||||
libmilter is currently not supported on:
|
libmilter is currently not supported on:
|
||||||
|
|
||||||
@@ -109,9 +110,33 @@ _FFR_MILTER for the cf macros. For example,
|
|||||||
|
|
||||||
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
|
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
|
||||||
|
|
||||||
|
|
||||||
|
RedHat 6.2 Notes
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The Redhat 6.2 sendmail RPM does not enable milter. You can obtain a
|
||||||
|
modified spec file at
|
||||||
|
|
||||||
|
http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec
|
||||||
|
|
||||||
|
use it to rebuild the Redhat 7.2 SRPM. The RH6.2 SRPM does not have
|
||||||
|
recent sendmail security patches.
|
||||||
|
|
||||||
|
RedHat 7.2 Notes
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The Redhat 7.2 sendmail RPM enables milter in sendmail - but does not include
|
||||||
|
the headers needed for compiling a milter. You can obtain a modified spec
|
||||||
|
file with a sendmail-devel package that includes the needed static libraries
|
||||||
|
and headers at
|
||||||
|
|
||||||
|
http://www.bmsi.com/linux/sendmail-rh72.spec
|
||||||
|
|
||||||
IPv6 Notes
|
IPv6 Notes
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
IPv6 is still experimental.
|
||||||
|
|
||||||
The IPv6 protocol is supported if your operation system supports it
|
The IPv6 protocol is supported if your operation system supports it
|
||||||
and if sendmail was compiled with IPv6 support. To determine if your
|
and if sendmail was compiled with IPv6 support. To determine if your
|
||||||
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
|
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
|
||||||
@@ -169,7 +194,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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+14
-49
@@ -1,48 +1,46 @@
|
|||||||
## @mainpage Writing Milters in Python
|
## @mainpage Writing Milters in Python
|
||||||
#
|
#
|
||||||
# At the lowest level, the <code>milter</code> module provides a thin wrapper
|
# At the lowest level, the <code>milter</code> module provides a thin wrapper
|
||||||
# around the <a href="milter_api/index.html"> sendmail
|
# around the <a href="https://www.milter.org/developers/api/index"> sendmail
|
||||||
# libmilter API</a>. This API lets you register callbacks for a number of
|
# libmilter API</a>. This API lets you register callbacks for a number of
|
||||||
# events in the process of sendmail receiving a message via SMTP. These
|
# events in the process of sendmail receiving a message via SMTP. These
|
||||||
# events include the initial connection from a MTA, the envelope sender and
|
# events include the initial connection from a MTA, the envelope sender and
|
||||||
# recipients, the top level mail headers, and the message body. There are
|
# recipients, the top level mail headers, and the message body. There are
|
||||||
# options to mangle all of these components of the message as it passes through
|
# options to mangle all of these components of the message as it passes through
|
||||||
# the %milter.
|
# the milter.
|
||||||
#
|
#
|
||||||
# At the next level, the <code>Milter</code> module (note the case difference)
|
# At the next level, the <code>Milter</code> module (note the case difference)
|
||||||
# provides a Python friendly object oriented wrapper for the low level API. To
|
# provides a Python friendly object oriented wrapper for the low level API. To
|
||||||
# use the Milter module, an application registers a 'factory' to create an
|
# use the Milter module, an application registers a 'factory' to create an
|
||||||
# object for each connection from a MTA to sendmail. These connection objects
|
# object for each connection from a MTA to sendmail. These connection objects
|
||||||
# must provide methods corresponding to the libmilter 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.
|
||||||
#
|
#
|
||||||
# @section threading
|
# @section threading
|
||||||
#
|
#
|
||||||
# The libmilter library which pymilter wraps
|
# The libmilter library which pymilter wraps
|
||||||
# <a href="milter_overview#SignalHandling">handles
|
# <a href="https://www.milter.org/developers/overview#SignalHandling">handles
|
||||||
# all signals</a> itself, and expects to be called from a single main thread.
|
# all signals</a> itself, and expects to be called from a single main thread.
|
||||||
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
|
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
|
||||||
# <a href="milter_api/smfi_stop.html">smfi_stop</a>
|
# <a href="https://www.milter.org/developers/api/smfi_stop">smfi_stop</a>
|
||||||
# and the last to an internal ABORT.
|
# and the last to an internal ABORT.
|
||||||
#
|
#
|
||||||
# If you use python threads or threading modules, then signal handling gets
|
# If you use python threads or threading modules, then signal handling gets
|
||||||
@@ -52,37 +50,4 @@
|
|||||||
# You may find the
|
# You may find the
|
||||||
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
|
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
|
||||||
# multiprocessing</a> module useful. It can be a drop-in
|
# multiprocessing</a> module useful. It can be a drop-in
|
||||||
# replacement for threading as illustrated in
|
# replacement for threading as illustrated in @ref milter-template.py.
|
||||||
# <a href="milter-template_8py-example.html">milter-template.py</a>.
|
|
||||||
#
|
|
||||||
# @section Useful python packages for milters
|
|
||||||
#
|
|
||||||
# <a href="https://pypi.python.org/pypi/pyspf">pyspf</a> checks the
|
|
||||||
# SMTP envelope sender (MAIL FROM, passed to the Milter.Base.envfrom callback)
|
|
||||||
# against a Sender Policy published in DNS by the sending domain. This
|
|
||||||
# can prevent forgery of the MAIL FROM. SPF is Sender Policy Framework.
|
|
||||||
#
|
|
||||||
# <a href="https://launchpad.net/dkimpy">pydkim</a> checks a DKIM signature
|
|
||||||
# of the email body and headers against a public key published in DNS by
|
|
||||||
# the signing domain. DKIM is DomainKeys Identified Mail.
|
|
||||||
#
|
|
||||||
# The <a href="https://pypi.python.org/pypi/authres/">authres</a> module
|
|
||||||
# parses and formats the Authentication-Results email header, providing
|
|
||||||
# a standard place to summarize the results from DKIM, SPF, rDNS, SMTP AUTH,
|
|
||||||
# and other email authentication methods.
|
|
||||||
#
|
|
||||||
# <a href="https://pypi.python.org/pypi/pydspam/">pydspam</a> wraps
|
|
||||||
# the libdspam API of the <a href="http://dspam.sourceforge.net/">DSPAM</a>
|
|
||||||
# project.
|
|
||||||
#
|
|
||||||
# @section Milters written with pymilter
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/croessner/vrfydmn">Verify Domain</a> is a
|
|
||||||
# Postfix milter that rejects/fixes manipulated From: header
|
|
||||||
# on a mail host with multiple virtual domains.
|
|
||||||
#
|
|
||||||
# <a href="https://pypi.python.org/pypi/milter/">BMS Milter</a> has several
|
|
||||||
# milters, a big complicated spam filter that integrates multiple
|
|
||||||
# authentication protocols with pydpsm, and two simple ones: spfmilter.py and
|
|
||||||
# dkim-milter.py.
|
|
||||||
#
|
|
||||||
|
|||||||
+42
-116
@@ -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>
|
||||||
@@ -75,52 +20,51 @@ NOREPLY = 6
|
|||||||
# and converts function callbacks to instance method invocations.
|
# and converts function callbacks to instance method invocations.
|
||||||
#
|
#
|
||||||
class milterContext(object):
|
class milterContext(object):
|
||||||
## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_getsymval">smfi_getsymval</a>.
|
||||||
def getsymval(self,sym): pass
|
def getsymval(self,sym): pass
|
||||||
## Calls <a href="milter_api/smfi_setreply.html">
|
## Calls <a href="https://www.milter.org/developers/api/smfi_setreply">
|
||||||
# smfi_setreply</a> or
|
# smfi_setreply</a> or
|
||||||
# <a href="milter_api/smfi_setmlreply.html">
|
# <a href="https://www.milter.org/developers/api/smfi_setmlreply">
|
||||||
# smfi_setmlreply</a>.
|
# smfi_setmlreply</a>.
|
||||||
# @param rcode SMTP response code
|
# @param rcode SMTP response code
|
||||||
# @param xcode extended SMTP response code
|
# @param xcode extended SMTP response code
|
||||||
# @param msg one or more message lines. If the MTA does not support
|
# @param msg one or more message lines. If the MTA does not support
|
||||||
# multiline messages, only the first is used.
|
# multiline messages, only the first is used.
|
||||||
def setreply(self,rcode,xcode,*msg): pass
|
def setreply(self,rcode,xcode,*msg): pass
|
||||||
## Calls <a href="milter_api/smfi_addheader.html">smfi_addheader</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_addheader">smfi_addheader</a>.
|
||||||
def addheader(self,name,value,idx=-1): pass
|
def addheader(self,name,value,idx=-1): pass
|
||||||
## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">smfi_chgheader</a>.
|
||||||
def chgheader(self,name,idx,value): pass
|
def chgheader(self,name,idx,value): pass
|
||||||
## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">smfi_addrcpt</a>.
|
||||||
def addrcpt(self,rcpt,params=None): pass
|
def addrcpt(self,rcpt,params=None): pass
|
||||||
## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">smfi_delrcpt</a>.
|
||||||
def delrcpt(self,rcpt): pass
|
def delrcpt(self,rcpt): pass
|
||||||
## Calls <a href="milter_api/smfi_replacebody.html">smfi_replacebody</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">smfi_replacebody</a>.
|
||||||
def replacebody(self,data): pass
|
def replacebody(self,data): pass
|
||||||
## Attach a Python object to this connection context.
|
## Attach a Python object to this connection context.
|
||||||
# @return the old value or None
|
# @return the old value or None
|
||||||
def setpriv(self,priv): pass
|
def setpriv(self,priv): pass
|
||||||
## Return the Python object attached to this connection context.
|
## Return the Python object attached to this connection context.
|
||||||
def getpriv(self): pass
|
def getpriv(self): pass
|
||||||
## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">smfi_quarantine</a>.
|
||||||
def quarantine(self,reason): pass
|
def quarantine(self,reason): pass
|
||||||
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_progress">smfi_progress</a>.
|
||||||
def progress(self): pass
|
def progress(self): pass
|
||||||
## Calls <a href="milter_api/smfi_chgfrom.html">smfi_chgfrom</a>.
|
## Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">smfi_chgfrom</a>.
|
||||||
def chgfrom(self,sender,param=None): pass
|
def chgfrom(self,sender,param=None): pass
|
||||||
## Tell the MTA which macro values we are interested in for a given stage.
|
## Tell the MTA which macro values we are interested in for a given stage.
|
||||||
# Of interest only when you need to squeeze a few more bytes of bandwidth.
|
# Of interest only when you need to squeeze a few more bytes of bandwidth.
|
||||||
# It may only be called from the negotiate callback.
|
# It may only be called from the negotiate callback.
|
||||||
# The protocol stages are
|
# The protocol stages are
|
||||||
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
|
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
|
||||||
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>.
|
# Calls <a href="https://www.milter.org/developers/api/smfi_setsymlist">smfi_setsymlist</a>.
|
||||||
# @param stage protocol stage in which the macro list should be used
|
# @param stage protocol stage in which the macro list should be used
|
||||||
# @param macrolist a space separated list of macro names
|
|
||||||
def setsymlist(self,stage,macrolist): pass
|
def setsymlist(self,stage,macrolist): pass
|
||||||
|
|
||||||
class error(Exception): pass
|
class error(Exception): pass
|
||||||
|
|
||||||
## Enable optional %milter actions.
|
## Enable optional milter actions.
|
||||||
# Certain %milter actions need to be enabled before calling main()
|
# Certain milter actions need to be enabled before calling main()
|
||||||
# or they throw an exception. Pymilter enables them all by
|
# or they throw an exception. Pymilter enables them all by
|
||||||
# default (since 0.9.2), but you may wish to disable unneeded
|
# default (since 0.9.2), but you may wish to disable unneeded
|
||||||
# actions as an optimization.
|
# actions as an optimization.
|
||||||
@@ -138,72 +82,59 @@ 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. The default is TEMPFAIL.
|
||||||
# application should have its own top level exception handler for each
|
# You should not depend on this handler. Your application should
|
||||||
# callback. You can then choose your own reply message, log the stack track
|
# have its own top level exception handler for each callback. You can
|
||||||
# were you please, and so on. However, if you miss one, this last ditch
|
# then choose your own reply message, log the stack track were you please,
|
||||||
# handler will print a standard stack trace to sys.stderr, and return to
|
# and so on. However, if you miss one, this last ditch handler will
|
||||||
# sendmail.
|
# 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="https://www.milter.org/developers/api/xxfi_negotiate">
|
||||||
# xxfi_negotiate</a> callback, called to negotiate supported
|
# xxfi_negotiate</a> callback, called to negotiate supported
|
||||||
# actions, callbacks, and protocol steps.
|
# actions, callbacks, and protocol steps.
|
||||||
# @param unknown the
|
# @param unknown the
|
||||||
# <a href="milter_api/xxfi_unknown.html">
|
# <a href="https://www.milter.org/developers/api/xxfi_unknown">
|
||||||
# xxfi_unknown</a> callback, called when for SMTP commands
|
# xxfi_unknown</a> callback, called when for SMTP commands
|
||||||
# not recognized by the MTA. (Extend SMTP in your milter!)
|
# not recognized by the MTA. (Extend SMTP in your milter!)
|
||||||
# @param data the
|
# @param data the
|
||||||
# <a href="milter_api/xxfi_data.html">
|
# <a href="https://www.milter.org/developers/api/xxfi_data">
|
||||||
# xxfi_data</a> callback, called when the DATA
|
# xxfi_data</a> callback, called when the DATA
|
||||||
# SMTP command is received.
|
# SMTP command is received.
|
||||||
def register(name,negotiate=None,unknown=None,data=None): pass
|
def register(name,negotiate=None,unknown=None,data=None): pass
|
||||||
|
|
||||||
## Attempt to create the socket used to communicate with the MTA.
|
|
||||||
# milter.opensocket() attempts to create the socket specified previously by a
|
|
||||||
# call to milter.setconn() which will be the interface between MTAs and the
|
|
||||||
# %milter. This allows the calling application to ensure that the socket can be
|
|
||||||
# created. If this is not called, milter.main() will do so implicitly.
|
|
||||||
# Calls <a href="milter_api/smfi_opensocket.html">
|
|
||||||
# smfi_opensocket</a>. While not documented for libmilter, my experiments
|
|
||||||
# indicate that you must call register() before calling opensocket().
|
|
||||||
# @param rmsock Try to remove an existing unix domain socket if true.
|
|
||||||
def opensocket(rmsock): pass
|
def opensocket(rmsock): pass
|
||||||
|
|
||||||
## Transfer control to libmilter.
|
## Transfer control to libmilter.
|
||||||
# Calls <a href="milter_api/smfi_main.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_main">
|
||||||
# smfi_main</a>.
|
# smfi_main</a>.
|
||||||
def main(): pass
|
def main(): pass
|
||||||
|
|
||||||
## Set the libmilter debugging level.
|
## Set the libmilter debugging level.
|
||||||
# <a href="milter_api/smfi_setdbg.html">smfi_setdbg</a>
|
# <a href="https://www.milter.org/developers/api/smfi_setdbg">smfi_setdbg</a>
|
||||||
# sets the %milter library's internal debugging level to a new level
|
# sets the milter library's internal debugging level to a new level
|
||||||
# so that code details may be traced. A level of zero turns off debugging. The
|
# so that code details may be traced. A level of zero turns off debugging. The
|
||||||
# greater (more positive) the level the more detailed the debugging. Six is the
|
# greater (more positive) the level the more detailed the debugging. Six is the
|
||||||
# current, highest, useful value. Must be called before calling main().
|
# current, highest, useful value. Must be called before calling main().
|
||||||
def setdbg(lev): pass
|
def setdbg(lev): pass
|
||||||
|
|
||||||
## Set timeout for MTA communication.
|
## Set timeout for MTA communication.
|
||||||
# Calls <a href="milter_api/smfi_settimeout.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_settimeout">
|
||||||
# smfi_settimeout</a>. Must be called before calling main().
|
# smfi_settimeout</a>. Must be called before calling main().
|
||||||
def settimeout(secs): pass
|
def settimeout(secs): pass
|
||||||
|
|
||||||
## Set socket backlog.
|
## Set socket backlog.
|
||||||
# Calls <a href="milter_api/smfi_setbacklog.html">
|
# Calls <a href="https://www.milter.org/developers/api/smfi_setbacklog">
|
||||||
# smfi_setbacklog</a>. Must be called before calling main().
|
# smfi_setbacklog</a>. Must be called before calling main().
|
||||||
def setbacklog(n): pass
|
def setbacklog(n): pass
|
||||||
|
|
||||||
@@ -212,21 +143,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.
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
web:
|
web:
|
||||||
doxygen
|
doxygen
|
||||||
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-devel-* doc/html/milter_api
|
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
|
||||||
rsync -ravKk doc/html/ spidey2.bmsi.com:/Public/pymilter
|
|
||||||
cd doc/html; zip -r ../../doc .
|
|
||||||
|
|
||||||
VERSION=1.0
|
VERSION=0.9.8
|
||||||
CVSTAG=pymilter-1_0
|
CVSTAG=pymilter-0_9_8
|
||||||
PKG=pymilter-$(VERSION)
|
PKG=pymilter-$(VERSION)
|
||||||
SRCTAR=$(PKG).tar.gz
|
SRCTAR=$(PKG).tar.gz
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -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
|
||||||
@@ -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()
|
||||||
|
|||||||
+9
-17
@@ -1,18 +1,14 @@
|
|||||||
## To roll your own milter, create a class that extends Milter.
|
## To roll your own milter, create a class that extends Milter.
|
||||||
# See the pymilter project at http://bmsi.com/python/milter.html
|
# See the pymilter project at http://bmsi.com/python/milter.html
|
||||||
# based on Sendmail's milter API
|
# based on Sendmail's milter API http://www.milter.org/milter_api/api.html
|
||||||
# This code is open-source on the same terms as Python.
|
# This code is open-source on the same terms as Python.
|
||||||
|
|
||||||
## Milter calls methods of your class at milter events.
|
## Milter calls methods of your class at milter events.
|
||||||
## 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()
|
||||||
|
|||||||
-160
@@ -1,160 +0,0 @@
|
|||||||
diff --git a/miltermodule.c b/miltermodule.c
|
|
||||||
index aa10a08..af9a144 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);
|
|
||||||
@@ -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,
|
|
||||||
+ PyVarObject_HEAD_INIT(&PyType_Type,0)
|
|
||||||
"milterContext",
|
|
||||||
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,27 @@ 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);
|
|
||||||
+ m = PyModule_Create(&moduledef);
|
|
||||||
d = PyModule_GetDict(m);
|
|
||||||
MilterError = PyErr_NewException("milter.error", NULL, NULL);
|
|
||||||
PyDict_SetItemString(d,"error", MilterError);
|
|
||||||
@@ -1710,4 +1721,5 @@ initmilter(void) {
|
|
||||||
setitem(d,"DISCARD", SMFIS_DISCARD);
|
|
||||||
setitem(d,"ACCEPT", SMFIS_ACCEPT);
|
|
||||||
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
|
|
||||||
+ return m;
|
|
||||||
}
|
|
||||||
+3
-10
@@ -35,9 +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
|
* Revision 1.34 2013/03/09 05:42:14 customdesigned
|
||||||
* Make TestBase members private, fix getsymlist misspelling.
|
* Make TestBase members private, fix getsymlist misspelling.
|
||||||
*
|
*
|
||||||
@@ -644,8 +641,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;
|
||||||
@@ -665,9 +661,9 @@ _release_thread(PyThreadState *t) {
|
|||||||
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];
|
char untrapped_msg[80];
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
sprintf(untrapped_msg,"pymilter: untrapped exception in %.40s",
|
sprintf(untrapped_msg,"pymilter: untrapped exception in %.40s",
|
||||||
description.xxfi_name);
|
description.xxfi_name);
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
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);
|
||||||
@@ -679,11 +675,8 @@ static int _report_exception(milter_ContextObject *self) {
|
|||||||
smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg);
|
smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,33 +93,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,7 +123,7 @@ 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
|
||||||
@@ -140,7 +133,7 @@ class MimeGenerator(BytesGenerator):
|
|||||||
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."""
|
||||||
@@ -157,17 +150,19 @@ def unquote(s):
|
|||||||
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,7 +203,7 @@ 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]:
|
||||||
@@ -243,9 +238,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()
|
||||||
|
|
||||||
@@ -306,7 +301,7 @@ class MimeMessage(Message):
|
|||||||
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 +312,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."
|
||||||
@@ -356,6 +351,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
|
||||||
@@ -401,18 +398,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 +440,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
|
||||||
@@ -486,14 +483,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
|
||||||
def end_script(self):
|
|
||||||
#print('end script')
|
|
||||||
self.ignoring -= 1
|
|
||||||
if not self.ignoring:
|
|
||||||
self.out.write(self.msg)
|
self.out.write(self.msg)
|
||||||
|
def end_script(self):
|
||||||
|
self.ignoring -= 1
|
||||||
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):
|
||||||
@@ -508,14 +502,14 @@ def check_html(msg,savname=None):
|
|||||||
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 +532,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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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" $@
|
|
||||||
-192
@@ -1,192 +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.1
|
|
||||||
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 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
|
|
||||||
+31
-65
@@ -1,32 +1,22 @@
|
|||||||
%define __python python2
|
%define __python python2.6
|
||||||
%if 0%{?rhel} == 6
|
|
||||||
%define pythonbase python
|
%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.1
|
Version: 0.9.8
|
||||||
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,44 +71,17 @@ 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 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
|
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1
|
||||||
- Add Milter.test module for unit testing milters.
|
- Add Milter.test module for unit testing milters.
|
||||||
- Fix typo that prevented setsymlist from being active.
|
- Fix typo that prevented setsymlist from being active.
|
||||||
- Change untrapped exception message to:
|
- Change untrapped exception message to:
|
||||||
- "pymilter: untrapped exception in milter app"
|
- "pymilter: untrapped exception in milter app"
|
||||||
|
|
||||||
* Thu Apr 12 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
|
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
|
||||||
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
|
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
|
||||||
- Remove redundant table in miltermodule
|
- Remove redundant table in miltermodule
|
||||||
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
|
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
|
||||||
@@ -130,7 +96,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
@@ -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 };
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from __future__ import print_function
|
|
||||||
# A simple milter.
|
# A simple milter.
|
||||||
|
|
||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
@@ -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,9 +21,9 @@ class sampleMilter(Milter.Milter):
|
|||||||
"Milter to replace attachments poisonous to Windows with a WARNING message."
|
"Milter to replace attachments poisonous to Windows with a WARNING message."
|
||||||
|
|
||||||
def log(self,*msg):
|
def log(self,*msg):
|
||||||
print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),end=None)
|
print "%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),
|
||||||
for i in msg: print(i,end=None)
|
for i in msg: print i,
|
||||||
print()
|
print
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.tempname = None
|
self.tempname = None
|
||||||
@@ -41,7 +39,7 @@ class sampleMilter(Milter.Milter):
|
|||||||
def envfrom(self,f,*str):
|
def envfrom(self,f,*str):
|
||||||
"start of MAIL transaction"
|
"start of MAIL transaction"
|
||||||
self.log("mail from",f,str)
|
self.log("mail from",f,str)
|
||||||
self.fp = BytesIO()
|
self.fp = StringIO.StringIO()
|
||||||
self.tempname = None
|
self.tempname = None
|
||||||
self.mailfrom = f
|
self.mailfrom = f
|
||||||
self.bodysize = 0
|
self.bodysize = 0
|
||||||
@@ -97,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()
|
||||||
@@ -140,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):
|
||||||
@@ -170,13 +171,13 @@ if __name__ == "__main__":
|
|||||||
socketname = os.getenv("HOME") + "/pythonsock"
|
socketname = os.getenv("HOME") + "/pythonsock"
|
||||||
Milter.factory = sampleMilter
|
Milter.factory = sampleMilter
|
||||||
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS)
|
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS)
|
||||||
print("""To use this with sendmail, add the following to sendmail.cf:
|
print """To use this with sendmail, add the following to sendmail.cf:
|
||||||
|
|
||||||
O InputMailFilters=pythonfilter
|
O InputMailFilters=pythonfilter
|
||||||
Xpythonfilter, S=local:%s
|
Xpythonfilter, S=local:%s
|
||||||
|
|
||||||
See the sendmail README for libmilter.
|
See the sendmail README for libmilter.
|
||||||
sample milter startup""" % socketname)
|
sample milter startup""" % socketname
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
Milter.runmilter("pythonfilter",socketname,240)
|
Milter.runmilter("pythonfilter",socketname,240)
|
||||||
print("sample milter shutdown")
|
print "sample milter shutdown"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[bdist_rpm]
|
[bdist_rpm]
|
||||||
python=python3
|
python=python2.6
|
||||||
doc_files=README NEWS TODO COPYING CREDITS
|
doc_files=README NEWS TODO
|
||||||
packager=Stuart D. Gathman <stuart@gathman.org>
|
packager=Stuart D. Gathman <stuart@bmsi.com>
|
||||||
release=1
|
release=1
|
||||||
|
|||||||
@@ -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.1',
|
setup(name = "pymilter", version = '0.9.8',
|
||||||
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
|
||||||
@@ -31,7 +27,7 @@ sending DSNs or doing CBVs.
|
|||||||
maintainer_email="stuart@bmsi.com",
|
maintainer_email="stuart@bmsi.com",
|
||||||
license="GPL",
|
license="GPL",
|
||||||
url="http://www.bmsi.com/python/milter.html",
|
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
@@ -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()
|
|
||||||
@@ -8,7 +8,6 @@ class GreylistTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.fname = 'test.db'
|
self.fname = 'test.db'
|
||||||
if os.path.isfile(self.fname):
|
|
||||||
os.remove(self.fname)
|
os.remove(self.fname)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
|||||||
+50
-78
@@ -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;Girlz.jpg"')
|
plist = mime._parseparam('; name="Jim&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;Girlz.jpg"')
|
||||||
self.assertTrue(plist[0] == 'name="Jim&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;Girlz.jpg","name=%s"%name)
|
self.failUnless(name == "Jim&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()
|
||||||
|
|||||||
+6
-4
@@ -2,6 +2,8 @@ import unittest
|
|||||||
import Milter
|
import Milter
|
||||||
import sample
|
import sample
|
||||||
import mime
|
import mime
|
||||||
|
import rfc822
|
||||||
|
import StringIO
|
||||||
from Milter.test import TestBase
|
from Milter.test import TestBase
|
||||||
|
|
||||||
class TestMilter(TestBase,sample.sampleMilter):
|
class TestMilter(TestBase,sample.sampleMilter):
|
||||||
@@ -19,7 +21,7 @@ class BMSMilterTestCase(unittest.TestCase):
|
|||||||
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)
|
||||||
@@ -34,7 +36,7 @@ class BMSMilterTestCase(unittest.TestCase):
|
|||||||
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,14 +49,14 @@ class BMSMilterTestCase(unittest.TestCase):
|
|||||||
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')
|
||||||
|
|||||||
+3
-11
@@ -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):
|
||||||
|
|
||||||
@@ -13,7 +11,6 @@ class AddrCacheTestCase(unittest.TestCase):
|
|||||||
self.fname = 'test.dat'
|
self.fname = 'test.dat'
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
if os.path.exists(self.fname):
|
|
||||||
os.remove(self.fname)
|
os.remove(self.fname)
|
||||||
|
|
||||||
def testAdd(self):
|
def testAdd(self):
|
||||||
@@ -34,22 +31,17 @@ 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)
|
||||||
|
|
||||||
def testParseHeader(self):
|
|
||||||
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
|
|
||||||
h = Milter.utils.parse_header(s)
|
|
||||||
self.assertEqual(h,b'Last Few Coldplay Album Artworks Available\x00')
|
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
s = unittest.makeSuite(AddrCacheTestCase,'test')
|
s = unittest.makeSuite(AddrCacheTestCase,'test')
|
||||||
s.addTest(doctest.DocTestSuite(Milter.utils))
|
s.addTest(doctest.DocTestSuite(Milter.utils))
|
||||||
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__':
|
||||||
|
|||||||
Reference in New Issue
Block a user