Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7ef47d76b | |||
| 8f7c090879 | |||
| d69c002020 | |||
| 980dc5f599 | |||
| 8770262622 | |||
| af49a7a45e | |||
| fca8d83370 | |||
| f28cab2d1c | |||
| 76424c7c3f | |||
| 3e1754acff | |||
| 40de08925d | |||
| 522a631192 | |||
| 5c8c189330 | |||
| 5330047902 | |||
| a8f373ea65 | |||
| f357be1e99 | |||
| 84eeecf9a6 | |||
| a180b212c6 | |||
| bd0df5d77a | |||
| 34746823f7 | |||
| baeddd9fa5 | |||
| 4854f95b59 | |||
| 242f2fa78f | |||
| 1e0324399b | |||
| 078d9f2078 | |||
| ff06b5f1b4 | |||
| dd581f5d9a | |||
| 3fb9beb5c0 | |||
| b12c4c9746 | |||
| f3fbb1c99d | |||
| 27887daf3f | |||
| 23defb880b | |||
| 7502c29e47 | |||
| 594d3ad365 | |||
| b2e0b2ebc6 | |||
| 04a241f1e9 | |||
| 16bfe5d4da | |||
| 70d19001c0 | |||
| 0d001dd8e9 | |||
| 8f4a82794c | |||
| de0ec3430d | |||
| c9e32e4b06 | |||
| 83a1762515 | |||
| feb6526cb8 | |||
| 3a3add814e |
@@ -1,5 +1,5 @@
|
|||||||
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
||||||
and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that
|
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
|
||||||
kludge and added threading and context objects to it, wrote a proper OO
|
kludge and added threading and context objects to it, wrote a proper OO
|
||||||
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
||||||
it with distutils, and generally transformed it from a quick hack to a
|
it with distutils, and generally transformed it from a quick hack to a
|
||||||
@@ -7,7 +7,6 @@ real, usable Python extension.
|
|||||||
|
|
||||||
Other contributors (in random order):
|
Other contributors (in random order):
|
||||||
|
|
||||||
|
|
||||||
Daniel Troeder
|
Daniel Troeder
|
||||||
for pointing out a typo in @noreply
|
for pointing out a typo in @noreply
|
||||||
arkanes@irc.freenode.net
|
arkanes@irc.freenode.net
|
||||||
@@ -44,4 +43,4 @@ Business Management Systems - http://www.bmsi.com
|
|||||||
for hosting the website, and providing paying clients who need milter service
|
for hosting the website, and providing paying clients who need milter service
|
||||||
so I can work on it as part of my day job.
|
so I can work on it as part of my day job.
|
||||||
|
|
||||||
If I have left anybody out, send me a reminder: stuart@bmsi.com
|
If I have left anybody out, send me a reminder: stuart@gathman.org
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Doxyfile 1.5.7.1
|
# Doxyfile 1.6.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 = 0.9.5
|
PROJECT_NUMBER = 0.9.8
|
||||||
|
|
||||||
# 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, Farsi, Finnish, French, German, Greek,
|
# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German,
|
||||||
# Hungarian, Italian, Japanese, Japanese-en (Japanese with English messages),
|
# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English
|
||||||
# Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, Polish,
|
# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian,
|
||||||
# Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, Slovene,
|
# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak,
|
||||||
# Spanish, Swedish, and Ukrainian.
|
# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese.
|
||||||
|
|
||||||
OUTPUT_LANGUAGE = English
|
OUTPUT_LANGUAGE = English
|
||||||
|
|
||||||
@@ -207,6 +207,17 @@ 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
|
||||||
@@ -394,6 +405,10 @@ 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.
|
||||||
@@ -468,7 +483,8 @@ 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. This will remove the Namespaces entry from the Quick Index
|
# Namespaces page.
|
||||||
|
# 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
|
||||||
@@ -511,7 +527,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 = YES
|
WARN_IF_UNDOCUMENTED = NO
|
||||||
|
|
||||||
# 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
|
||||||
@@ -552,7 +568,10 @@ 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 doc/mainpage.py doc/milter.py Milter
|
INPUT = mime.py \
|
||||||
|
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
|
||||||
@@ -636,14 +655,17 @@ 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. If FILTER_PATTERNS is specified, this tag will be
|
# to standard output.
|
||||||
|
# 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. Doxygen will compare the file name with each pattern and apply the
|
# basis.
|
||||||
# filter if there is a match. The filters are a list of the form:
|
# 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:
|
||||||
# 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.
|
||||||
@@ -693,7 +715,8 @@ 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. Otherwise they will link to the documentstion.
|
# link to the source code.
|
||||||
|
# Otherwise they will link to the documentation.
|
||||||
|
|
||||||
REFERENCES_LINK_SOURCE = YES
|
REFERENCES_LINK_SOURCE = YES
|
||||||
|
|
||||||
@@ -767,6 +790,11 @@ 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
|
||||||
@@ -875,20 +903,37 @@ 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
|
||||||
# <a href="http://doc.trolltech.com/qthelpproject.html#namespace">Qt Help Project / Namespace</a>.
|
# http://doc.trolltech.com/qthelpproject.html#namespace
|
||||||
|
|
||||||
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
|
||||||
# <a href="http://doc.trolltech.com/qthelpproject.html#virtual-folders">Qt Help Project / Virtual Folders</a>.
|
# http://doc.trolltech.com/qthelpproject.html#virtual-folders
|
||||||
|
|
||||||
QHP_VIRTUAL_FOLDER = doc
|
QHP_VIRTUAL_FOLDER = doc
|
||||||
|
|
||||||
|
# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to add.
|
||||||
|
# For more information please see
|
||||||
|
# http://doc.trolltech.com/qthelpproject.html#custom-filters
|
||||||
|
|
||||||
|
QHP_CUST_FILTER_NAME =
|
||||||
|
|
||||||
|
# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the custom filter to add.For more information please see
|
||||||
|
# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters">Qt Help Project / Custom Filters</a>.
|
||||||
|
|
||||||
|
QHP_CUST_FILTER_ATTRS =
|
||||||
|
|
||||||
|
# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this project's
|
||||||
|
# filter section matches.
|
||||||
|
# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes">Qt Help Project / Filter Attributes</a>.
|
||||||
|
|
||||||
|
QHP_SECT_FILTER_ATTRS =
|
||||||
|
|
||||||
# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can
|
# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can
|
||||||
# be used to specify the location of Qt's qhelpgenerator.
|
# be used to specify the location of Qt's qhelpgenerator.
|
||||||
# If non-empty doxygen will try to run qhelpgenerator on the generated
|
# If non-empty doxygen will try to run qhelpgenerator on the generated
|
||||||
# .qhp file .
|
# .qhp file.
|
||||||
|
|
||||||
QHG_LOCATION =
|
QHG_LOCATION =
|
||||||
|
|
||||||
@@ -905,21 +950,19 @@ 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 FRAME, a side panel will be generated
|
# If the tag value is set to YES, 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 (for instance Mozilla 1.0+,
|
# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser).
|
||||||
# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are
|
# Windows users are probably better off using the HTML help feature.
|
||||||
# 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.
|
||||||
@@ -934,6 +977,13 @@ 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
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
@@ -1010,6 +1060,10 @@ 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
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
@@ -1146,8 +1200,10 @@ 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. This is useful
|
# nicely formatted so it can be parsed by a human reader.
|
||||||
# if you want to understand what is going on. On the other hand, if this
|
# This is useful
|
||||||
|
# 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.
|
||||||
|
|
||||||
@@ -1234,8 +1290,10 @@ 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
|
||||||
@@ -1462,12 +1520,3 @@ 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
|
|
||||||
|
|||||||
+105
-47
@@ -8,23 +8,19 @@
|
|||||||
# Copyright 2001,2009 Business Management Systems, Inc.
|
# Copyright 2001,2009 Business Management Systems, Inc.
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
# This code is under the GNU General Public License. See COPYING for details.
|
||||||
|
|
||||||
__version__ = '0.9.5'
|
__version__ = '0.9.8'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import milter
|
import milter
|
||||||
import thread
|
import thread
|
||||||
|
|
||||||
from milter import *
|
from milter import *
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
_seq_lock = thread.allocate_lock()
|
_seq_lock = thread.allocate_lock()
|
||||||
_seq = 0
|
_seq = 0
|
||||||
|
|
||||||
## @fn set_flags(flags)
|
|
||||||
# @brief Enable optional %milter actions.
|
|
||||||
# Certain %milter actions need to be enabled before calling milter.runmilter()
|
|
||||||
# or they throw an exception.
|
|
||||||
# @param flags Bit ored mask of optional actions to enable
|
|
||||||
|
|
||||||
def uniqueID():
|
def uniqueID():
|
||||||
"""Return a unique sequence number (incremented on each call).
|
"""Return a unique sequence number (incremented on each call).
|
||||||
"""
|
"""
|
||||||
@@ -47,6 +43,9 @@ OPTIONAL_CALLBACKS = {
|
|||||||
'header':(P_NR_HDR,P_NOHDRS)
|
'header':(P_NR_HDR,P_NOHDRS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## @private
|
||||||
|
R = re.compile(r'%+')
|
||||||
|
|
||||||
## @private
|
## @private
|
||||||
def decode_mask(bits,names):
|
def decode_mask(bits,names):
|
||||||
t = [ (s,getattr(milter,s)) for s in names]
|
t = [ (s,getattr(milter,s)) for s in names]
|
||||||
@@ -137,7 +136,12 @@ def nocallback(func):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'@nocallback applied to non-optional method: '+func.__name__)
|
'@nocallback applied to non-optional method: '+func.__name__)
|
||||||
return func
|
def wrapper(self,*args):
|
||||||
|
if func(self,*args) != CONTINUE:
|
||||||
|
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
|
||||||
|
% func.__name__)
|
||||||
|
return CONTINUE
|
||||||
|
return wrapper
|
||||||
|
|
||||||
## Function decorator to disable callback reply.
|
## Function decorator to disable callback reply.
|
||||||
# If the MTA supports it, tells the MTA not to wait for a reply from
|
# If the MTA supports it, tells the MTA not to wait for a reply from
|
||||||
@@ -152,9 +156,14 @@ def noreply(func):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'@noreply applied to non-optional method: '+func.__name__)
|
'@noreply applied to non-optional method: '+func.__name__)
|
||||||
|
@wraps(func)
|
||||||
def wrapper(self,*args):
|
def wrapper(self,*args):
|
||||||
rc = func(self,*args)
|
rc = func(self,*args)
|
||||||
if self._protocol & nr_mask: return NOREPLY
|
if self._protocol & nr_mask:
|
||||||
|
if rc != CONTINUE:
|
||||||
|
raise RuntimeError('%s return code must be CONTINUE with @noreply'
|
||||||
|
% func.__name__)
|
||||||
|
return NOREPLY
|
||||||
return rc
|
return rc
|
||||||
wrapper.milter_protocol = nr_mask
|
wrapper.milter_protocol = nr_mask
|
||||||
return wrapper
|
return wrapper
|
||||||
@@ -212,7 +221,7 @@ class Base(object):
|
|||||||
# Some optional actions may be disabled by calling milter.set_flags(), or
|
# Some optional actions may be disabled by calling milter.set_flags(), or
|
||||||
# by overriding the negotiate callback. The bits include:
|
# by overriding the negotiate callback. The bits include:
|
||||||
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
|
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
|
||||||
# CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST</code>.
|
# CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</code>.
|
||||||
# The <code>Milter.CURR_ACTS</code> bitmask is all actions
|
# The <code>Milter.CURR_ACTS</code> bitmask is all actions
|
||||||
# known when the milter module was compiled.
|
# known when the milter module was compiled.
|
||||||
# Application code can also inspect this field to determine
|
# Application code can also inspect this field to determine
|
||||||
@@ -247,7 +256,9 @@ 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 for each connection to the MTA. Called by the
|
||||||
|
# <a href="https://www.milter.org/developers/api/xxfi_connect">
|
||||||
|
# xxfi_connect</a> callback.
|
||||||
# The <code>hostname</code> provided by the local MTA is either
|
# The <code>hostname</code> provided by the local MTA is either
|
||||||
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
|
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
|
||||||
# The format of hostaddr depends on the socket family:
|
# The format of hostaddr depends on the socket family:
|
||||||
@@ -260,6 +271,17 @@ class Base(object):
|
|||||||
# <dt><code>socket.AF_UNIX</code>
|
# <dt><code>socket.AF_UNIX</code>
|
||||||
# <dd>A string with the socketname
|
# <dd>A string with the socketname
|
||||||
# </dl>
|
# </dl>
|
||||||
|
# To vary behavior based on what port the client connected to,
|
||||||
|
# for example skipping blacklist checks for port 587 (which must
|
||||||
|
# be authenticated), use @link #getsymval getsymval('{daemon_port}') @endlink.
|
||||||
|
# The <code>{daemon_port}</code> macro must be enabled in sendmail.cf
|
||||||
|
# <pre>
|
||||||
|
# O Milter.macros.connect=j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}
|
||||||
|
# </pre>
|
||||||
|
# or sendmail.mc
|
||||||
|
# <pre>
|
||||||
|
# define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}'')dnl
|
||||||
|
# </pre>
|
||||||
# @param hostname the PTR name or bracketed IP of the SMTP client
|
# @param hostname the PTR name or bracketed IP of the SMTP client
|
||||||
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
|
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
|
||||||
# or <code>socket.AF_UNIX</code>
|
# or <code>socket.AF_UNIX</code>
|
||||||
@@ -271,12 +293,26 @@ class Base(object):
|
|||||||
# this almost always results in terminating the connection.
|
# this almost always results in terminating the connection.
|
||||||
@nocallback
|
@nocallback
|
||||||
def hello(self,hostname): return CONTINUE
|
def hello(self,hostname): return CONTINUE
|
||||||
## Called when the SMTP client says MAIL FROM.
|
## Called when the SMTP client says MAIL FROM. Called by the
|
||||||
|
# <a href="https://www.milter.org/developers/api/xxfi_envfrom">
|
||||||
|
# xxfi_envfrom</a> callback.
|
||||||
# Returning REJECT rejects the message, but not the connection.
|
# Returning REJECT rejects the message, but not the connection.
|
||||||
|
# The sender is the "envelope" from as defined by
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
||||||
|
# For the From: header (author) defined in
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
||||||
|
# see @link #header the header callback @endlink.
|
||||||
@nocallback
|
@nocallback
|
||||||
def envfrom(self,f,*str): return CONTINUE
|
def envfrom(self,f,*str): return CONTINUE
|
||||||
## Called when the SMTP client says RCPT TO.
|
## Called when the SMTP client says RCPT TO. Called by the
|
||||||
|
# <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
|
||||||
|
# xxfi_envrcpt</a> callback.
|
||||||
# Returning REJECT rejects the current recipient, not the entire message.
|
# Returning REJECT rejects the current recipient, not the entire message.
|
||||||
|
# The recipient is the "envelope" recipient as defined by
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
||||||
|
# For recipients defined in
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
||||||
|
# for example To: or Cc:, see @link #header the header callback @endlink.
|
||||||
@nocallback
|
@nocallback
|
||||||
def envrcpt(self,to,*str): return CONTINUE
|
def envrcpt(self,to,*str): return CONTINUE
|
||||||
## Called when the SMTP client says DATA.
|
## Called when the SMTP client says DATA.
|
||||||
@@ -310,7 +346,7 @@ class Base(object):
|
|||||||
## Called when the connection is closed.
|
## Called when the connection is closed.
|
||||||
def close(self): return CONTINUE
|
def close(self): return CONTINUE
|
||||||
|
|
||||||
## Return mask of SMFIP_N.. protocol option bits to clear for this class
|
## Return mask of SMFIP_N* protocol option bits to clear for this class
|
||||||
# The @@nocallback and @@noreply decorators set the
|
# The @@nocallback and @@noreply decorators set the
|
||||||
# <code>milter_protocol</code> function attribute to the protocol mask bit to
|
# <code>milter_protocol</code> function attribute to the protocol mask bit to
|
||||||
# pass to libmilter, causing that callback or its reply to be skipped.
|
# pass to libmilter, causing that callback or its reply to be skipped.
|
||||||
@@ -336,7 +372,10 @@ class Base(object):
|
|||||||
|
|
||||||
## Negotiate milter protocol options. Called by the
|
## Negotiate milter protocol options. Called by the
|
||||||
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
|
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
|
||||||
# xffi_negotiate</a> callback.
|
# xffi_negotiate</a> callback. This is an advanced callback,
|
||||||
|
# do not override unless you know what you are doing. Most
|
||||||
|
# negotiation can be done simply by using the supplied
|
||||||
|
# class and function decorators.
|
||||||
# Options are passed as
|
# Options are passed as
|
||||||
# a list of 4 32-bit ints which can be modified and are passed
|
# a list of 4 32-bit ints which can be modified and are passed
|
||||||
# back to libmilter on return.
|
# back to libmilter on return.
|
||||||
@@ -363,28 +402,59 @@ 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="https://www.milter.org/developers/api/smfi_getsymval">
|
||||||
|
# smfi_getsymval</a> for default sendmail macros.
|
||||||
# @param sym the macro name
|
# @param sym the macro name
|
||||||
def getsymval(self,sym):
|
def getsymval(self,sym):
|
||||||
return self._ctx.getsymval(sym)
|
return self._ctx.getsymval(sym)
|
||||||
|
|
||||||
## Set the SMTP reply code and message.
|
## Set the SMTP reply code and message.
|
||||||
# If the MTA does not support setmlreply, then only the
|
# If the MTA does not support setmlreply, then only the
|
||||||
# first msg line is used.
|
# first msg line is used. Any '%%' in a message line
|
||||||
|
# must be doubled, or libmilter will silently ignore the setreply.
|
||||||
|
# Beginning with 0.9.6, we test for that case and throw ValueError to avoid
|
||||||
|
# head scratching. What will <i>really</i> irritate you, however,
|
||||||
|
# is that if you carefully double any '%%', your message will be
|
||||||
|
# sent - but with the '%%' still doubled!
|
||||||
|
# See <a href="https://www.milter.org/developers/api/smfi_setreply">
|
||||||
|
# smfi_setreply</a> for more information.
|
||||||
|
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
|
||||||
|
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
|
||||||
|
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
|
||||||
|
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
|
||||||
|
# @param msg The text part of the SMTP reply. If msg is None,
|
||||||
|
# an empty message is used.
|
||||||
|
# @param ml Optional additional message lines.
|
||||||
def setreply(self,rcode,xcode=None,msg=None,*ml):
|
def setreply(self,rcode,xcode=None,msg=None,*ml):
|
||||||
|
for m in (msg,)+ml:
|
||||||
|
if 1 in [len(s)&1 for s in R.findall(m)]:
|
||||||
|
raise ValueError("'%' must be doubled: "+m)
|
||||||
return self._ctx.setreply(rcode,xcode,msg,*ml)
|
return self._ctx.setreply(rcode,xcode,msg,*ml)
|
||||||
|
|
||||||
## Tell the MTA which macro names will be used.
|
## Tell the MTA which macro names will be used.
|
||||||
# The <code>Milter.SETSMLIST</code> action flag must be set.
|
# This information can reduce the size of messages received from sendmail,
|
||||||
|
# and hence could reduce bandwidth between sendmail and your milter where
|
||||||
|
# that is a factor. The <code>Milter.SETSYMLIST</code> action flag must be
|
||||||
|
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT,
|
||||||
|
# M_DATA, M_EOM, M_EOH.
|
||||||
#
|
#
|
||||||
# May only be called from negotiate callback.
|
# May only be called from negotiate callback.
|
||||||
# @since 0.9.2
|
# @since 0.9.8, previous version was misspelled!
|
||||||
# @param stage the protocol stage to set to macro list for
|
# @param stage the protocol stage to set to macro list for,
|
||||||
# @param macros a string with a space delimited list of macros
|
# one of the M_* constants defined in Milter
|
||||||
def setsmlist(self,stage,macros):
|
# @param macros space separated and/or lists of strings
|
||||||
if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST")
|
def setsymlist(self,stage,*macros):
|
||||||
if type(macros) in (list,tuple):
|
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
|
||||||
macros = ' '.join(macros)
|
a = []
|
||||||
return self._ctx.setsmlist(stage,macros)
|
for m in macros:
|
||||||
|
try:
|
||||||
|
m = m.encode('utf8')
|
||||||
|
except: pass
|
||||||
|
try:
|
||||||
|
m = m.split(' ')
|
||||||
|
except: pass
|
||||||
|
a += m
|
||||||
|
return self._ctx.setsmlist(stage,' '.join(a))
|
||||||
|
|
||||||
# Milter methods which can only be called from eom callback.
|
# Milter methods which can only be called from eom callback.
|
||||||
|
|
||||||
@@ -648,28 +718,7 @@ def envcallback(c,args):
|
|||||||
# @param socketname the socket to be passed to milter.setconn()
|
# @param socketname the socket to be passed to milter.setconn()
|
||||||
# @param timeout the time in secs the MTA should wait for a response before
|
# @param timeout the time in secs the MTA should wait for a response before
|
||||||
# considering this %milter dead
|
# considering this %milter dead
|
||||||
def runmilter(name,socketname,timeout = 0):
|
def runmilter(name,socketname,timeout = 0,rmsock=True):
|
||||||
# This bit is here on the assumption that you will be starting this filter
|
|
||||||
# before sendmail. If sendmail is not running and the socket already exists,
|
|
||||||
# libmilter will throw a warning. If sendmail is running, this is still
|
|
||||||
# safe if there are no messages currently being processed. It's safer to
|
|
||||||
# shutdown sendmail, kill the filter process, restart the filter, and then
|
|
||||||
# restart sendmail.
|
|
||||||
pos = socketname.find(':')
|
|
||||||
if pos > 1:
|
|
||||||
s = socketname[:pos]
|
|
||||||
fname = socketname[pos+1:]
|
|
||||||
else:
|
|
||||||
s = "unix"
|
|
||||||
fname = socketname
|
|
||||||
if s == "unix" or s == "local":
|
|
||||||
print "Removing %s" % fname
|
|
||||||
try:
|
|
||||||
os.unlink(fname)
|
|
||||||
except os.error, x:
|
|
||||||
import errno
|
|
||||||
if x.errno != errno.ENOENT:
|
|
||||||
raise milter.error(x)
|
|
||||||
|
|
||||||
# The default flags set include everything
|
# The default flags set include everything
|
||||||
# milter.set_flags(milter.ADDHDRS)
|
# milter.set_flags(milter.ADDHDRS)
|
||||||
@@ -700,6 +749,14 @@ def runmilter(name,socketname,timeout = 0):
|
|||||||
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
|
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
|
||||||
negotiate=ncb
|
negotiate=ncb
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# We remove the socket here by default on the assumption that you will be
|
||||||
|
# starting this filter before sendmail. If sendmail is not running and the
|
||||||
|
# socket already exists, libmilter will throw a warning. If sendmail is
|
||||||
|
# running, this is still safe if there are no messages currently being
|
||||||
|
# processed. It's safer to shutdown sendmail, kill the filter process,
|
||||||
|
# restart the filter, and then restart sendmail.
|
||||||
|
milter.opensocket(rmsock)
|
||||||
start_seq = _seq
|
start_seq = _seq
|
||||||
try:
|
try:
|
||||||
milter.main()
|
milter.main()
|
||||||
@@ -714,4 +771,5 @@ for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'):
|
|||||||
__all__ = __all__.keys()
|
__all__ = __all__.keys()
|
||||||
|
|
||||||
## @example milter-template.py
|
## @example milter-template.py
|
||||||
|
## @example milter-nomix.py
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
# CBV results.
|
# CBV results.
|
||||||
#
|
#
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.9 2008/05/08 21:35:57 customdesigned
|
||||||
|
# Allow explicitly whitelisted email from banned_users.
|
||||||
|
#
|
||||||
# Revision 1.8 2007/09/03 16:18:45 customdesigned
|
# Revision 1.8 2007/09/03 16:18:45 customdesigned
|
||||||
# Delete unparseable timestamps when loading address cache. These have
|
# Delete unparseable timestamps when loading address cache. These have
|
||||||
# arisen because of failure to parse MAIL FROM properly. Will have to
|
# arisen because of failure to parse MAIL FROM properly. Will have to
|
||||||
|
|||||||
+6
-4
@@ -1,4 +1,5 @@
|
|||||||
from ConfigParser import ConfigParser
|
from ConfigParser import ConfigParser
|
||||||
|
import os.path
|
||||||
|
|
||||||
class MilterConfigParser(ConfigParser):
|
class MilterConfigParser(ConfigParser):
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class MilterConfigParser(ConfigParser):
|
|||||||
return [q.strip() for q in self.get(sect,opt).split(',')]
|
return [q.strip() for q in self.get(sect,opt).split(',')]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def getaddrset(self,sect,opt):
|
def getaddrset(self,sect,opt,dir=''):
|
||||||
if not self.has_option(sect,opt):
|
if not self.has_option(sect,opt):
|
||||||
return {}
|
return {}
|
||||||
s = self.get(sect,opt)
|
s = self.get(sect,opt)
|
||||||
@@ -29,13 +30,14 @@ class MilterConfigParser(ConfigParser):
|
|||||||
q = q.strip()
|
q = q.strip()
|
||||||
if q.startswith('file:'):
|
if q.startswith('file:'):
|
||||||
domain = q[5:].lower()
|
domain = q[5:].lower()
|
||||||
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
|
fname = os.path.join(dir,domain)
|
||||||
|
d[domain] = d.setdefault(domain,[]) + open(fname,'r').read().split()
|
||||||
else:
|
else:
|
||||||
user,domain = q.split('@')
|
user,domain = q.split('@')
|
||||||
d.setdefault(domain.lower(),[]).append(user)
|
d.setdefault(domain.lower(),[]).append(user)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def getaddrdict(self,sect,opt):
|
def getaddrdict(self,sect,opt,dir=''):
|
||||||
if not self.has_option(sect,opt):
|
if not self.has_option(sect,opt):
|
||||||
return {}
|
return {}
|
||||||
d = {}
|
d = {}
|
||||||
@@ -46,7 +48,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 = addr[5:]
|
fname = os.path.join(dir,addr[5:])
|
||||||
for a in open(fname,'r').read().split():
|
for a in open(fname,'r').read().split():
|
||||||
d[a] = q
|
d[a] = q
|
||||||
else:
|
else:
|
||||||
|
|||||||
+24
-4
@@ -70,15 +70,23 @@ class Session(object):
|
|||||||
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
|
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
|
||||||
post: isinstance(__return__, types.ListType)
|
post: isinstance(__return__, types.ListType)
|
||||||
"""
|
"""
|
||||||
|
if name.endswith('.'): name = name[:-1]
|
||||||
|
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
|
||||||
|
return [] # invalid DNS name (too long or empty)
|
||||||
result = self.cache.get( (name, qtype) )
|
result = self.cache.get( (name, qtype) )
|
||||||
cname = None
|
cname = None
|
||||||
|
if result: return result
|
||||||
|
cnamek = (name,'CNAME')
|
||||||
|
cname = self.cache.get( cnamek )
|
||||||
|
|
||||||
if not result:
|
if cname:
|
||||||
|
cname = cname[0]
|
||||||
|
else:
|
||||||
safe2cache = Session.SAFE2CACHE
|
safe2cache = Session.SAFE2CACHE
|
||||||
for k, v in DNSLookup(name, qtype):
|
for k, v in DNSLookup(name, qtype):
|
||||||
if k == (name, 'CNAME'):
|
if k == cnamek:
|
||||||
cname = v
|
cname = v
|
||||||
if (qtype,k[1]) in safe2cache:
|
if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache:
|
||||||
self.cache.setdefault(k, []).append(v)
|
self.cache.setdefault(k, []).append(v)
|
||||||
result = self.cache.get( (name, qtype), [])
|
result = self.cache.get( (name, qtype), [])
|
||||||
if not result and cname:
|
if not result and cname:
|
||||||
@@ -89,10 +97,22 @@ class Session(object):
|
|||||||
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 in cnames:
|
if cname in cnames:
|
||||||
raise DNSError, 'CNAME loop'
|
raise DNSError('CNAME loop')
|
||||||
result = self.dns(cname, qtype, cnames=cnames)
|
result = self.dns(cname, qtype, cnames=cnames)
|
||||||
|
if result:
|
||||||
|
self.cache[(name,qtype)] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def dns_txt(self, domainname, enc='ascii'):
|
||||||
|
"Get a list of TXT records for a domain name."
|
||||||
|
if domainname:
|
||||||
|
try:
|
||||||
|
return [''.join(s.decode(enc) for s in a)
|
||||||
|
for a in self.dns(domainname, 'TXT')]
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise DNSError('Non-ascii character in SPF TXT record.')
|
||||||
|
return []
|
||||||
|
|
||||||
DNS.DiscoverNameServers()
|
DNS.DiscoverNameServers()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
# Send DSNs, do call back verification,
|
# Send DSNs, do call back verification,
|
||||||
# and generate DSN messages from a template
|
# and generate DSN messages from a template
|
||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.22 2011/03/18 20:41:31 customdesigned
|
||||||
|
# Python2.6 SMTP.close() fails when instance never connected.
|
||||||
|
#
|
||||||
# Revision 1.21 2011/03/03 05:11:58 customdesigned
|
# Revision 1.21 2011/03/03 05:11:58 customdesigned
|
||||||
# Release 0.9.4
|
# Release 0.9.4
|
||||||
#
|
#
|
||||||
|
|||||||
+55
-9
@@ -18,13 +18,19 @@ def quoteAddress(s):
|
|||||||
class Record(object):
|
class Record(object):
|
||||||
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
|
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self,timeinc=0):
|
||||||
now = time.time()
|
now = time.time() + timeinc
|
||||||
self.firstseen = now
|
self.firstseen = now
|
||||||
self.lastseen = now
|
self.lastseen = now
|
||||||
self.cnt = 0
|
self.cnt = 0
|
||||||
self.umis = None
|
self.umis = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Grey[%s:%s:%s:%d]" % (
|
||||||
|
time.ctime(self.firstseen),time.ctime(self.lastseen),
|
||||||
|
self.umis,self.cnt
|
||||||
|
)
|
||||||
|
|
||||||
class Greylist(object):
|
class Greylist(object):
|
||||||
|
|
||||||
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
||||||
@@ -35,7 +41,37 @@ 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 check(self,ip,sender,recipient):
|
def export_csv(self,fp,timeinc=0):
|
||||||
|
"Export records to csv."
|
||||||
|
import csv
|
||||||
|
dbp = self.dbp
|
||||||
|
w = csv.writer(fp)
|
||||||
|
now = time.time() + timeinc
|
||||||
|
for key, r in dbp.iteritems():
|
||||||
|
if now > r.lastseen + self.greylist_retain: continue
|
||||||
|
ip,sender,recipient = key.rsplit(':',2)
|
||||||
|
w.writerow([ip,sender,recipient,r.firstseen,r.lastseen,r.cnt,r.umis])
|
||||||
|
|
||||||
|
def clean(self,timeinc=0):
|
||||||
|
"Delete records past the retention limit."
|
||||||
|
now = time.time() + timeinc
|
||||||
|
cnt = 0
|
||||||
|
dbp = self.dbp
|
||||||
|
for key, r in dbp.iteritems():
|
||||||
|
#print key,r,time.ctime(now)
|
||||||
|
if now > r.lastseen + self.greylist_retain:
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
r = dbp[key]
|
||||||
|
now = time.time() + timeinc
|
||||||
|
if now > r.lastseen + self.greylist_retain:
|
||||||
|
del dbp[key]
|
||||||
|
cnt += 1
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
return cnt
|
||||||
|
|
||||||
|
def check(self,ip,sender,recipient,timeinc=0):
|
||||||
"Return number of allowed messages for greylist triple."
|
"Return number of allowed messages for greylist triple."
|
||||||
sender = quoteAddress(sender)
|
sender = quoteAddress(sender)
|
||||||
recipient = quoteAddress(recipient)
|
recipient = quoteAddress(recipient)
|
||||||
@@ -45,15 +81,15 @@ class Greylist(object):
|
|||||||
dbp = self.dbp
|
dbp = self.dbp
|
||||||
try:
|
try:
|
||||||
r = dbp[key]
|
r = dbp[key]
|
||||||
now = time.time()
|
now = time.time() + timeinc
|
||||||
if now > r.lastseen + self.greylist_retain:
|
if now > r.lastseen + self.greylist_retain:
|
||||||
# expired
|
# expired
|
||||||
log.debug('Expired greylist: %s',key)
|
log.debug('Expired greylist: %s',key)
|
||||||
r = Record()
|
r = Record(timeinc)
|
||||||
elif now < r.firstseen + self.greylist_time:
|
elif now < r.firstseen + self.greylist_time + 5:
|
||||||
# still greylisted
|
# still greylisted
|
||||||
log.debug('Early greylist: %s',key)
|
log.debug('Early greylist: %s',key)
|
||||||
#r = Record()
|
#r = Record(timeinc)
|
||||||
r.lastseen = now
|
r.lastseen = now
|
||||||
elif r.cnt or now < r.firstseen + self.greylist_expire:
|
elif r.cnt or now < r.firstseen + self.greylist_expire:
|
||||||
# in greylist window or active
|
# in greylist window or active
|
||||||
@@ -63,12 +99,22 @@ class Greylist(object):
|
|||||||
else:
|
else:
|
||||||
# passed greylist window
|
# passed greylist window
|
||||||
log.debug('Late greylist: %s',key)
|
log.debug('Late greylist: %s',key)
|
||||||
r = Record()
|
r = Record(timeinc)
|
||||||
dbp[key] = r
|
dbp[key] = r
|
||||||
except:
|
except:
|
||||||
r = Record()
|
r = Record(timeinc)
|
||||||
dbp[key] = r
|
dbp[key] = r
|
||||||
dbp.sync()
|
dbp.sync()
|
||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
return r.cnt
|
return r.cnt
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.dbp.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
g = Greylist(sys.argv[1],5,24,36)
|
||||||
|
try:
|
||||||
|
g.export_csv(sys.stdout)
|
||||||
|
finally: g.close()
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import urllib
|
||||||
|
import sqlite3
|
||||||
|
import thread
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
log = logging.getLogger('milter.greylist')
|
||||||
|
|
||||||
|
_db_lock = thread.allocate_lock()
|
||||||
|
|
||||||
|
class Greylist(object):
|
||||||
|
|
||||||
|
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
||||||
|
self.ignoreLastByte = False
|
||||||
|
self.greylist_time = grey_time * 60 # minutes
|
||||||
|
self.greylist_expire = grey_expire * 3600 # hours
|
||||||
|
self.greylist_retain = grey_retain * 24 * 3600 # days
|
||||||
|
self.conn = sqlite3.connect(dbname)
|
||||||
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
self.conn.execute('''create table greylist(
|
||||||
|
ip text , sender text, recipient text,
|
||||||
|
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
|
||||||
|
primary key (ip,sender,recipient))''')
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def import_csv(self,fp):
|
||||||
|
import csv
|
||||||
|
rdr = csv.reader(fp)
|
||||||
|
cur = self.conn.execute('begin immediate')
|
||||||
|
try:
|
||||||
|
for r in rdr:
|
||||||
|
cur.execute('''insert into
|
||||||
|
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
|
||||||
|
values(?,?,?,?,?,?,?)''', r)
|
||||||
|
self.conn.commit()
|
||||||
|
finally:
|
||||||
|
cur.close();
|
||||||
|
|
||||||
|
def clean(self,timeinc=0):
|
||||||
|
"Delete records past the retention limit."
|
||||||
|
now = time.time() + timeinc - self.greylist_retain
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute('delete from greylist where lastseen < ?',(now,))
|
||||||
|
cnt = cur.rowcount
|
||||||
|
self.conn.commit()
|
||||||
|
finally: cur.close()
|
||||||
|
return cnt
|
||||||
|
|
||||||
|
def check(self,ip,sender,recipient,timeinc=0):
|
||||||
|
"Return number of allowed messages for greylist triple."
|
||||||
|
_db_lock.acquire()
|
||||||
|
cur = self.conn.execute('begin immediate')
|
||||||
|
try:
|
||||||
|
cur.execute('''select firstseen,lastseen,cnt,umis from greylist where
|
||||||
|
ip=? and sender=? and recipient=?''',(ip,sender,recipient))
|
||||||
|
r = cur.fetchone()
|
||||||
|
now = time.time() + timeinc
|
||||||
|
cnt = 0
|
||||||
|
if not r:
|
||||||
|
cur.execute('''insert into
|
||||||
|
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
|
||||||
|
values(?,?,?,?,?,?,?)''', (ip,sender,recipient,now,now,0,None))
|
||||||
|
elif now > r['lastseen'] + self.greylist_retain:
|
||||||
|
# expired
|
||||||
|
log.debug('Expired greylist: %s:%s:%s',ip,sender,recipient)
|
||||||
|
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,now,0,None,ip,sender,recipient))
|
||||||
|
elif now < r['firstseen'] + self.greylist_time + 5:
|
||||||
|
# still greylisted
|
||||||
|
log.debug('Early greylist: %s:%s:%s',ip,sender,recipient)
|
||||||
|
#r = Record()
|
||||||
|
cur.execute('''update greylist set lastseen=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,ip,sender,recipient))
|
||||||
|
elif r['cnt'] or now < r['firstseen'] + self.greylist_expire:
|
||||||
|
# in greylist window or active
|
||||||
|
cnt = r['cnt'] + 1
|
||||||
|
cur.execute('''update greylist set lastseen=?,cnt=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,cnt,ip,sender,recipient))
|
||||||
|
log.debug('Active greylist(%d): %s:%s:%s',cnt,ip,sender,recipient)
|
||||||
|
else:
|
||||||
|
# passed greylist window
|
||||||
|
log.debug('Late greylist: %s:%s:%s',ip,sender,recipient)
|
||||||
|
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,now,0,None,ip,sender,recipient))
|
||||||
|
self.conn.commit()
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
_db_lock.release()
|
||||||
|
return cnt
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
g = Greylist(sys.argv[1])
|
||||||
|
try:
|
||||||
|
g.import_csv(sys.stdin)
|
||||||
|
finally: g.close()
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
## @package Milter.test
|
||||||
|
# A test framework for milters
|
||||||
|
|
||||||
|
import rfc822
|
||||||
|
import StringIO
|
||||||
|
import Milter
|
||||||
|
|
||||||
|
Milter.NOREPLY = Milter.CONTINUE
|
||||||
|
|
||||||
|
## Test mixin for unit testing %milter applications.
|
||||||
|
# This mixin overrides many Milter.MilterBase methods
|
||||||
|
# with stub versions that simply record what was done.
|
||||||
|
# @since 0.9.8
|
||||||
|
class TestBase(object):
|
||||||
|
|
||||||
|
def __init__(self,logfile='test/milter.log'):
|
||||||
|
self._protocol = 0
|
||||||
|
self.logfp = open(logfile,"a")
|
||||||
|
## List of recipients deleted
|
||||||
|
self._delrcpt = []
|
||||||
|
## List of recipients added
|
||||||
|
self._addrcpt = []
|
||||||
|
## Macros defined
|
||||||
|
self._macros = { }
|
||||||
|
## The message body.
|
||||||
|
self._body = None
|
||||||
|
## True if the %milter replaced the message body.
|
||||||
|
self._bodyreplaced = False
|
||||||
|
## True if the %milter changed any headers.
|
||||||
|
self._headerschanged = False
|
||||||
|
## Reply codes and messages set by the %milter
|
||||||
|
self._reply = None
|
||||||
|
## The rfc822 message object for the current email being fed to the %milter.
|
||||||
|
self._msg = None
|
||||||
|
self._symlist = [ None, None, None, None, None, None, None ]
|
||||||
|
|
||||||
|
def log(self,*msg):
|
||||||
|
for i in msg: print >>self.logfp, i,
|
||||||
|
print >>self.logfp
|
||||||
|
|
||||||
|
## Set a macro value.
|
||||||
|
# These are retrieved by the %milter with getsymval.
|
||||||
|
# @param name the macro name, as passed to getsymval
|
||||||
|
# @param val the macro value
|
||||||
|
def setsymval(self,name,val):
|
||||||
|
self._macros[name] = val
|
||||||
|
|
||||||
|
def getsymval(self,name):
|
||||||
|
# FIXME: track stage, and use _symlist
|
||||||
|
return self._macros.get(name,'')
|
||||||
|
|
||||||
|
def replacebody(self,chunk):
|
||||||
|
if self._body:
|
||||||
|
self._body.write(chunk)
|
||||||
|
self._bodyreplaced = True
|
||||||
|
else:
|
||||||
|
raise IOError,"replacebody not called from eom()"
|
||||||
|
|
||||||
|
# FIXME: rfc822 indexing does not really reflect the way chg/add header
|
||||||
|
# work for a %milter
|
||||||
|
def chgheader(self,field,idx,value):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"chgheader not called from eom()"
|
||||||
|
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
|
||||||
|
if value == '':
|
||||||
|
del self._msg[field]
|
||||||
|
else:
|
||||||
|
self._msg[field] = value
|
||||||
|
self._headerschanged = True
|
||||||
|
|
||||||
|
def addheader(self,field,value,idx=-1):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"addheader not called from eom()"
|
||||||
|
self.log('addheader: %s=%s' % (field,value))
|
||||||
|
self._msg[field] = value
|
||||||
|
self._headerschanged = True
|
||||||
|
|
||||||
|
def delrcpt(self,rcpt):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"delrcpt not called from eom()"
|
||||||
|
self._delrcpt.append(rcpt)
|
||||||
|
|
||||||
|
def addrcpt(self,rcpt):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"addrcpt not called from eom()"
|
||||||
|
self._addrcpt.append(rcpt)
|
||||||
|
|
||||||
|
## Save the reply codes and messages in self._reply.
|
||||||
|
def setreply(self,rcode,xcode,*msg):
|
||||||
|
self._reply = (rcode,xcode) + msg
|
||||||
|
|
||||||
|
def setsymlist(self,stage,macros):
|
||||||
|
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
|
||||||
|
# not used yet, but just for grins we save the data
|
||||||
|
a = []
|
||||||
|
for m in macros:
|
||||||
|
try:
|
||||||
|
m = m.encode('utf8')
|
||||||
|
except: pass
|
||||||
|
try:
|
||||||
|
m = m.split(' ')
|
||||||
|
except: pass
|
||||||
|
a += m
|
||||||
|
self._symlist[stage] = set(a)
|
||||||
|
|
||||||
|
## Feed a file like object to the %milter. Calls envfrom, envrcpt for
|
||||||
|
# each recipient, header for each header field, body for each body
|
||||||
|
# block, and finally eom. A return code from the %milter other than
|
||||||
|
# CONTINUE returns immediately with that return code.
|
||||||
|
#
|
||||||
|
# This is a convenience method, a test could invoke the callbacks
|
||||||
|
# in sequence on its own - and for some complex tests, this may
|
||||||
|
# be necessary.
|
||||||
|
# @param fp the file with rfc2822 message stream
|
||||||
|
# @param sender the MAIL FROM
|
||||||
|
# @param rcpt RCPT TO - additional recipients may follow
|
||||||
|
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
|
||||||
|
self._body = None
|
||||||
|
self._bodyreplaced = False
|
||||||
|
self._headerschanged = False
|
||||||
|
self._reply = None
|
||||||
|
msg = rfc822.Message(fp)
|
||||||
|
rc = self.envfrom('<%s>'%sender)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
for rcpt in (rcpt,) + rcpts:
|
||||||
|
rc = self.envrcpt('<%s>'%rcpt)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
line = None
|
||||||
|
for h in msg.headers:
|
||||||
|
if h[:1].isspace():
|
||||||
|
line = line + h
|
||||||
|
continue
|
||||||
|
if not line:
|
||||||
|
line = h
|
||||||
|
continue
|
||||||
|
s = line.split(': ',1)
|
||||||
|
if len(s) > 1: val = s[1].strip()
|
||||||
|
else: val = ''
|
||||||
|
rc = self.header(s[0],val)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
line = h
|
||||||
|
if line:
|
||||||
|
s = line.split(': ',1)
|
||||||
|
rc = self.header(s[0],s[1])
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
rc = self.eoh()
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
while 1:
|
||||||
|
buf = fp.read(8192)
|
||||||
|
if len(buf) == 0: break
|
||||||
|
rc = self.body(buf)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
self._msg = msg
|
||||||
|
self._body = StringIO.StringIO()
|
||||||
|
rc = self.eom()
|
||||||
|
if self._bodyreplaced:
|
||||||
|
body = self._body.getvalue()
|
||||||
|
else:
|
||||||
|
msg.rewindbody()
|
||||||
|
body = msg.fp.read()
|
||||||
|
self._body = StringIO.StringIO()
|
||||||
|
self._body.writelines(msg.headers)
|
||||||
|
self._body.write('\n')
|
||||||
|
self._body.write(body)
|
||||||
|
return rc
|
||||||
|
|
||||||
|
## Feed an email contained in a file to the %milter.
|
||||||
|
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
||||||
|
# @param sender MAIL FROM
|
||||||
|
# @param rcpts RCPT TO, multiple recipients may be supplied
|
||||||
|
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
||||||
|
with open('test/'+fname,'r') as fp:
|
||||||
|
return self.feedFile(fp,sender,*rcpts)
|
||||||
|
|
||||||
|
## Call the connect and helo callbacks.
|
||||||
|
# The helo callback is not called if connect does not return CONTINUE.
|
||||||
|
# @param host the hostname passed to the connect callback
|
||||||
|
# @param helo the hostname passed to the helo callback
|
||||||
|
# @param ip the IP address passed to the connect callback
|
||||||
|
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
|
||||||
|
self._body = None
|
||||||
|
self._bodyreplaced = False
|
||||||
|
opts = [ Milter.CURR_ACTS,~0,0,0 ]
|
||||||
|
rc = self.negotiate(opts)
|
||||||
|
rc = super(TestBase,self).connect(host,1,(ip,1234))
|
||||||
|
if rc != Milter.CONTINUE:
|
||||||
|
self.close()
|
||||||
|
return rc
|
||||||
|
rc = self.hello(helo)
|
||||||
|
if rc != Milter.CONTINUE:
|
||||||
|
self.close()
|
||||||
|
return rc
|
||||||
+1
-1
@@ -49,7 +49,7 @@ if hasattr(socket,'has_ipv6') and socket.has_ipv6:
|
|||||||
def inet_ntop(s):
|
def inet_ntop(s):
|
||||||
return socket.inet_ntop(socket.AF_INET6,s)
|
return socket.inet_ntop(socket.AF_INET6,s)
|
||||||
def inet_pton(s):
|
def inet_pton(s):
|
||||||
return socket.inet_pton(socket.AF_INET6,s)
|
return socket.inet_pton(socket.AF_INET6,s.strip())
|
||||||
else:
|
else:
|
||||||
from pyip6 import inet_ntop, inet_pton
|
from pyip6 import inet_ntop, inet_pton
|
||||||
|
|
||||||
|
|||||||
@@ -135,8 +135,6 @@ 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
|
||||||
@@ -194,7 +192,7 @@ Authors
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
||||||
and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that
|
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
|
||||||
kludge and added threading and context objects to it, wrote a proper OO
|
kludge and added threading and context objects to it, wrote a proper OO
|
||||||
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
||||||
it with distutils, and generally transformed it from a quick hack to a
|
it with distutils, and generally transformed it from a quick hack to a
|
||||||
|
|||||||
@@ -1,6 +1,2 @@
|
|||||||
Support smfi_negotiate and auto negotiate only those callbacks for which
|
|
||||||
Milter.Milter methods have been overridden. (Python should be able to
|
|
||||||
do that.)
|
|
||||||
|
|
||||||
Lookup exact RFC syntax of real name / email and make
|
Lookup exact RFC syntax of real name / email and make
|
||||||
Milter.utils.parse_addr() pass all unit tests.
|
Milter.utils.parse_addr() pass all unit tests.
|
||||||
|
|||||||
+14
-11
@@ -7,30 +7,32 @@
|
|||||||
# events include the initial connection from a MTA, the envelope sender and
|
# events include the initial connection from a MTA, the envelope sender and
|
||||||
# recipients, the top level mail headers, and the message body. There are
|
# recipients, the top level mail headers, and the message body. There are
|
||||||
# options to mangle all of these components of the message as it passes through
|
# options to mangle all of these components of the message as it passes through
|
||||||
# the milter.
|
# the %milter.
|
||||||
#
|
#
|
||||||
# At the next level, the <code>Milter</code> module (note the case difference)
|
# At the next level, the <code>Milter</code> module (note the case difference)
|
||||||
# provides a Python friendly object oriented wrapper for the low level API. To
|
# provides a Python friendly object oriented wrapper for the low level API. To
|
||||||
# use the Milter module, an application registers a 'factory' to create an
|
# use the Milter module, an application registers a 'factory' to create an
|
||||||
# object for each connection from a MTA to sendmail. These connection objects
|
# object for each connection from a MTA to sendmail. These connection objects
|
||||||
# must provide methods corresponding to the libmilter callback events.
|
# must provide methods corresponding to the libmilter event callbacks.
|
||||||
#
|
#
|
||||||
# Each event method returns a code to tell sendmail whether to proceed with
|
# Each callback method returns a code to tell sendmail whether to proceed with
|
||||||
# processing the message. This is a big advantage of milters over other mail
|
# processing the message. This is a big advantage of milters over other mail
|
||||||
# filtering systems. Unwanted mail can be stopped in its tracks at the
|
# filtering systems. Unwanted mail can be stopped in its tracks at the
|
||||||
# earliest possible point.
|
# earliest possible point. The callback return codes are
|
||||||
|
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
|
||||||
|
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
|
||||||
#
|
#
|
||||||
# The <code>Milter.Base</code> class provides default implementations for
|
# The Milter.Base class provides default implementations for
|
||||||
# event methods that do nothing, and also provides wrappers for the libmilter
|
# event methods that do nothing, and also provides wrappers for the libmilter
|
||||||
# methods to mutate the message. It automatically negotiates with MTA
|
# methods to mutate the message. It automatically negotiates with MTA
|
||||||
# which protocol steps need to be processed by the milter, based on
|
# which protocol steps need to be processed by the %milter, based on
|
||||||
# which callback methods are overridden.
|
# which callback methods are overridden.
|
||||||
#
|
#
|
||||||
# The <code>Milter.Milter</code> class provides an alternate default
|
# The Milter.Milter class provides an alternate default
|
||||||
# implementation that logs the main milter events, but otherwise does nothing.
|
# implementation that logs the main milter callbacks, but otherwise does
|
||||||
# It is provided for compatibility.
|
# nothing. It is provided for compatibility.
|
||||||
#
|
#
|
||||||
# The <code>mime</code> module provides a wrapper for the Python email package
|
# The mime module provides a wrapper for the Python email package
|
||||||
# that fixes some bugs, and simplifies modifying selected parts of a MIME
|
# that fixes some bugs, and simplifies modifying selected parts of a MIME
|
||||||
# message.
|
# message.
|
||||||
#
|
#
|
||||||
@@ -50,4 +52,5 @@
|
|||||||
# 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 @ref milter-template.py.
|
# replacement for threading as illustrated in
|
||||||
|
# <a href="milter-template_8py-example.html">milter-template.py</a>.
|
||||||
|
|||||||
+103
-19
@@ -3,10 +3,65 @@
|
|||||||
|
|
||||||
## @package milter
|
## @package milter
|
||||||
#
|
#
|
||||||
# A thin wrapper around libmilter.
|
# A thin wrapper around libmilter. Most users will not import
|
||||||
|
# milter directly, but will instead import Milter and subclass
|
||||||
|
# Milter.Base. This module gives you ultimate low level control
|
||||||
|
# from python.
|
||||||
#
|
#
|
||||||
|
|
||||||
## Hold context for a milter connection.
|
## Continue processing the current connection, message, or recipient.
|
||||||
|
CONTINUE = 0
|
||||||
|
## For a connection-oriented routine, reject this connection;
|
||||||
|
# call Milter.Base.close(). For a message-oriented routine, except
|
||||||
|
# Milter.Base.eom() or Milter.Base.abort(), reject this message. For a
|
||||||
|
# recipient-oriented routine, reject the current recipient (but continue
|
||||||
|
# processing the current message).
|
||||||
|
REJECT = 1
|
||||||
|
|
||||||
|
## For a message- or recipient-oriented routine, accept this message, but
|
||||||
|
# silently discard it. SMFIS_DISCARD should not be returned by a
|
||||||
|
# connection-oriented routine.
|
||||||
|
DISCARD = 2
|
||||||
|
|
||||||
|
## For a connection-oriented routine, accept this connection without further
|
||||||
|
# filter processing; call Milter.Base.close(). For a message- or
|
||||||
|
# recipient-oriented routine, accept this message without further filtering.
|
||||||
|
ACCEPT = 3
|
||||||
|
|
||||||
|
## Return a temporary failure, i.e., the corresponding SMTP command will return
|
||||||
|
# an appropriate 4xx status code. For a message-oriented routine, except
|
||||||
|
# Milter.Base.envfrom(), fail for this message. For a connection-oriented
|
||||||
|
# routine, fail for this connection; call Milter.Base.close(). For a recipient-oriented
|
||||||
|
# routine, only
|
||||||
|
# fail for the current recipient; continue message processing.
|
||||||
|
TEMPFAIL = 4
|
||||||
|
|
||||||
|
## Skip further callbacks of the same type in this transaction.
|
||||||
|
# Currently this return value is only allowed in Milter.Base.body(). It can be
|
||||||
|
# used if a %milter has received sufficiently many body chunks to make a
|
||||||
|
# decision, but still wants to invoke message modification functions that are
|
||||||
|
# only allowed to be called from Milter.Base.eom(). Note: the %milter must
|
||||||
|
# negotiate this behavior with the MTA, i.e., it must check whether the
|
||||||
|
# protocol action SMFIP_SKIP is available and if so, the %milter must request
|
||||||
|
# it.
|
||||||
|
SKIP = 5
|
||||||
|
|
||||||
|
## Do not send a reply back to the MTA.
|
||||||
|
# The %milter must negotiate this behavior with the MTA, i.e., it must check
|
||||||
|
# whether the appropriate protocol action P_NR_* is available and if so,
|
||||||
|
# the %milter must request it. If you set the P_NR_* protocol action for a
|
||||||
|
# callback, that callback must always reply with NOREPLY. Using any other
|
||||||
|
# reply code is a violation of the API. If in some cases your callback may
|
||||||
|
# return another value (e.g., due to some resource shortages), then you must
|
||||||
|
# not set P_NR_* and you must use CONTINUE as the default return
|
||||||
|
# code. (Alternatively you can try to delay reporting the problem to a later
|
||||||
|
# callback for which P_NR_* is not set.)
|
||||||
|
#
|
||||||
|
# This is negotiated and returned automatically by the Milter.noreply
|
||||||
|
# function decorator.
|
||||||
|
NOREPLY = 6
|
||||||
|
|
||||||
|
## Hold context for a %milter connection.
|
||||||
# Each connection to sendmail creates a new <code>SMFICTX</code> struct within
|
# Each connection to sendmail creates a new <code>SMFICTX</code> struct within
|
||||||
# libmilter. The milter module in turn creates a milterContext
|
# libmilter. The milter module in turn creates a milterContext
|
||||||
# tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code>
|
# tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code>
|
||||||
@@ -54,12 +109,18 @@ class milterContext(object):
|
|||||||
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.
|
||||||
def setsmlist(self,stage,macrolist): pass
|
# It may only be called from the negotiate callback.
|
||||||
|
# The protocol stages are
|
||||||
|
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
|
||||||
|
# Calls <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 macrolist a space separated list of macro names
|
||||||
|
def setsymlist(self,stage,macrolist): pass
|
||||||
|
|
||||||
class error(Exception): pass
|
class error(Exception): pass
|
||||||
|
|
||||||
## Enable optional milter actions.
|
## 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.
|
||||||
@@ -77,19 +138,27 @@ def set_abort_callback(cb): pass
|
|||||||
def set_close_callback(cb): pass
|
def set_close_callback(cb): pass
|
||||||
|
|
||||||
## Sets the return code for untrapped Python exceptions during a callback.
|
## Sets the return code for untrapped Python exceptions during a callback.
|
||||||
# Must be one of TEMPFAIL,REJECT,CONTINUE
|
# The default is TEMPFAIL. You should not depend on this handler. Your
|
||||||
|
# application should have its own top level exception handler for each
|
||||||
|
# callback. You can then choose your own reply message, log the stack track
|
||||||
|
# were you please, and so on. However, if you miss one, this last ditch
|
||||||
|
# handler will print a standard stack trace to sys.stderr, and return to
|
||||||
|
# sendmail.
|
||||||
|
# @param code one of #TEMPFAIL,#REJECT,#CONTINUE, or since 1.0, #ACCEPT
|
||||||
def set_exception_policy(code): pass
|
def set_exception_policy(code): pass
|
||||||
|
|
||||||
## Register python milter with libmilter.
|
## Register python %milter with libmilter.
|
||||||
# The name we pass is used to identify the milter in the MTA configuration.
|
# The name we pass is used to identify the %milter in the MTA configuration.
|
||||||
# Callback functions must be set using the set_*_callback() functions before
|
# Callback functions must be set using the set_*_callback() functions before
|
||||||
# registering the milter.
|
# registering the %milter.
|
||||||
# Three additional callbacks are specified as keyword parameters. These
|
# Three additional callbacks are specified as keyword parameters. These
|
||||||
# were added by recent versions of libmilter. The keyword parameters is
|
# were added by recent versions of libmilter. The keyword parameters is
|
||||||
# a nicer way to do it, I think, since it makes clear that you have to do
|
# a nicer way to do it, I think, since it makes clear that you have to do
|
||||||
# it before registering. I may move all the callbacks
|
# it before registering. I may move all the callbacks in the future (perhaps
|
||||||
# in the future (perhaps keeping the set functions for compatibility).
|
# keeping the set functions for compatibility). Note that Milter.Base
|
||||||
# @param name the milter name by which the MTA finds us
|
# automatically maps all callbacks to member functions, and negotiates which
|
||||||
|
# member functions are actually overridden by an application class.
|
||||||
|
# @param name the %milter name by which the MTA finds us
|
||||||
# @param negotiate the
|
# @param negotiate the
|
||||||
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
|
# <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
|
||||||
@@ -103,6 +172,16 @@ def set_exception_policy(code): pass
|
|||||||
# 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="https://www.milter.org/developers/api/smfi_opensocket">
|
||||||
|
# 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.
|
||||||
@@ -112,7 +191,7 @@ def main(): pass
|
|||||||
|
|
||||||
## Set the libmilter debugging level.
|
## Set the libmilter debugging level.
|
||||||
# <a href="https://www.milter.org/developers/api/smfi_setdbg">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().
|
||||||
@@ -133,16 +212,21 @@ def setbacklog(n): pass
|
|||||||
# unix, inet, or inet6 socket. By default, a unix domain socket
|
# unix, inet, or inet6 socket. By default, a unix domain socket
|
||||||
# is used. It must not exist,
|
# is used. It must not exist,
|
||||||
# and sendmail will throw warnings if, eg, the file is under a
|
# and sendmail will throw warnings if, eg, the file is under a
|
||||||
# group or world writable directory.
|
# group or world writable directory. milter.setconn() will not fail with
|
||||||
|
# an invalid socket - this will be detected only when calling milter.main()
|
||||||
|
# or milter.opensocket().
|
||||||
|
# @param s the socket address in proto:address format
|
||||||
# <pre>
|
# <pre>
|
||||||
# setconn('unix:/var/run/pythonfilter')
|
# milter.setconn('unix:/var/run/pythonfilter') # a named pipe
|
||||||
# setconn('inet:8800') # listen on ANY interface
|
# milter.setconn('local:/var/run/pythonfilter') # a named pipe
|
||||||
# setconn('inet:7871@@publichost') # listen on a specific interface
|
# milter.setconn('inet:8800') # listen on ANY interface
|
||||||
# setconn('inet6:8020')
|
# milter.setconn('inet:7871@@publichost') # listen on a specific interface
|
||||||
|
# milter.setconn('inet6:8020')
|
||||||
|
# milter.setconn('inet6:8020@[2001:db8:1234::1]') # listen on specific IP
|
||||||
# </pre>
|
# </pre>
|
||||||
def setconn(s): pass
|
def setconn(s): pass
|
||||||
|
|
||||||
## Stop the milter gracefully.
|
## Stop the %milter gracefully.
|
||||||
def stop(): pass
|
def stop(): pass
|
||||||
|
|
||||||
## Retrieve diagnostic info.
|
## Retrieve diagnostic info.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
web:
|
web:
|
||||||
doxygen
|
doxygen
|
||||||
|
cd doc/html; zip -r ../../doc .
|
||||||
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
|
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
|
||||||
|
|
||||||
VERSION=0.9.5
|
VERSION=1.0
|
||||||
CVSTAG=pymilter-0_9_5
|
CVSTAG=pymilter-1_0
|
||||||
PKG=pymilter-$(VERSION)
|
PKG=pymilter-$(VERSION)
|
||||||
SRCTAR=$(PKG).tar.gz
|
SRCTAR=$(PKG).tar.gz
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
## A very simple milter to prevent mixing of internal and external mail.
|
||||||
|
# Internal is defined as using one of a list of internal top level domains.
|
||||||
|
# This code is open-source on the same terms as Python.
|
||||||
|
|
||||||
|
import Milter
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
from Milter.utils import parse_addr
|
||||||
|
|
||||||
|
internal_tlds = ["corp", "personal"]
|
||||||
|
|
||||||
|
## Determine if a hostname is internal or not.
|
||||||
|
# True if internal, False otherwise
|
||||||
|
def is_internal(hostname):
|
||||||
|
components = hostname.split(".")
|
||||||
|
return components.pop() in internal_tlds:
|
||||||
|
|
||||||
|
# Determine if internal and external hosts are mixed based on a list
|
||||||
|
# of hostnames
|
||||||
|
def are_mixed(hostnames):
|
||||||
|
hostnames_mapped = map(is_internal, hostnames)
|
||||||
|
|
||||||
|
# Num internals
|
||||||
|
num_internal_hosts = hostnames_mapped.count(True)
|
||||||
|
|
||||||
|
# Num externals
|
||||||
|
num_external_hosts = hostnames_mapped.count(False)
|
||||||
|
|
||||||
|
return num_external_hosts >= 1 and num_internal_hosts >= 1
|
||||||
|
|
||||||
|
class NoMixMilter(Milter.Base):
|
||||||
|
|
||||||
|
def __init__(self): # A new instance with each new connection.
|
||||||
|
self.id = Milter.uniqueID() # Integer incremented with each call.
|
||||||
|
|
||||||
|
|
||||||
|
## def envfrom(self,f,*str):
|
||||||
|
@Milter.noreply
|
||||||
|
def envfrom(self, mailfrom, *str):
|
||||||
|
self.mailfrom = mailfrom
|
||||||
|
self.domains = []
|
||||||
|
t = parse_addr(mailfrom)
|
||||||
|
if len(t) > 1:
|
||||||
|
self.domains.append(t[1])
|
||||||
|
else:
|
||||||
|
self.domains.append('local')
|
||||||
|
self.internal = False
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
## def envrcpt(self, to, *str):
|
||||||
|
def envrcpt(self, to, *str):
|
||||||
|
self.R.append(to)
|
||||||
|
t = parse_addr(to)
|
||||||
|
if len(t) > 1:
|
||||||
|
self.domains.append(t[1])
|
||||||
|
else:
|
||||||
|
self.domains.append('local')
|
||||||
|
|
||||||
|
if are_mixed(self.domains):
|
||||||
|
# FIXME: log recipients collected in self.mailfrom and self.R
|
||||||
|
self.setreply('550','5.7.1','Mixing internal and external TLDs')
|
||||||
|
return Milter.REJECT
|
||||||
|
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
def main():
|
||||||
|
socketname = "/var/run/nomixsock"
|
||||||
|
timeout = 600
|
||||||
|
# Register to have the Milter factory create instances of your class:
|
||||||
|
Milter.factory = NoMixMilter
|
||||||
|
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
|
||||||
|
sys.stdout.flush()
|
||||||
|
Milter.runmilter("nomixfilter",socketname,timeout)
|
||||||
|
logq.put(None)
|
||||||
|
bt.join()
|
||||||
|
print "%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+4
-3
@@ -71,6 +71,9 @@ class myMilter(Milter.Base):
|
|||||||
self.fromparms = Milter.dictfromlist(str) # ESMTP parms
|
self.fromparms = Milter.dictfromlist(str) # ESMTP parms
|
||||||
self.user = self.getsymval('{auth_authen}') # authenticated user
|
self.user = self.getsymval('{auth_authen}') # authenticated user
|
||||||
self.log("mail from:", mailfrom, *str)
|
self.log("mail from:", mailfrom, *str)
|
||||||
|
# NOTE: self.fp is only an *internal* copy of message data. You
|
||||||
|
# must use addheader, chgheader, replacebody to change the message
|
||||||
|
# on the MTA.
|
||||||
self.fp = StringIO.StringIO()
|
self.fp = StringIO.StringIO()
|
||||||
self.canon_from = '@'.join(parse_addr(mailfrom))
|
self.canon_from = '@'.join(parse_addr(mailfrom))
|
||||||
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
|
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
|
||||||
@@ -79,7 +82,7 @@ class myMilter(Milter.Base):
|
|||||||
|
|
||||||
## def envrcpt(self, to, *str):
|
## def envrcpt(self, to, *str):
|
||||||
@Milter.noreply
|
@Milter.noreply
|
||||||
def envrcpt(self, recipient, *str):
|
def envrcpt(self, to, *str):
|
||||||
rcptinfo = to,Milter.dictfromlist(str)
|
rcptinfo = to,Milter.dictfromlist(str)
|
||||||
self.R.append(rcptinfo)
|
self.R.append(rcptinfo)
|
||||||
|
|
||||||
@@ -104,13 +107,11 @@ 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')
|
||||||
return Milter.ACCEPT
|
return Milter.ACCEPT
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
# always called, even when abort is called. Clean up
|
# always called, even when abort is called. Clean up
|
||||||
# any external resources here.
|
# any external resources here.
|
||||||
|
|||||||
+129
-84
@@ -35,6 +35,28 @@ $ python setup.py help
|
|||||||
libraries=["milter","smutil","resolv"]
|
libraries=["milter","smutil","resolv"]
|
||||||
|
|
||||||
* $Log$
|
* $Log$
|
||||||
|
* Revision 1.35 2013/03/14 22:11:25 customdesigned
|
||||||
|
* Release 0.9.8
|
||||||
|
*
|
||||||
|
* Revision 1.34 2013/03/09 05:42:14 customdesigned
|
||||||
|
* Make TestBase members private, fix getsymlist misspelling.
|
||||||
|
*
|
||||||
|
* Revision 1.33 2013/03/09 00:25:23 customdesigned
|
||||||
|
* Better untrapped exception message. const char for doc comments.
|
||||||
|
*
|
||||||
|
* Revision 1.32 2013/01/13 01:46:16 customdesigned
|
||||||
|
* Doc updates.
|
||||||
|
*
|
||||||
|
* Revision 1.31 2012/04/12 23:32:50 customdesigned
|
||||||
|
* Replace redundant callback array with macros. If this doesn't break anything,
|
||||||
|
* macros can be eliminated with code changes.
|
||||||
|
*
|
||||||
|
* Revision 1.30 2012/04/12 23:08:06 customdesigned
|
||||||
|
* Support RFC2553 on BSD
|
||||||
|
*
|
||||||
|
* Revision 1.29 2011/06/09 15:45:27 customdesigned
|
||||||
|
* Print callback name for non-int return error.
|
||||||
|
*
|
||||||
* Revision 1.28 2011/06/08 23:13:48 customdesigned
|
* Revision 1.28 2011/06/08 23:13:48 customdesigned
|
||||||
* Generate special exception when callback return not int.
|
* Generate special exception when callback return not int.
|
||||||
*
|
*
|
||||||
@@ -257,10 +279,10 @@ $ python setup.py help
|
|||||||
#define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */
|
#define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */
|
||||||
/* Now see if it supports the RFC-2553 socket's API spec. Early
|
/* Now see if it supports the RFC-2553 socket's API spec. Early
|
||||||
* IPv6 "prototype" implementations existed before the RFC was
|
* IPv6 "prototype" implementations existed before the RFC was
|
||||||
* published. Unfortunately I know of now good way to do this
|
* published. Unfortunately I know of no good way to do this
|
||||||
* other than with OS-specific tests.
|
* other than with OS-specific tests.
|
||||||
*/
|
*/
|
||||||
#ifdef linux
|
#if defined(__FreeBSD_kernel__) || defined(__linux__)
|
||||||
#define HAVE_IPV6_RFC2553
|
#define HAVE_IPV6_RFC2553
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -274,43 +296,51 @@ $ python setup.py help
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Yes, these are static. If you need multiple different callbacks, */
|
enum callbacks {
|
||||||
/* it's cleaner to use multiple filters, or convert to OO method calls. */
|
CONNECT,HELO,ENVFROM,ENVRCPT,HEADER,EOH,BODY,EOM,ABORT,CLOSE,
|
||||||
static PyObject *connect_callback = NULL;
|
|
||||||
static PyObject *helo_callback = NULL;
|
|
||||||
static PyObject *envfrom_callback = NULL;
|
|
||||||
static PyObject *envrcpt_callback = NULL;
|
|
||||||
static PyObject *header_callback = NULL;
|
|
||||||
static PyObject *eoh_callback = NULL;
|
|
||||||
static PyObject *body_callback = NULL;
|
|
||||||
static PyObject *eom_callback = NULL;
|
|
||||||
static PyObject *abort_callback = NULL;
|
|
||||||
static PyObject *close_callback = NULL;
|
|
||||||
#ifdef SMFIS_ALL_OPTS
|
#ifdef SMFIS_ALL_OPTS
|
||||||
static PyObject *unknown_callback = NULL;
|
UNKNOWN,DATA,NEGOTIATE,
|
||||||
static PyObject *data_callback = NULL;
|
|
||||||
static PyObject *negotiate_callback = NULL;
|
|
||||||
#endif
|
#endif
|
||||||
|
NUMCALLBACKS
|
||||||
|
};
|
||||||
|
|
||||||
|
#define connect_callback callback[CONNECT].cb
|
||||||
|
#define helo_callback callback[HELO].cb
|
||||||
|
#define envfrom_callback callback[ENVFROM].cb
|
||||||
|
#define envrcpt_callback callback[ENVRCPT].cb
|
||||||
|
#define header_callback callback[HEADER].cb
|
||||||
|
#define eoh_callback callback[EOH].cb
|
||||||
|
#define body_callback callback[BODY].cb
|
||||||
|
#define eom_callback callback[EOM].cb
|
||||||
|
#define abort_callback callback[ABORT].cb
|
||||||
|
#define close_callback callback[CLOSE].cb
|
||||||
|
#define unknown_callback callback[UNKNOWN].cb
|
||||||
|
#define data_callback callback[DATA].cb
|
||||||
|
#define negotiate_callback callback[NEGOTIATE].cb
|
||||||
|
|
||||||
|
/* Yes, these are static. If you need multiple different callbacks,
|
||||||
|
it's cleaner to use multiple filters, or convert to OO method calls. */
|
||||||
|
|
||||||
static struct MilterCallback {
|
static struct MilterCallback {
|
||||||
PyObject **cbp;
|
PyObject *cb;
|
||||||
const char *name;
|
const char *name;
|
||||||
} callback_names[] = {
|
} callback[NUMCALLBACKS+1] = {
|
||||||
{ &connect_callback,"connect" },
|
{ NULL ,"connect" },
|
||||||
{ &helo_callback,"helo" },
|
{ NULL ,"helo" },
|
||||||
{ &envfrom_callback,"envfrom" },
|
{ NULL ,"envfrom" },
|
||||||
{ &envrcpt_callback,"envrcpt" },
|
{ NULL ,"envrcpt" },
|
||||||
{ &header_callback,"header" },
|
{ NULL ,"header" },
|
||||||
{ &eoh_callback,"eoh" },
|
{ NULL ,"eoh" },
|
||||||
{ &body_callback,"body" },
|
{ NULL ,"body" },
|
||||||
{ &eom_callback,"eom" },
|
{ NULL ,"eom" },
|
||||||
{ &abort_callback,"abort" },
|
{ NULL ,"abort" },
|
||||||
{ &close_callback,"close" },
|
{ NULL ,"close" },
|
||||||
#ifdef SMFIS_ALL_OPTS
|
#ifdef SMFIS_ALL_OPTS
|
||||||
{ &unknown_callback,"unknown" },
|
{ NULL ,"unknown" },
|
||||||
{ &data_callback,"data" },
|
{ NULL ,"data" },
|
||||||
{ &negotiate_callback,"negotiate" },
|
{ NULL ,"negotiate" },
|
||||||
#endif
|
#endif
|
||||||
{ NULL, NULL }
|
{ NULL , NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
staticforward struct smfiDesc description; /* forward declaration */
|
staticforward struct smfiDesc description; /* forward declaration */
|
||||||
@@ -422,7 +452,7 @@ _thread_return(PyThreadState *t,int val,char *errstr) {
|
|||||||
return _generic_return(val,errstr);
|
return _generic_return(val,errstr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_flags__doc__[] =
|
static const char milter_set_flags__doc__[] =
|
||||||
"set_flags(int) -> None\n\
|
"set_flags(int) -> None\n\
|
||||||
Set flags for filter capabilities; OR of one or more of:\n\
|
Set flags for filter capabilities; OR of one or more of:\n\
|
||||||
ADDHDRS - filter may add headers\n\
|
ADDHDRS - filter may add headers\n\
|
||||||
@@ -463,7 +493,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
|
|||||||
return Py_None;
|
return Py_None;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_connect_callback__doc__[] =
|
static const char milter_set_connect_callback__doc__[] =
|
||||||
"set_connect_callback(Function) -> None\n\
|
"set_connect_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked when a connection is made to sendmail.\n\
|
Sets the Python function invoked when a connection is made to sendmail.\n\
|
||||||
Function takes args (ctx, hostname, integer, hostaddr) -> int\n\
|
Function takes args (ctx, hostname, integer, hostaddr) -> int\n\
|
||||||
@@ -490,7 +520,7 @@ milter_set_connect_callback(PyObject *self, PyObject *args) {
|
|||||||
"O:set_connect_callback", &connect_callback);
|
"O:set_connect_callback", &connect_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_helo_callback__doc__[] =
|
static const char milter_set_helo_callback__doc__[] =
|
||||||
"set_helo_callback(Function) -> None\n\
|
"set_helo_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked upon SMTP HELO.\n\
|
Sets the Python function invoked upon SMTP HELO.\n\
|
||||||
Function takes args (ctx, hostname) -> int\n\
|
Function takes args (ctx, hostname) -> int\n\
|
||||||
@@ -501,7 +531,7 @@ milter_set_helo_callback(PyObject *self, PyObject *args) {
|
|||||||
return generic_set_callback(args, "O:set_helo_callback", &helo_callback);
|
return generic_set_callback(args, "O:set_helo_callback", &helo_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_envfrom_callback__doc__[] =
|
static const char milter_set_envfrom_callback__doc__[] =
|
||||||
"set_envfrom_callback(Function) -> None\n\
|
"set_envfrom_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked on envelope from.\n\
|
Sets the Python function invoked on envelope from.\n\
|
||||||
Function takes args (ctx, from, *str) -> int\n\
|
Function takes args (ctx, from, *str) -> int\n\
|
||||||
@@ -514,7 +544,7 @@ milter_set_envfrom_callback(PyObject *self, PyObject *args) {
|
|||||||
&envfrom_callback);
|
&envfrom_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_envrcpt_callback__doc__[] =
|
static const char milter_set_envrcpt_callback__doc__[] =
|
||||||
"set_envrcpt_callback(Function) -> None\n\
|
"set_envrcpt_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked on each envelope recipient.\n\
|
Sets the Python function invoked on each envelope recipient.\n\
|
||||||
Function takes args (ctx, rcpt, *str) -> int\n\
|
Function takes args (ctx, rcpt, *str) -> int\n\
|
||||||
@@ -527,7 +557,7 @@ milter_set_envrcpt_callback(PyObject *self, PyObject *args) {
|
|||||||
&envrcpt_callback);
|
&envrcpt_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_header_callback__doc__[] =
|
static const char milter_set_header_callback__doc__[] =
|
||||||
"set_header_callback(Function) -> None\n\
|
"set_header_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked on each message header.\n\
|
Sets the Python function invoked on each message header.\n\
|
||||||
Function takes args (ctx, field, value) ->int\n\
|
Function takes args (ctx, field, value) ->int\n\
|
||||||
@@ -540,7 +570,7 @@ milter_set_header_callback(PyObject *self, PyObject *args) {
|
|||||||
&header_callback);
|
&header_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_eoh_callback__doc__[] =
|
static const char milter_set_eoh_callback__doc__[] =
|
||||||
"set_eoh_callback(Function) -> None\n\
|
"set_eoh_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked at end of header.\n\
|
Sets the Python function invoked at end of header.\n\
|
||||||
Function takes args (ctx) -> int";
|
Function takes args (ctx) -> int";
|
||||||
@@ -550,7 +580,7 @@ milter_set_eoh_callback(PyObject *self, PyObject *args) {
|
|||||||
return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback);
|
return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_body_callback__doc__[] =
|
static const char milter_set_body_callback__doc__[] =
|
||||||
"set_body_callback(Function) -> None\n\
|
"set_body_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked for each body chunk. There may\n\
|
Sets the Python function invoked for each body chunk. There may\n\
|
||||||
be multiple body chunks passed to the filter. End-of-lines are\n\
|
be multiple body chunks passed to the filter. End-of-lines are\n\
|
||||||
@@ -563,7 +593,7 @@ milter_set_body_callback(PyObject *self, PyObject *args) {
|
|||||||
return generic_set_callback(args, "O:set_body_callback", &body_callback);
|
return generic_set_callback(args, "O:set_body_callback", &body_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_eom_callback__doc__[] =
|
static const char milter_set_eom_callback__doc__[] =
|
||||||
"set_eom_callback(Function) -> None\n\
|
"set_eom_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked at end of message.\n\
|
Sets the Python function invoked at end of message.\n\
|
||||||
This routine is the only place where special operations\n\
|
This routine is the only place where special operations\n\
|
||||||
@@ -576,7 +606,7 @@ milter_set_eom_callback(PyObject *self, PyObject *args) {
|
|||||||
return generic_set_callback(args, "O:set_eom_callback", &eom_callback);
|
return generic_set_callback(args, "O:set_eom_callback", &eom_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_abort_callback__doc__[] =
|
static const char milter_set_abort_callback__doc__[] =
|
||||||
"set_abort_callback(Function) -> None\n\
|
"set_abort_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked if message is aborted\n\
|
Sets the Python function invoked if message is aborted\n\
|
||||||
outside of the control of the filter, for example,\n\
|
outside of the control of the filter, for example,\n\
|
||||||
@@ -590,7 +620,7 @@ milter_set_abort_callback(PyObject *self, PyObject *args) {
|
|||||||
return generic_set_callback(args, "O:set_abort_callback", &abort_callback);
|
return generic_set_callback(args, "O:set_abort_callback", &abort_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_set_close_callback__doc__[] =
|
static const char milter_set_close_callback__doc__[] =
|
||||||
"set_close_callback(Function) -> None\n\
|
"set_close_callback(Function) -> None\n\
|
||||||
Sets the Python function invoked at end of the connection. This\n\
|
Sets the Python function invoked at end of the connection. This\n\
|
||||||
is called on close even if the previous mail transaction was aborted.\n\
|
is called on close even if the previous mail transaction was aborted.\n\
|
||||||
@@ -603,7 +633,7 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
|
|||||||
|
|
||||||
static int exception_policy = SMFIS_TEMPFAIL;
|
static int exception_policy = SMFIS_TEMPFAIL;
|
||||||
|
|
||||||
static char milter_set_exception_policy__doc__[] =
|
static const char milter_set_exception_policy__doc__[] =
|
||||||
"set_exception_policy(i) -> None\n\
|
"set_exception_policy(i) -> None\n\
|
||||||
Sets the policy for untrapped Python exceptions during a callback.\n\
|
Sets the policy for untrapped Python exceptions during a callback.\n\
|
||||||
Must be one of TEMPFAIL,REJECT,CONTINUE";
|
Must be one of TEMPFAIL,REJECT,CONTINUE";
|
||||||
@@ -614,7 +644,8 @@ milter_set_exception_policy(PyObject *self, PyObject *args) {
|
|||||||
if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i))
|
if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i))
|
||||||
return NULL;
|
return NULL;
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case SMFIS_REJECT: case SMFIS_TEMPFAIL: case SMFIS_CONTINUE:
|
case SMFIS_REJECT: case SMFIS_TEMPFAIL:
|
||||||
|
case SMFIS_CONTINUE: case SMFIS_ACCEPT:
|
||||||
exception_policy = i;
|
exception_policy = i;
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return Py_None;
|
return Py_None;
|
||||||
@@ -629,23 +660,30 @@ _release_thread(PyThreadState *t) {
|
|||||||
PyEval_ReleaseThread(t);
|
PyEval_ReleaseThread(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Report and clear any python exception before returning to libmilter.
|
/** Report and clear any python exception before returning to libmilter.
|
||||||
The interpreter is locked when we are called, and we unlock it. */
|
The interpreter is locked when we are called, and we unlock it. */
|
||||||
static int _report_exception(milter_ContextObject *self) {
|
static int _report_exception(milter_ContextObject *self) {
|
||||||
|
char untrapped_msg[80];
|
||||||
if (PyErr_Occurred()) {
|
if (PyErr_Occurred()) {
|
||||||
|
sprintf(untrapped_msg,"pymilter: untrapped exception in %.40s",
|
||||||
|
description.xxfi_name);
|
||||||
PyErr_Print();
|
PyErr_Print();
|
||||||
PyErr_Clear(); /* must clear since not returning to python */
|
PyErr_Clear(); /* must clear since not returning to python */
|
||||||
_release_thread(self->t);
|
_release_thread(self->t);
|
||||||
switch (exception_policy) {
|
switch (exception_policy) {
|
||||||
case SMFIS_REJECT:
|
case SMFIS_REJECT:
|
||||||
smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure");
|
smfi_setreply(self->ctx, "554", "5.3.0", untrapped_msg);
|
||||||
return SMFIS_REJECT;
|
return SMFIS_REJECT;
|
||||||
case SMFIS_TEMPFAIL:
|
case SMFIS_TEMPFAIL:
|
||||||
smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure");
|
smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg);
|
||||||
return SMFIS_TEMPFAIL;
|
return SMFIS_TEMPFAIL;
|
||||||
}
|
}
|
||||||
return SMFIS_CONTINUE;
|
return exception_policy;
|
||||||
}
|
}
|
||||||
|
/* This should never happen, _report_exception is only called when
|
||||||
|
* the caller has already detected a python exception. If it
|
||||||
|
* does somehow happen, pretend nothing is wrong... */
|
||||||
_release_thread(self->t);
|
_release_thread(self->t);
|
||||||
return SMFIS_CONTINUE;
|
return SMFIS_CONTINUE;
|
||||||
}
|
}
|
||||||
@@ -667,8 +705,8 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
|
|||||||
const char *cbname = "milter";
|
const char *cbname = "milter";
|
||||||
char buf[40];
|
char buf[40];
|
||||||
Py_DECREF(result);
|
Py_DECREF(result);
|
||||||
for (p = callback_names; p->cbp; ++p) {
|
for (p = callback; p->name; ++p) {
|
||||||
if (cb == *p->cbp) {
|
if (cb == p->cb) {
|
||||||
cbname = p->name;
|
cbname = p->name;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -973,7 +1011,7 @@ milter_wrap_close(SMFICTX *ctx) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_register__doc__[] =
|
static const char milter_register__doc__[] =
|
||||||
"register(name,unknown=,data=,negotiate=) -> None\n\
|
"register(name,unknown=,data=,negotiate=) -> None\n\
|
||||||
Registers the milter name with current callbacks, and flags.\n\
|
Registers the milter name with current callbacks, and flags.\n\
|
||||||
Required before main() is called.";
|
Required before main() is called.";
|
||||||
@@ -1018,7 +1056,7 @@ milter_register(PyObject *self, PyObject *args, PyObject *kwds) {
|
|||||||
return _generic_return(smfi_register(description), "cannot register");
|
return _generic_return(smfi_register(description), "cannot register");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_opensocket__doc__[] =
|
static const char milter_opensocket__doc__[] =
|
||||||
"opensocket(rmsock) -> None\n\
|
"opensocket(rmsock) -> None\n\
|
||||||
Attempts to create and open the socket provided with setconn.\n\
|
Attempts to create and open the socket provided with setconn.\n\
|
||||||
Removes the socket first if rmsock is True.";
|
Removes the socket first if rmsock is True.";
|
||||||
@@ -1031,7 +1069,7 @@ milter_opensocket(PyObject *self, PyObject *args) {
|
|||||||
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
|
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_main__doc__[] =
|
static const char milter_main__doc__[] =
|
||||||
"main() -> None\n\
|
"main() -> None\n\
|
||||||
Main milter routine. Set any callbacks, and flags desired, then call\n\
|
Main milter routine. Set any callbacks, and flags desired, then call\n\
|
||||||
setconn(), then call register(name), and finally call main().";
|
setconn(), then call register(name), and finally call main().";
|
||||||
@@ -1055,7 +1093,7 @@ milter_main(PyObject *self, PyObject *args) {
|
|||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_setdbg__doc__[] =
|
static const char milter_setdbg__doc__[] =
|
||||||
"setdbg(int) -> None\n\
|
"setdbg(int) -> None\n\
|
||||||
Sets debug level in sendmail/libmilter source. Dubious usefulness.";
|
Sets debug level in sendmail/libmilter source. Dubious usefulness.";
|
||||||
|
|
||||||
@@ -1066,7 +1104,7 @@ milter_setdbg(PyObject *self, PyObject *args) {
|
|||||||
return _generic_return(smfi_setdbg(val), "cannot set debug value");
|
return _generic_return(smfi_setdbg(val), "cannot set debug value");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_setbacklog__doc__[] =
|
static const char milter_setbacklog__doc__[] =
|
||||||
"setbacklog(int) -> None\n\
|
"setbacklog(int) -> None\n\
|
||||||
Set the TCP connection queue size for the milter socket.";
|
Set the TCP connection queue size for the milter socket.";
|
||||||
|
|
||||||
@@ -1078,7 +1116,7 @@ milter_setbacklog(PyObject *self, PyObject *args) {
|
|||||||
return _generic_return(smfi_setbacklog(val), "cannot set backlog");
|
return _generic_return(smfi_setbacklog(val), "cannot set backlog");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_settimeout__doc__[] =
|
static const char milter_settimeout__doc__[] =
|
||||||
"settimeout(int) -> None\n\
|
"settimeout(int) -> None\n\
|
||||||
Set the time (in seconds) that sendmail will wait before\n\
|
Set the time (in seconds) that sendmail will wait before\n\
|
||||||
considering this filter dead.";
|
considering this filter dead.";
|
||||||
@@ -1091,7 +1129,7 @@ milter_settimeout(PyObject *self, PyObject *args) {
|
|||||||
return _generic_return(smfi_settimeout(val), "cannot set timeout");
|
return _generic_return(smfi_settimeout(val), "cannot set timeout");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_setconn__doc__[] =
|
static const char milter_setconn__doc__[] =
|
||||||
"setconn(filename) -> None\n\
|
"setconn(filename) -> None\n\
|
||||||
Sets the pathname to the unix, inet, or inet6 socket that\n\
|
Sets the pathname to the unix, inet, or inet6 socket that\n\
|
||||||
sendmail will use to communicate with this filter. By default,\n\
|
sendmail will use to communicate with this filter. By default,\n\
|
||||||
@@ -1111,7 +1149,7 @@ milter_setconn(PyObject *self, PyObject *args) {
|
|||||||
return _generic_return(smfi_setconn(str), "cannot set connection");
|
return _generic_return(smfi_setconn(str), "cannot set connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_stop__doc__[] =
|
static const char milter_stop__doc__[] =
|
||||||
"stop() -> None\n\
|
"stop() -> None\n\
|
||||||
This function appears to be a controlled method to tell sendmail to\n\
|
This function appears to be a controlled method to tell sendmail to\n\
|
||||||
stop using this filter. It will close the socket.";
|
stop using this filter. It will close the socket.";
|
||||||
@@ -1124,7 +1162,7 @@ milter_stop(PyObject *self, PyObject *args) {
|
|||||||
return _thread_return(t,smfi_stop(), "cannot stop");
|
return _thread_return(t,smfi_stop(), "cannot stop");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_getdiag__doc__[] =
|
static const char milter_getdiag__doc__[] =
|
||||||
"getdiag() -> tuple\n\
|
"getdiag() -> tuple\n\
|
||||||
Return a tuple of diagnostic data. The first two items are context new\n\
|
Return a tuple of diagnostic data. The first two items are context new\n\
|
||||||
count and context del count. The rest are yet to be defined.";
|
count and context del count. The rest are yet to be defined.";
|
||||||
@@ -1134,7 +1172,7 @@ milter_getdiag(PyObject *self, PyObject *args) {
|
|||||||
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
|
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_getversion__doc__[] =
|
static const char milter_getversion__doc__[] =
|
||||||
"getversion() -> tuple\n\
|
"getversion() -> tuple\n\
|
||||||
Return runtime libmilter version as a tuple of major,minor,patchlevel.";
|
Return runtime libmilter version as a tuple of major,minor,patchlevel.";
|
||||||
static PyObject *
|
static PyObject *
|
||||||
@@ -1148,7 +1186,7 @@ milter_getversion(PyObject *self, PyObject *args) {
|
|||||||
return Py_BuildValue("(kkk)", major,minor,patch);
|
return Py_BuildValue("(kkk)", major,minor,patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_getsymval__doc__[] =
|
static const char milter_getsymval__doc__[] =
|
||||||
"getsymval(String) -> String\n\
|
"getsymval(String) -> String\n\
|
||||||
Returns a symbol's value. Context-dependent, and unclear from the dox.";
|
Returns a symbol's value. Context-dependent, and unclear from the dox.";
|
||||||
|
|
||||||
@@ -1163,7 +1201,7 @@ milter_getsymval(PyObject *self, PyObject *args) {
|
|||||||
return Py_BuildValue("s", smfi_getsymval(ctx, str));
|
return Py_BuildValue("s", smfi_getsymval(ctx, str));
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_setreply__doc__[] =
|
static const char milter_setreply__doc__[] =
|
||||||
"setreply(rcode, xcode, message) -> None\n\
|
"setreply(rcode, xcode, message) -> None\n\
|
||||||
Sets the specific reply code to be used in response\n\
|
Sets the specific reply code to be used in response\n\
|
||||||
to the active command.\n\
|
to the active command.\n\
|
||||||
@@ -1227,7 +1265,7 @@ milter_setreply(PyObject *self, PyObject *args) {
|
|||||||
"cannot set reply");
|
"cannot set reply");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_addheader__doc__[] =
|
static const char milter_addheader__doc__[] =
|
||||||
"addheader(field, value, idx=-1) -> None\n\
|
"addheader(field, value, idx=-1) -> None\n\
|
||||||
Add a header to the message. This header is not passed to other\n\
|
Add a header to the message. This header is not passed to other\n\
|
||||||
filters. It is not checked for standards compliance;\n\
|
filters. It is not checked for standards compliance;\n\
|
||||||
@@ -1264,7 +1302,7 @@ milter_addheader(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef SMFIF_CHGFROM
|
#ifdef SMFIF_CHGFROM
|
||||||
static char milter_chgfrom__doc__[] =
|
static const char milter_chgfrom__doc__[] =
|
||||||
"chgfrom(sender,params) -> None\n\
|
"chgfrom(sender,params) -> None\n\
|
||||||
Change the envelope sender (MAIL From) of the current message.\n\
|
Change the envelope sender (MAIL From) of the current message.\n\
|
||||||
A filter which calls smfi_chgfrom must have set the CHGFROM flag\n\
|
A filter which calls smfi_chgfrom must have set the CHGFROM flag\n\
|
||||||
@@ -1287,7 +1325,7 @@ milter_chgfrom(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static char milter_chgheader__doc__[] =
|
static const char milter_chgheader__doc__[] =
|
||||||
"chgheader(field, int, value) -> None\n\
|
"chgheader(field, int, value) -> None\n\
|
||||||
Change/delete a header in the message. \n\
|
Change/delete a header in the message. \n\
|
||||||
It is not checked for standards compliance; the mail filter\n\
|
It is not checked for standards compliance; the mail filter\n\
|
||||||
@@ -1315,7 +1353,7 @@ milter_chgheader(PyObject *self, PyObject *args) {
|
|||||||
"cannot change header");
|
"cannot change header");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_addrcpt__doc__[] =
|
static const char milter_addrcpt__doc__[] =
|
||||||
"addrcpt(string,params=None) -> None\n\
|
"addrcpt(string,params=None) -> None\n\
|
||||||
Add a recipient to the envelope. It must be in the same format\n\
|
Add a recipient to the envelope. It must be in the same format\n\
|
||||||
as is passed to the envrcpt callback in the first tuple element.\n\
|
as is passed to the envrcpt callback in the first tuple element.\n\
|
||||||
@@ -1345,7 +1383,7 @@ milter_addrcpt(PyObject *self, PyObject *args) {
|
|||||||
return _thread_return(t,rc, "cannot add recipient");
|
return _thread_return(t,rc, "cannot add recipient");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_delrcpt__doc__[] =
|
static const char milter_delrcpt__doc__[] =
|
||||||
"delrcpt(string) -> None\n\
|
"delrcpt(string) -> None\n\
|
||||||
Delete a recipient from the envelope.\n\
|
Delete a recipient from the envelope.\n\
|
||||||
This function can only be called from the EOM callback.";
|
This function can only be called from the EOM callback.";
|
||||||
@@ -1363,7 +1401,7 @@ milter_delrcpt(PyObject *self, PyObject *args) {
|
|||||||
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
|
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_replacebody__doc__[] =
|
static const char milter_replacebody__doc__[] =
|
||||||
"replacebody(string) -> None\n\
|
"replacebody(string) -> None\n\
|
||||||
Replace the body of the message. This routine may be called multiple\n\
|
Replace the body of the message. This routine may be called multiple\n\
|
||||||
times if the body is longer than convenient to send in one call. End of\n\
|
times if the body is longer than convenient to send in one call. End of\n\
|
||||||
@@ -1385,7 +1423,7 @@ milter_replacebody(PyObject *self, PyObject *args) {
|
|||||||
(unsigned char *)bodyp, bodylen), "cannot replace message body");
|
(unsigned char *)bodyp, bodylen), "cannot replace message body");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_setpriv__doc__[] =
|
static const char milter_setpriv__doc__[] =
|
||||||
"setpriv(object) -> object\n\
|
"setpriv(object) -> object\n\
|
||||||
Associates any Python object with this context, and returns\n\
|
Associates any Python object with this context, and returns\n\
|
||||||
the old value or None. Use this to\n\
|
the old value or None. Use this to\n\
|
||||||
@@ -1411,7 +1449,7 @@ milter_setpriv(PyObject *self, PyObject *args) {
|
|||||||
return old;
|
return old;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_getpriv__doc__[] =
|
static const char milter_getpriv__doc__[] =
|
||||||
"getpriv() -> None\n\
|
"getpriv() -> None\n\
|
||||||
Returns the Python object associated with the current context (if any).\n\
|
Returns the Python object associated with the current context (if any).\n\
|
||||||
Use this in conjunction with setpriv to keep track of data in a thread-safe\n\
|
Use this in conjunction with setpriv to keep track of data in a thread-safe\n\
|
||||||
@@ -1429,7 +1467,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef SMFIF_QUARANTINE
|
#ifdef SMFIF_QUARANTINE
|
||||||
static char milter_quarantine__doc__[] =
|
static const char milter_quarantine__doc__[] =
|
||||||
"quarantine(string) -> None\n\
|
"quarantine(string) -> None\n\
|
||||||
Place the message in quarantine. A string with a description of the reason\n\
|
Place the message in quarantine. A string with a description of the reason\n\
|
||||||
is the only argument.";
|
is the only argument.";
|
||||||
@@ -1450,7 +1488,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SMFIR_PROGRESS
|
#ifdef SMFIR_PROGRESS
|
||||||
static char milter_progress__doc__[] =
|
static const char milter_progress__doc__[] =
|
||||||
"progress() -> None\n\
|
"progress() -> None\n\
|
||||||
Notify the MTA that we are working on a message so it will reset timeouts.";
|
Notify the MTA that we are working on a message so it will reset timeouts.";
|
||||||
|
|
||||||
@@ -1467,23 +1505,23 @@ milter_progress(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SMFIF_SETSMLIST
|
#ifdef SMFIF_SETSYMLIST
|
||||||
static char milter_setsmlist__doc__[] =
|
static const char milter_setsymlist__doc__[] =
|
||||||
"setsmlist(stage,macrolist) -> None\n\
|
"setsymlist(stage,macrolist) -> None\n\
|
||||||
Tell the MTA which macro values we are interested in for a given stage";
|
Tell the MTA which macro values we are interested in for a given stage";
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
milter_setsmlist(PyObject *self, PyObject *args) {
|
milter_setsymlist(PyObject *self, PyObject *args) {
|
||||||
SMFICTX *ctx;
|
SMFICTX *ctx;
|
||||||
PyThreadState *t;
|
PyThreadState *t;
|
||||||
int stage = 0;
|
int stage = 0;
|
||||||
char *smlist = 0;
|
char *smlist = 0;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "is:setsmlist",&stage, &smlist)) return NULL;
|
if (!PyArg_ParseTuple(args, "is:setsymlist",&stage, &smlist)) return NULL;
|
||||||
ctx = _find_context(self);
|
ctx = _find_context(self);
|
||||||
if (ctx == NULL) return NULL;
|
if (ctx == NULL) return NULL;
|
||||||
t = PyEval_SaveThread();
|
t = PyEval_SaveThread();
|
||||||
return _thread_return(t,smfi_setsmlist(ctx,stage,smlist),
|
return _thread_return(t,smfi_setsymlist(ctx,stage,smlist),
|
||||||
"cannot set macro list");
|
"cannot set macro list");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1507,8 +1545,8 @@ static PyMethodDef context_methods[] = {
|
|||||||
#ifdef SMFIF_CHGFROM
|
#ifdef SMFIF_CHGFROM
|
||||||
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
|
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
|
||||||
#endif
|
#endif
|
||||||
#ifdef SMFIF_SETSMLIST
|
#ifdef SMFIF_SETSYMLIST
|
||||||
{ "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__doc__},
|
{ "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__doc__},
|
||||||
#endif
|
#endif
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
@@ -1589,7 +1627,7 @@ static PyTypeObject milter_ContextType = {
|
|||||||
Py_TPFLAGS_DEFAULT, /* tp_flags */
|
Py_TPFLAGS_DEFAULT, /* tp_flags */
|
||||||
};
|
};
|
||||||
|
|
||||||
static char milter_documentation[] =
|
static const char milter_documentation[] =
|
||||||
"This module interfaces with Sendmail's libmilter functionality,\n\
|
"This module interfaces with Sendmail's libmilter functionality,\n\
|
||||||
allowing one to write email filters directly in Python.\n\
|
allowing one to write email filters directly in Python.\n\
|
||||||
Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
|
Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
|
||||||
@@ -1631,8 +1669,15 @@ initmilter(void) {
|
|||||||
#ifdef SMFIF_CHGFROM
|
#ifdef SMFIF_CHGFROM
|
||||||
setitem(d,"CHGFROM",SMFIF_CHGFROM);
|
setitem(d,"CHGFROM",SMFIF_CHGFROM);
|
||||||
#endif
|
#endif
|
||||||
#ifdef SMFIF_SETSMLIST
|
#ifdef SMFIF_SETSYMLIST
|
||||||
setitem(d,"SETSMLIST",SMFIF_SETSMLIST);
|
setitem(d,"SETSYMLIST",SMFIF_SETSYMLIST);
|
||||||
|
setitem(d,"M_CONNECT",SMFIM_CONNECT);/* connect */
|
||||||
|
setitem(d,"M_HELO",SMFIM_HELO); /* HELO/EHLO */
|
||||||
|
setitem(d,"M_ENVFROM",SMFIM_ENVFROM);/* MAIL From */
|
||||||
|
setitem(d,"M_ENVRCPT",SMFIM_ENVRCPT);/* RCPT To */
|
||||||
|
setitem(d,"M_DATA",SMFIM_DATA); /* DATA */
|
||||||
|
setitem(d,"M_EOM",SMFIM_EOM); /* end of message (final dot) */
|
||||||
|
setitem(d,"M_EOH",SMFIM_EOH); /* end of header */
|
||||||
#endif
|
#endif
|
||||||
#ifdef SMFIS_ALL_OPTS
|
#ifdef SMFIS_ALL_OPTS
|
||||||
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
|
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.8 2011/11/05 15:51:03 customdesigned
|
||||||
|
# New example
|
||||||
|
#
|
||||||
|
# Revision 1.7 2009/06/13 21:15:12 customdesigned
|
||||||
|
# Doxygen updates.
|
||||||
|
#
|
||||||
# Revision 1.6 2009/06/09 03:13:13 customdesigned
|
# Revision 1.6 2009/06/09 03:13:13 customdesigned
|
||||||
# More doxygen docs.
|
# More doxygen docs.
|
||||||
#
|
#
|
||||||
@@ -165,15 +171,14 @@ class MimeMessage(Message):
|
|||||||
"""
|
"""
|
||||||
def __init__(self,fp=None,seekable=1):
|
def __init__(self,fp=None,seekable=1):
|
||||||
Message.__init__(self)
|
Message.__init__(self)
|
||||||
self.headerchange = None
|
|
||||||
self.submsg = None
|
self.submsg = None
|
||||||
self.modified = False
|
self.modified = False
|
||||||
|
|
||||||
## @var headerchange
|
## @var headerchange
|
||||||
# Provide a headerchange event for integration with Milter.
|
# Provide a headerchange event for integration with Milter.
|
||||||
# The headerchange attribute can be assigned a function to be called when
|
# The headerchange attribute can be assigned a function to be called when
|
||||||
# changing headers. The signature is:
|
# changing headers. The signature is:
|
||||||
# headerchange(msg,name,value) -> None
|
# headerchange(msg,name,value) -> None
|
||||||
|
self.headerchange = None
|
||||||
|
|
||||||
def get_param(self, param, failobj=None, header='content-type', unquote=True):
|
def get_param(self, param, failobj=None, header='content-type', unquote=True):
|
||||||
val = Message.get_param(self,param,failobj,header,unquote)
|
val = Message.get_param(self,param,failobj,header,unquote)
|
||||||
|
|||||||
+59
-27
@@ -1,14 +1,15 @@
|
|||||||
%define __python python2.6
|
%define __python python2.6
|
||||||
%define pythonbase python26
|
%define pythonbase python
|
||||||
|
|
||||||
%define libdir %{_libdir}/pymilter
|
%define libdir %{_libdir}/pymilter
|
||||||
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
|
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
|
||||||
|
|
||||||
Summary: Python interface to sendmail milter API
|
Summary: Python interface to sendmail milter API
|
||||||
Name: %{pythonbase}-pymilter
|
Name: %{pythonbase}-pymilter
|
||||||
Version: 0.9.5
|
Version: 1.0
|
||||||
Release: 1%{dist}
|
Release: 1%{dist}
|
||||||
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
|
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
|
||||||
|
Source1: pymilter.te
|
||||||
License: GPLv2+
|
License: GPLv2+
|
||||||
Group: Development/Libraries
|
Group: Development/Libraries
|
||||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||||
@@ -26,11 +27,23 @@ attach to sendmail's libmilter functionality. Additional python
|
|||||||
modules provide for navigating and modifying MIME parts, sending
|
modules provide for navigating and modifying MIME parts, sending
|
||||||
DSNs, and doing CBV.
|
DSNs, and doing CBV.
|
||||||
|
|
||||||
|
%package selinux
|
||||||
|
Summary: SELinux policy module for pymilter
|
||||||
|
Group: System Environment/Base
|
||||||
|
Requires: policycoreutils, selinux-policy, %{name}
|
||||||
|
BuildRequires: policycoreutils, checkpolicy
|
||||||
|
|
||||||
|
%description selinux
|
||||||
|
SELinux policy module for using pymilter with sendmail with selinux enforcing
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
%setup -q -n pymilter-%{version}
|
%setup -q -n pymilter-%{version}
|
||||||
|
cp %{SOURCE1} pymilter.te
|
||||||
|
|
||||||
%build
|
%build
|
||||||
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
|
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
|
||||||
|
checkmodule -m -M -o pymilter.mod pymilter.te
|
||||||
|
semodule_package -o pymilter.pp -m pymilter.mod
|
||||||
|
|
||||||
%install
|
%install
|
||||||
rm -rf $RPM_BUILD_ROOT
|
rm -rf $RPM_BUILD_ROOT
|
||||||
@@ -38,31 +51,11 @@ rm -rf $RPM_BUILD_ROOT
|
|||||||
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
|
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
|
||||||
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
|
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
|
||||||
mkdir -p $RPM_BUILD_ROOT%{libdir}
|
mkdir -p $RPM_BUILD_ROOT%{libdir}
|
||||||
cp start.sh $RPM_BUILD_ROOT%{libdir}
|
|
||||||
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
|
|
||||||
/^datadir=/
|
|
||||||
c
|
|
||||||
datadir="%{_localstatedir}/log/milter"
|
|
||||||
.
|
|
||||||
/^piddir=/
|
|
||||||
c
|
|
||||||
piddir="%{_localstatedir}/run/milter"
|
|
||||||
.
|
|
||||||
/^libdir=/
|
|
||||||
c
|
|
||||||
libdir="%{libdir}"
|
|
||||||
.
|
|
||||||
/^python=/
|
|
||||||
c
|
|
||||||
python="%{__python}"
|
|
||||||
.
|
|
||||||
w
|
|
||||||
q
|
|
||||||
EOF
|
|
||||||
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
|
|
||||||
|
|
||||||
# start.sh is used by spfmilter, srsmilter, and milter, and could be used by
|
# install selinux modules
|
||||||
# other milters using pymilter.
|
mkdir -p %{buildroot}%{_datadir}/selinux/targeted
|
||||||
|
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
|
||||||
|
|
||||||
%files
|
%files
|
||||||
%defattr(-,root,root,-)
|
%defattr(-,root,root,-)
|
||||||
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
|
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
|
||||||
@@ -71,11 +64,50 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
|
|||||||
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
|
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
|
||||||
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
|
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
|
||||||
|
|
||||||
|
%files selinux
|
||||||
|
%doc pymilter.te
|
||||||
|
%{_datadir}/selinux/targeted/*
|
||||||
|
|
||||||
%clean
|
%clean
|
||||||
rm -rf $RPM_BUILD_ROOT
|
rm -rf $RPM_BUILD_ROOT
|
||||||
|
|
||||||
|
%post selinux
|
||||||
|
/usr/sbin/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
|
||||||
|
&>/dev/null || :
|
||||||
|
|
||||||
|
%postun selinux
|
||||||
|
if [ $1 -eq 0 ] ; then
|
||||||
|
/usr/sbin/semodule -s targeted -r pymilter &> /dev/null || :
|
||||||
|
fi
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.5-1
|
* Sat Mar 1 2014 Stuart Gathman <stuart@gathman.org> 1.0-2
|
||||||
|
- Remove start.sh to track EPEL repository, suggest daemonize as replacement
|
||||||
|
- Selinux subpackage should not care about pymilter version
|
||||||
|
|
||||||
|
* Wed Jun 26 2013 Stuart Gathman <stuart@gathman.org> 1.0-1
|
||||||
|
- Allow ACCEPT as untrapped exception policy
|
||||||
|
- Optional dir for getaddrset and getaddrdict in Milter.config
|
||||||
|
- Show registered milter name in untrapped exception message.
|
||||||
|
- Include selinux subpackage
|
||||||
|
- Provide Milter.greylist export and Milter.greylist import to migrate data
|
||||||
|
|
||||||
|
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1
|
||||||
|
- Add Milter.test module for unit testing milters.
|
||||||
|
- Fix typo that prevented setsymlist from being active.
|
||||||
|
- Change untrapped exception message to:
|
||||||
|
- "pymilter: untrapped exception in milter app"
|
||||||
|
|
||||||
|
* Thu Apr 12 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
|
||||||
|
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
|
||||||
|
- 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.
|
- Print milter.error for invalid callback return type.
|
||||||
(Since stacktrace is empty, the TypeError exception is confusing.)
|
(Since stacktrace is empty, the TypeError exception is confusing.)
|
||||||
- Fix milter-template.py
|
- Fix milter-template.py
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
module pymilter 1.0;
|
||||||
|
|
||||||
|
require {
|
||||||
|
type sendmail_t;
|
||||||
|
type var_run_t;
|
||||||
|
type initrc_t;
|
||||||
|
class sock_file { write getattr };
|
||||||
|
class unix_stream_socket connectto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#============= sendmail_t ==============
|
||||||
|
allow sendmail_t initrc_t:unix_stream_socket connectto;
|
||||||
|
allow sendmail_t var_run_t:sock_file { write getattr };
|
||||||
@@ -35,7 +35,9 @@ class sampleMilter(Milter.Milter):
|
|||||||
# multiple messages can be received on a single connection
|
# multiple messages can be received on a single connection
|
||||||
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
|
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
|
||||||
# of each message.
|
# of each message.
|
||||||
|
@Milter.noreply
|
||||||
def envfrom(self,f,*str):
|
def envfrom(self,f,*str):
|
||||||
|
"start of MAIL transaction"
|
||||||
self.log("mail from",f,str)
|
self.log("mail from",f,str)
|
||||||
self.fp = StringIO.StringIO()
|
self.fp = StringIO.StringIO()
|
||||||
self.tempname = None
|
self.tempname = None
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[bdist_rpm]
|
[bdist_rpm]
|
||||||
python=python2.6
|
python=python2.6
|
||||||
doc_files=README NEWS TODO
|
doc_files=README NEWS TODO COPYING CREDITS
|
||||||
packager=Stuart D. Gathman <stuart@bmsi.com>
|
packager=Stuart D. Gathman <stuart@gathman.org>
|
||||||
release=1
|
release=1
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ libs = ["milter"]
|
|||||||
libdirs = ["/usr/lib/libmilter"] # needed for Debian
|
libdirs = ["/usr/lib/libmilter"] # needed for Debian
|
||||||
|
|
||||||
# NOTE: importing Milter to obtain version fails when milter.so not built
|
# NOTE: importing Milter to obtain version fails when milter.so not built
|
||||||
setup(name = "pymilter", version = '0.9.5',
|
setup(name = "pymilter", version = '1.0',
|
||||||
description="Python interface to sendmail milter API",
|
description="Python interface to sendmail milter API",
|
||||||
long_description="""\
|
long_description="""\
|
||||||
This is a python extension module to enable python scripts to
|
This is a python extension module to enable python scripts to
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import unittest
|
|||||||
import testmime
|
import testmime
|
||||||
import testsample
|
import testsample
|
||||||
import testutils
|
import testutils
|
||||||
|
import testgrey
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
@@ -9,6 +10,7 @@ def suite():
|
|||||||
s.addTest(testmime.suite())
|
s.addTest(testmime.suite())
|
||||||
s.addTest(testsample.suite())
|
s.addTest(testsample.suite())
|
||||||
s.addTest(testutils.suite())
|
s.addTest(testutils.suite())
|
||||||
|
s.addTest(testgrey.suite())
|
||||||
return s
|
return s
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+18587
File diff suppressed because it is too large
Load Diff
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import unittest
|
||||||
|
import doctest
|
||||||
|
import os
|
||||||
|
#from Milter.greylist import Greylist
|
||||||
|
from Milter.greysql import Greylist
|
||||||
|
|
||||||
|
class GreylistTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.fname = 'test.db'
|
||||||
|
os.remove(self.fname)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
#os.remove(self.fname)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def testGrey(self):
|
||||||
|
grey = Greylist(self.fname)
|
||||||
|
# first time
|
||||||
|
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com')
|
||||||
|
self.assertEqual(rc,0)
|
||||||
|
# not in window yet
|
||||||
|
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*60)
|
||||||
|
self.assertEqual(rc,0)
|
||||||
|
# within window
|
||||||
|
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=15*60)
|
||||||
|
self.assertEqual(rc,1)
|
||||||
|
# new triple
|
||||||
|
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=15*60)
|
||||||
|
self.assertEqual(rc,0)
|
||||||
|
# seen again
|
||||||
|
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*3600)
|
||||||
|
self.assertEqual(rc,2)
|
||||||
|
# new one past expire
|
||||||
|
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=6*3600)
|
||||||
|
self.assertEqual(rc,0)
|
||||||
|
# original past retain
|
||||||
|
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=37*24*3600)
|
||||||
|
self.assertEqual(rc,0)
|
||||||
|
# new one for testing expire
|
||||||
|
rc = grey.check('1.2.3.5','flub@bar.com','baz@spat.com',timeinc=20*24*3600)
|
||||||
|
self.assertEqual(rc,0)
|
||||||
|
grey.close()
|
||||||
|
# test cleanup
|
||||||
|
grey = Greylist(self.fname)
|
||||||
|
rc = grey.clean(timeinc=37*24*3600)
|
||||||
|
self.assertEqual(rc,1)
|
||||||
|
grey.close()
|
||||||
|
|
||||||
|
def suite():
|
||||||
|
s = unittest.makeSuite(GreylistTestCase,'test')
|
||||||
|
return s
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.TextTestRunner().run(suite())
|
||||||
+4
-1
@@ -1,4 +1,7 @@
|
|||||||
# $Log$
|
# $Log$
|
||||||
|
# Revision 1.5 2011/06/09 17:27:42 customdesigned
|
||||||
|
# Documentation updates.
|
||||||
|
#
|
||||||
# Revision 1.4 2005/07/20 14:49:44 customdesigned
|
# Revision 1.4 2005/07/20 14:49:44 customdesigned
|
||||||
# Handle corrupt and empty ZIP files.
|
# Handle corrupt and empty ZIP files.
|
||||||
#
|
#
|
||||||
@@ -170,7 +173,7 @@ class MimeTestCase(unittest.TestCase):
|
|||||||
msg = mime.message_from_file(open('test/'+fname,'r'))
|
msg = mime.message_from_file(open('test/'+fname,'r'))
|
||||||
mime.defang(msg,scan_zip=True)
|
mime.defang(msg,scan_zip=True)
|
||||||
self.failIf(msg.ismodified())
|
self.failIf(msg.ismodified())
|
||||||
msg = mime.message_from_file(open('test/tmpytgcE5.fail','r'))
|
msg = mime.message_from_file(open('test/test2','r'))
|
||||||
rc = mime.check_attachments(msg,self._chk_attach)
|
rc = mime.check_attachments(msg,self._chk_attach)
|
||||||
self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
|
self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
|
||||||
self.assertEquals(rc,Milter.CONTINUE)
|
self.assertEquals(rc,Milter.CONTINUE)
|
||||||
|
|||||||
+10
-95
@@ -4,97 +4,12 @@ import sample
|
|||||||
import mime
|
import mime
|
||||||
import rfc822
|
import rfc822
|
||||||
import StringIO
|
import StringIO
|
||||||
|
from Milter.test import TestBase
|
||||||
|
|
||||||
class TestMilter(sample.sampleMilter):
|
class TestMilter(TestBase,sample.sampleMilter):
|
||||||
|
|
||||||
_protocol = 0
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logfp = open("test/milter.log","a")
|
TestBase.__init__(self)
|
||||||
|
sample.sampleMilter.__init__(self)
|
||||||
def log(self,*msg):
|
|
||||||
for i in msg: print >>self.logfp, i,
|
|
||||||
print >>self.logfp
|
|
||||||
|
|
||||||
def replacebody(self,chunk):
|
|
||||||
if self._body:
|
|
||||||
self._body.write(chunk)
|
|
||||||
self.bodyreplaced = True
|
|
||||||
else:
|
|
||||||
raise IOError,"replacebody not called from eom()"
|
|
||||||
|
|
||||||
# FIXME: rfc822 indexing does not really reflect the way chg/add header
|
|
||||||
# work for a milter
|
|
||||||
def chgheader(self,field,idx,value):
|
|
||||||
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
|
|
||||||
if value == '':
|
|
||||||
del self._msg[field]
|
|
||||||
else:
|
|
||||||
self._msg[field] = value
|
|
||||||
self.headerschanged = True
|
|
||||||
|
|
||||||
def addheader(self,field,value):
|
|
||||||
self.log('addheader: %s=%s' % (field,value))
|
|
||||||
self._msg[field] = value
|
|
||||||
self.headerschanged = True
|
|
||||||
|
|
||||||
def feedMsg(self,fname):
|
|
||||||
self._body = None
|
|
||||||
self.bodyreplaced = False
|
|
||||||
self.headerschanged = 0
|
|
||||||
fp = open('test/'+fname,'r')
|
|
||||||
msg = rfc822.Message(fp)
|
|
||||||
rc = self.envfrom('<spam@advertisements.com>')
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
rc = self.envrcpt('<victim@lamb.com>')
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
line = None
|
|
||||||
for h in msg.headers:
|
|
||||||
if h[:1].isspace():
|
|
||||||
line = line + h
|
|
||||||
continue
|
|
||||||
if not line:
|
|
||||||
line = h
|
|
||||||
continue
|
|
||||||
s = line.split(': ',1)
|
|
||||||
rc = self.header(s[0],s[1].strip())
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
line = h
|
|
||||||
if line:
|
|
||||||
s = line.split(': ',1)
|
|
||||||
rc = self.header(s[0],s[1])
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
rc = self.eoh()
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
while 1:
|
|
||||||
buf = fp.read(8192)
|
|
||||||
if len(buf) == 0: break
|
|
||||||
rc = self.body(buf)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
self._msg = msg
|
|
||||||
self._body = StringIO.StringIO()
|
|
||||||
rc = self.eom()
|
|
||||||
if self.bodyreplaced:
|
|
||||||
body = self._body.getvalue()
|
|
||||||
else:
|
|
||||||
msg.rewindbody()
|
|
||||||
body = msg.fp.read()
|
|
||||||
self._body = StringIO.StringIO()
|
|
||||||
self._body.writelines(msg.headers)
|
|
||||||
self._body.write('\n')
|
|
||||||
self._body.write(body)
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def connect(self,host='localhost'):
|
|
||||||
self._body = None
|
|
||||||
self.bodyreplaced = False
|
|
||||||
rc = sample.sampleMilter.connect(self,host,1,0)
|
|
||||||
if rc != Milter.CONTINUE and rc != Milter.ACCEPT:
|
|
||||||
self.close()
|
|
||||||
return rc
|
|
||||||
rc = self.hello('spamrelay')
|
|
||||||
if rc != Milter.CONTINUE:
|
|
||||||
self.close()
|
|
||||||
return rc
|
|
||||||
|
|
||||||
class BMSMilterTestCase(unittest.TestCase):
|
class BMSMilterTestCase(unittest.TestCase):
|
||||||
|
|
||||||
@@ -104,7 +19,7 @@ class BMSMilterTestCase(unittest.TestCase):
|
|||||||
self.failUnless(rc == Milter.CONTINUE)
|
self.failUnless(rc == Milter.CONTINUE)
|
||||||
rc = milter.feedMsg(fname)
|
rc = milter.feedMsg(fname)
|
||||||
self.failUnless(rc == Milter.ACCEPT)
|
self.failUnless(rc == Milter.ACCEPT)
|
||||||
self.failUnless(milter.bodyreplaced,"Message body not replaced")
|
self.failUnless(milter._bodyreplaced,"Message body not replaced")
|
||||||
fp = milter._body
|
fp = milter._body
|
||||||
open('test/'+fname+".tstout","w").write(fp.getvalue())
|
open('test/'+fname+".tstout","w").write(fp.getvalue())
|
||||||
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
|
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
|
||||||
@@ -119,7 +34,7 @@ class BMSMilterTestCase(unittest.TestCase):
|
|||||||
milter.connect('somehost')
|
milter.connect('somehost')
|
||||||
rc = milter.feedMsg(fname)
|
rc = milter.feedMsg(fname)
|
||||||
self.failUnless(rc == Milter.ACCEPT)
|
self.failUnless(rc == Milter.ACCEPT)
|
||||||
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
|
self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.")
|
||||||
fp = milter._body
|
fp = milter._body
|
||||||
open('test/'+fname+".tstout","w").write(fp.getvalue())
|
open('test/'+fname+".tstout","w").write(fp.getvalue())
|
||||||
milter.close()
|
milter.close()
|
||||||
@@ -129,17 +44,17 @@ class BMSMilterTestCase(unittest.TestCase):
|
|||||||
milter.connect('somehost')
|
milter.connect('somehost')
|
||||||
rc = milter.feedMsg('samp1')
|
rc = milter.feedMsg('samp1')
|
||||||
self.failUnless(rc == Milter.ACCEPT)
|
self.failUnless(rc == Milter.ACCEPT)
|
||||||
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
|
self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.")
|
||||||
rc = milter.feedMsg("virus3")
|
rc = milter.feedMsg("virus3")
|
||||||
self.failUnless(rc == Milter.ACCEPT)
|
self.failUnless(rc == Milter.ACCEPT)
|
||||||
self.failUnless(milter.bodyreplaced,"Message body not replaced")
|
self.failUnless(milter._bodyreplaced,"Message body not replaced")
|
||||||
fp = milter._body
|
fp = milter._body
|
||||||
open("test/virus3.tstout","w").write(fp.getvalue())
|
open("test/virus3.tstout","w").write(fp.getvalue())
|
||||||
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
|
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
|
||||||
rc = milter.feedMsg("virus6")
|
rc = milter.feedMsg("virus6")
|
||||||
self.failUnless(rc == Milter.ACCEPT)
|
self.failUnless(rc == Milter.ACCEPT)
|
||||||
self.failUnless(milter.bodyreplaced,"Message body not replaced")
|
self.failUnless(milter._bodyreplaced,"Message body not replaced")
|
||||||
self.failUnless(milter.headerschanged,"Message headers not adjusted")
|
self.failUnless(milter._headerschanged,"Message headers not adjusted")
|
||||||
fp = milter._body
|
fp = milter._body
|
||||||
open("test/virus6.tstout","w").write(fp.getvalue())
|
open("test/virus6.tstout","w").write(fp.getvalue())
|
||||||
milter.close()
|
milter.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user