Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1efc262dc5 |
-11
@@ -1,11 +0,0 @@
|
||||
*.pyc
|
||||
*.tar.gz
|
||||
build/
|
||||
test/*.out
|
||||
test/*.tstout
|
||||
test/*.log
|
||||
test/*.db
|
||||
test.db
|
||||
dist
|
||||
log
|
||||
MANIFEST
|
||||
@@ -1,5 +1,5 @@
|
||||
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
||||
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
|
||||
and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that
|
||||
kludge and added threading and context objects to it, wrote a proper OO
|
||||
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
||||
it with distutils, and generally transformed it from a quick hack to a
|
||||
@@ -7,6 +7,7 @@ real, usable Python extension.
|
||||
|
||||
Other contributors (in random order):
|
||||
|
||||
|
||||
Daniel Troeder
|
||||
for pointing out a typo in @noreply
|
||||
arkanes@irc.freenode.net
|
||||
@@ -43,4 +44,4 @@ Business Management Systems - http://www.bmsi.com
|
||||
for hosting the website, and providing paying clients who need milter service
|
||||
so I can work on it as part of my day job.
|
||||
|
||||
If I have left anybody out, send me a reminder: stuart@gathman.org
|
||||
If I have left anybody out, send me a reminder: stuart@bmsi.com
|
||||
|
||||
@@ -1,213 +1,3 @@
|
||||
# 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
|
||||
# Generate special exception when callback return not int.
|
||||
#
|
||||
# Revision 1.27 2009/07/28 21:45:54 customdesigned
|
||||
# Add getversion() to return runtime version.
|
||||
#
|
||||
# Revision 1.26 2009/07/28 21:08:20 customdesigned
|
||||
# Increment del count.
|
||||
#
|
||||
# Revision 1.25 2009/07/28 20:58:55 customdesigned
|
||||
# getdiag method
|
||||
#
|
||||
# Revision 1.24 2009/06/09 01:54:44 customdesigned
|
||||
# Forgot to initialize optional parameter.
|
||||
#
|
||||
# Revision 1.23 2009/05/29 20:44:58 customdesigned
|
||||
# Typo SMFIP_NO constants.
|
||||
#
|
||||
# Revision 1.22 2009/05/29 19:53:36 customdesigned
|
||||
# Typo SMFIS_ALL_OPTS
|
||||
#
|
||||
# Revision 1.21 2009/05/29 19:49:40 customdesigned
|
||||
# Typo calling helo instead of negotiate.
|
||||
#
|
||||
# Revision 1.20 2009/05/29 18:25:59 customdesigned
|
||||
# Null terminate keyword list.
|
||||
#
|
||||
# Revision 1.19 2009/05/28 18:36:42 customdesigned
|
||||
# Support new callbacks, including negotiate
|
||||
#
|
||||
# Revision 1.18 2009/05/21 21:53:05 customdesigned
|
||||
# First cut at support unknown, data, negotiate callbacks.
|
||||
#
|
||||
# Revision 1.17 2009/02/06 04:28:08 customdesigned
|
||||
# Oops! Missing options argument pointer for addrcpt.
|
||||
#
|
||||
# Revision 1.16 2008/12/16 04:21:05 customdesigned
|
||||
# Fedora release
|
||||
#
|
||||
# Revision 1.15 2008/12/13 20:29:56 customdesigned
|
||||
# Split off milter applications.
|
||||
#
|
||||
# Revision 1.14 2008/12/04 19:43:00 customdesigned
|
||||
# Doc updates.
|
||||
#
|
||||
# Revision 1.13 2008/11/23 03:06:47 customdesigned
|
||||
# Milter support for chgfrom.
|
||||
#
|
||||
# Revision 1.12 2008/11/21 20:42:52 customdesigned
|
||||
# Support smfi_chgfrom and smfi_addrcpt_par.
|
||||
#
|
||||
# Revision 1.11 2007/09/25 02:26:29 customdesigned
|
||||
# Update license.
|
||||
#
|
||||
# Revision 1.10 2006/02/12 02:00:42 customdesigned
|
||||
# Resolve FIXME for wrap_close.
|
||||
#
|
||||
# Revision 1.9 2005/12/23 21:46:36 customdesigned
|
||||
# Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER)
|
||||
#
|
||||
# Revision 1.8 2005/10/20 23:23:36 customdesigned
|
||||
# Include smfi_progress is SMFIR_PROGRESS defined
|
||||
#
|
||||
# Revision 1.7 2005/10/20 23:04:46 customdesigned
|
||||
# Add optional idx for position of added header.
|
||||
#
|
||||
# Revision 1.6 2005/07/15 22:18:17 customdesigned
|
||||
# Support callback exception policy
|
||||
#
|
||||
# Revision 1.5 2005/06/24 04:20:07 customdesigned
|
||||
# Report context allocation error.
|
||||
#
|
||||
# Revision 1.4 2005/06/24 04:12:43 customdesigned
|
||||
# Remove unused name argument to generic wrappers.
|
||||
#
|
||||
# Revision 1.3 2005/06/24 03:57:35 customdesigned
|
||||
# Handle close called before connect.
|
||||
#
|
||||
# Revision 1.2 2005/06/02 04:18:55 customdesigned
|
||||
# Update copyright notices after reading article on /.
|
||||
#
|
||||
# Revision 1.1.1.2 2005/05/31 18:09:06 customdesigned
|
||||
# Release 0.7.1
|
||||
#
|
||||
# Revision 2.31 2004/08/23 02:24:36 stuart
|
||||
# Support setbacklog
|
||||
#
|
||||
# Revision 2.30 2004/08/21 20:29:53 stuart
|
||||
# Support option of 11 lines max for mlreply.
|
||||
#
|
||||
# Revision 2.29 2004/08/21 04:14:29 stuart
|
||||
# mlreply support
|
||||
#
|
||||
# Revision 2.28 2004/08/21 02:45:21 stuart
|
||||
# Don't leak int constants if module unloaded.
|
||||
#
|
||||
# Revision 2.27 2004/04/06 03:19:59 stuart
|
||||
# Release 0.6.8
|
||||
#
|
||||
# Revision 2.26 2004/03/04 21:43:06 stuart
|
||||
# Fix memory leak by removing unused dynamic template buffer,
|
||||
# thanks again to Alexander Kourakos.
|
||||
#
|
||||
# Revision 2.25 2004/03/01 19:45:03 stuart
|
||||
# Release 0.6.5
|
||||
#
|
||||
# Revision 2.24 2004/03/01 18:56:50 stuart
|
||||
# Support progress reporting.
|
||||
#
|
||||
# Revision 2.23 2004/03/01 18:36:09 stuart
|
||||
# Plug memory leak. Thanks to Alexander Kourakos.
|
||||
#
|
||||
# Revision 2.22 2003/11/02 03:01:46 stuart
|
||||
# Adjust SMTP error codes after careful reading of standard.
|
||||
#
|
||||
# Revision 2.21 2003/06/24 19:57:04 stuart
|
||||
# Allow removing a python milter callback by setting to None.
|
||||
#
|
||||
# Revision 2.20 2003/02/13 17:08:57 stuart
|
||||
# IPV6 support
|
||||
#
|
||||
# Revision 2.19 2003/02/13 16:58:29 stuart
|
||||
# Support passing None to setreply and chgheader.
|
||||
#
|
||||
# Revision 2.18 2002/12/11 16:44:06 stuart
|
||||
# Support QUARANTINE if supported by libmilter.
|
||||
#
|
||||
# Revision 2.17 2002/04/18 20:20:35 stuart
|
||||
# Fix for NULL hostaddr in connect callback from Jason Erickson.
|
||||
#
|
||||
# Revision 2.16 2001/09/26 13:29:09 stuart
|
||||
# sa_len not supported by linux.
|
||||
#
|
||||
# Revision 2.15 2001/09/25 17:28:40 stuart
|
||||
# Copyrights, documentation, release 0.3.1
|
||||
#
|
||||
# Revision 2.14 2001/09/25 00:36:57 stuart
|
||||
# Pass hostaddr to python code in format used by standard socket module.
|
||||
#
|
||||
# Revision 2.13 2001/09/24 23:44:55 stuart
|
||||
# Return old callback from setcallback functions.
|
||||
#
|
||||
# Revision 2.12 2001/09/24 20:02:30 stuart
|
||||
# Remove redundant setpriv
|
||||
#
|
||||
# Revision 2.11 2001/09/23 22:26:35 stuart
|
||||
# Update docs. Streamline Milter.py
|
||||
# update testbms.py to reflect actual sendmail behaviour with multiple
|
||||
# messages per connection.
|
||||
#
|
||||
# Revision 2.10 2001/09/22 15:33:42 stuart
|
||||
# More doc comment updates.
|
||||
#
|
||||
# Revision 2.9 2001/09/22 14:52:27 stuart
|
||||
# Actually return retval in _generic_return.
|
||||
# Go over doc comments.
|
||||
#
|
||||
# Revision 2.8 2001/09/22 01:59:32 stuart
|
||||
# Prevent reentrant call of milter_main, which libmilter doesn't support.
|
||||
#
|
||||
# Revision 2.7 2001/09/22 01:47:37 stuart
|
||||
# Forgot to set milter interp.
|
||||
#
|
||||
# Revision 2.6 2001/09/22 01:23:53 stuart
|
||||
# Added proper threading after research in python docs.
|
||||
#
|
||||
# Revision 2.5 2001/09/21 20:08:51 stuart
|
||||
# Release 0.2.3
|
||||
#
|
||||
# Revision 2.4 2001/09/20 16:18:16 stuart
|
||||
# libmilter checks in_eom state, so we don't have to.
|
||||
#
|
||||
# Revision 2.3 2001/09/19 06:02:33 stuart
|
||||
# Make more stuff static.
|
||||
#
|
||||
# Revision 2.1 2001/09/19 04:24:13 stuart
|
||||
# Use extension type to track context in python.
|
||||
#
|
||||
# Revision 1.4 2001/09/18 18:48:28 stuart
|
||||
# clear private data reference in _clear_context
|
||||
#
|
||||
# Revision 1.3 2001/09/15 04:19:37 stuart
|
||||
# nasty off by 1 mem overwrite bugs in wrap_env
|
||||
# generic_set_callback
|
||||
#
|
||||
# Revision 1.2 2001/09/15 03:15:39 stuart
|
||||
# several bugs fixed, works smoothly
|
||||
#
|
||||
# Revision 1.69 2006/11/04 22:09:39 customdesigned
|
||||
# Another lame DSN heuristic. Block PTR cache poisoning attack.
|
||||
#
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Doxyfile 1.6.1
|
||||
# Doxyfile 1.5.7.1
|
||||
|
||||
# This file describes the settings to be used by the documentation system
|
||||
# doxygen (www.doxygen.org) for a project
|
||||
@@ -31,7 +31,7 @@ PROJECT_NAME = pymilter
|
||||
# This could be handy for archiving the generated documentation or
|
||||
# if some version control system is used.
|
||||
|
||||
PROJECT_NUMBER = 1.0.5
|
||||
PROJECT_NUMBER = 0.9.6
|
||||
|
||||
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
|
||||
# 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.
|
||||
# The default language is English, other supported languages are:
|
||||
# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional,
|
||||
# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German,
|
||||
# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English
|
||||
# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian,
|
||||
# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak,
|
||||
# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese.
|
||||
# Croatian, Czech, Danish, Dutch, Farsi, Finnish, French, German, Greek,
|
||||
# Hungarian, Italian, Japanese, Japanese-en (Japanese with English messages),
|
||||
# Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, Polish,
|
||||
# Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, Slovene,
|
||||
# Spanish, Swedish, and Ukrainian.
|
||||
|
||||
OUTPUT_LANGUAGE = English
|
||||
|
||||
@@ -207,17 +207,6 @@ OPTIMIZE_FOR_FORTRAN = 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
|
||||
# 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
|
||||
@@ -405,10 +394,6 @@ SORT_MEMBER_DOCS = YES
|
||||
|
||||
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
|
||||
# hierarchy of group names into alphabetical order. If set to NO (the default)
|
||||
# the group names will appear in their defined order.
|
||||
@@ -483,8 +468,7 @@ SHOW_DIRECTORIES = YES
|
||||
SHOW_FILES = YES
|
||||
|
||||
# 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.
|
||||
|
||||
SHOW_NAMESPACES = YES
|
||||
@@ -527,7 +511,7 @@ WARNINGS = YES
|
||||
# for undocumented members. If EXTRACT_ALL is set to YES then this flag will
|
||||
# automatically be disabled.
|
||||
|
||||
WARN_IF_UNDOCUMENTED = NO
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
|
||||
# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for
|
||||
# potential errors in the documentation, such as not documenting some
|
||||
@@ -568,10 +552,7 @@ WARN_LOGFILE =
|
||||
# directories like "/usr/src/myproject". Separate the files or directories
|
||||
# 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
|
||||
# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
|
||||
@@ -655,17 +636,14 @@ IMAGE_PATH =
|
||||
# by executing (via popen()) the command <filter> <input-file>, where <filter>
|
||||
# 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
|
||||
# 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.
|
||||
|
||||
INPUT_FILTER =
|
||||
|
||||
# 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
|
||||
# filter if there is a match.
|
||||
# The filters are a list of the form:
|
||||
# basis. 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
|
||||
# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER
|
||||
# is applied to all files.
|
||||
@@ -715,8 +693,7 @@ REFERENCES_RELATION = YES
|
||||
# If the REFERENCES_LINK_SOURCE tag is set to YES (the default)
|
||||
# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
|
||||
# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
|
||||
# link to the source code.
|
||||
# Otherwise they will link to the documentation.
|
||||
# link to the source code. Otherwise they will link to the documentstion.
|
||||
|
||||
REFERENCES_LINK_SOURCE = YES
|
||||
|
||||
@@ -790,11 +767,6 @@ HTML_HEADER =
|
||||
|
||||
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
|
||||
# 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
|
||||
@@ -903,37 +875,20 @@ QCH_FILE =
|
||||
|
||||
# The QHP_NAMESPACE tag specifies the namespace to use when generating
|
||||
# Qt Help Project output. For more information please see
|
||||
# http://doc.trolltech.com/qthelpproject.html#namespace
|
||||
# <a href="http://doc.trolltech.com/qthelpproject.html#namespace">Qt Help Project / Namespace</a>.
|
||||
|
||||
QHP_NAMESPACE = org.doxygen.Project
|
||||
|
||||
# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating
|
||||
# Qt Help Project output. For more information please see
|
||||
# http://doc.trolltech.com/qthelpproject.html#virtual-folders
|
||||
# <a href="http://doc.trolltech.com/qthelpproject.html#virtual-folders">Qt Help Project / Virtual Folders</a>.
|
||||
|
||||
QHP_VIRTUAL_FOLDER = doc
|
||||
|
||||
# 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
|
||||
# be used to specify the location of Qt's qhelpgenerator.
|
||||
# If non-empty doxygen will try to run qhelpgenerator on the generated
|
||||
# .qhp file.
|
||||
# .qhp file .
|
||||
|
||||
QHG_LOCATION =
|
||||
|
||||
@@ -950,19 +905,21 @@ ENUM_VALUES_PER_LINE = 4
|
||||
|
||||
# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
|
||||
# structure should be generated to display hierarchical information.
|
||||
# If the tag value is set to YES, a side panel will be generated
|
||||
# If the tag value is set to FRAME, a side panel will be generated
|
||||
# containing a tree-like index structure (just like the one that
|
||||
# is generated for HTML Help). For this to work a browser that supports
|
||||
# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser).
|
||||
# Windows users are probably better off using the HTML help feature.
|
||||
# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+,
|
||||
# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are
|
||||
# probably better off using the HTML help feature. Other possible values
|
||||
# for this tag are: HIERARCHIES, which will generate the Groups, Directories,
|
||||
# and Class Hierarchy pages using a tree view instead of an ordered list;
|
||||
# ALL, which combines the behavior of FRAME and HIERARCHIES; and NONE, which
|
||||
# disables this behavior completely. For backwards compatibility with previous
|
||||
# releases of Doxygen, the values YES and NO are equivalent to FRAME and NONE
|
||||
# respectively.
|
||||
|
||||
GENERATE_TREEVIEW = NO
|
||||
|
||||
# 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
|
||||
# used to set the initial width (in pixels) of the frame in which the tree
|
||||
# is shown.
|
||||
@@ -977,13 +934,6 @@ TREEVIEW_WIDTH = 250
|
||||
|
||||
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
|
||||
#---------------------------------------------------------------------------
|
||||
@@ -1060,10 +1010,6 @@ LATEX_BATCHMODE = 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
|
||||
#---------------------------------------------------------------------------
|
||||
@@ -1200,10 +1146,8 @@ GENERATE_PERLMOD = NO
|
||||
PERLMOD_LATEX = NO
|
||||
|
||||
# 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
|
||||
# if you want to understand what is going on.
|
||||
# On the other hand, if this
|
||||
# nicely formatted so it can be parsed by a human reader. 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
|
||||
# and Perl will parse it just the same.
|
||||
|
||||
@@ -1290,10 +1234,8 @@ SKIP_FUNCTION_MACROS = YES
|
||||
# Optionally an initial location of the external documentation
|
||||
# can be added for each tagfile. The format of a tag file without
|
||||
# this location is as follows:
|
||||
#
|
||||
# TAGFILES = file1 file2 ...
|
||||
# Adding location for the tag files is done as follows:
|
||||
#
|
||||
# TAGFILES = file1=loc1 "file2 = loc2" ...
|
||||
# where "loc1" and "loc2" can be relative or absolute paths or
|
||||
# URLs. If a location is present for each tag, the installdox tool
|
||||
@@ -1520,3 +1462,12 @@ GENERATE_LEGEND = YES
|
||||
# the various graphs.
|
||||
|
||||
DOT_CLEANUP = YES
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration::additions related to the search engine
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
# The SEARCHENGINE tag specifies whether or not a search engine should be
|
||||
# used. If set to NO the values of all tags below this one will be ignored.
|
||||
|
||||
SEARCHENGINE = NO
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ include COPYING
|
||||
include TODO
|
||||
include NEWS
|
||||
include CREDITS
|
||||
include README.md
|
||||
include README
|
||||
include ChangeLog
|
||||
include MANIFEST.in
|
||||
include testsample.py
|
||||
|
||||
+68
-189
@@ -8,18 +8,12 @@
|
||||
# Copyright 2001,2009 Business Management Systems, Inc.
|
||||
# This code is under the GNU General Public License. See COPYING for details.
|
||||
|
||||
from __future__ import print_function
|
||||
__version__ = '1.0.6'
|
||||
__version__ = '0.9.7'
|
||||
|
||||
import os
|
||||
import re
|
||||
import milter
|
||||
import sys
|
||||
try:
|
||||
import thread
|
||||
except:
|
||||
# libmilter uses posix threads
|
||||
import _thread as thread
|
||||
import thread
|
||||
|
||||
from milter import *
|
||||
from functools import wraps
|
||||
@@ -27,6 +21,12 @@ from functools import wraps
|
||||
_seq_lock = thread.allocate_lock()
|
||||
_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():
|
||||
"""Return a unique sequence number (incremented on each call).
|
||||
"""
|
||||
@@ -49,12 +49,6 @@ OPTIONAL_CALLBACKS = {
|
||||
'header':(P_NR_HDR,P_NOHDRS)
|
||||
}
|
||||
|
||||
MACRO_CALLBACKS = {
|
||||
'connect': M_CONNECT,
|
||||
'hello': M_HELO, 'envfrom': M_ENVFROM, 'envrcpt': M_ENVRCPT,
|
||||
'data': M_DATA, 'eom': M_EOM, 'eoh': M_EOH
|
||||
}
|
||||
|
||||
## @private
|
||||
R = re.compile(r'%+')
|
||||
|
||||
@@ -113,7 +107,7 @@ def rejected_recipients(klass):
|
||||
return enable_protocols(klass,P_RCPT_REJ)
|
||||
|
||||
## Milter leading space on headers. A class decorator that calls
|
||||
# enable_protocols() with the P_HDR_LEADSPC flag. By default,
|
||||
# enable_protocols() with the P_HEAD_LEADSPC flag. By default,
|
||||
# header continuation lines are collected and joined before getting
|
||||
# sent to a milter. Headers modified or added by the milter are
|
||||
# folded by the MTA as necessary according to its own standards.
|
||||
@@ -131,7 +125,7 @@ def rejected_recipients(klass):
|
||||
# @param klass the %milter application class to modify
|
||||
# @return the modified %milter class
|
||||
def header_leading_space(klass):
|
||||
return enable_protocols(klass,P_HDR_LEADSPC)
|
||||
return enable_protocols(klass,P_HEAD_LEADSPC)
|
||||
|
||||
## Function decorator to disable callback methods.
|
||||
# If the MTA supports it, tells the MTA not to invoke this callback,
|
||||
@@ -148,7 +142,6 @@ def nocallback(func):
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
'@nocallback applied to non-optional method: '+func.__name__)
|
||||
@wraps(func)
|
||||
def wrapper(self,*args):
|
||||
if func(self,*args) != CONTINUE:
|
||||
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
|
||||
@@ -181,47 +174,6 @@ def noreply(func):
|
||||
wrapper.milter_protocol = nr_mask
|
||||
return wrapper
|
||||
|
||||
## Function decorator to set decoding error strategy.
|
||||
# Current RFCs define UTF-8 as the standard encoding for SMTP
|
||||
# envelope and header fields. By default, Milter.Base decodes
|
||||
# envelope and header values with errors='surrogateescape'.
|
||||
# Applications can recover the original bytes with
|
||||
# <pre>
|
||||
# b = s.encode(errors='surrogateescape')
|
||||
# </pre>
|
||||
# This preserves information, but can lead to unexpected exceptions
|
||||
# as you cannot, e.g. print strings with surrogates.
|
||||
# Illegal bytes occur quite often in real life, so there must
|
||||
# be a way to deal with them.
|
||||
# This decorator can change the error strategy to
|
||||
# <ul>
|
||||
# <li> bytes - original bytes are passed unmodified
|
||||
# <li> strict - pass bytes if illegal bytes are present, string otherwise
|
||||
# <li> ignore - illegal bytes are removed
|
||||
# <li> replace - illegal bytes are replaced with a unicode error symbol
|
||||
# </ul>
|
||||
#
|
||||
def decode(strategy):
|
||||
def setstrategy(func):
|
||||
func.error_strategy = strategy
|
||||
return func
|
||||
return setstrategy
|
||||
|
||||
## Function decorator to set macros used in a callback.
|
||||
# By default, the MTA sends all macros defined for a callback.
|
||||
# If some or all of these are unused, the bandwidth can be saved
|
||||
# by listing the ones that are used.
|
||||
# @since 1.0.2
|
||||
def symlist(*syms):
|
||||
def setsyms(func):
|
||||
if len(syms) > 5:
|
||||
raise ValueError('@symlist limited to 5 macros by MTA: '+func.__name__)
|
||||
if func.__name__ not in MACRO_CALLBACKS:
|
||||
raise ValueError('@symlist applied to non-symlist method: '+func.__name__)
|
||||
func._symlist = syms
|
||||
return func
|
||||
return setsyms
|
||||
|
||||
## Disabled action exception.
|
||||
# set_flags() can tell the MTA that this application will not use certain
|
||||
# features (such as CHGFROM). This can also be negotiated for each
|
||||
@@ -275,7 +227,7 @@ class Base(object):
|
||||
# Some optional actions may be disabled by calling milter.set_flags(), or
|
||||
# by overriding the negotiate callback. The bits include:
|
||||
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
|
||||
# CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</code>.
|
||||
# CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST</code>.
|
||||
# The <code>Milter.CURR_ACTS</code> bitmask is all actions
|
||||
# known when the milter module was compiled.
|
||||
# Application code can also inspect this field to determine
|
||||
@@ -311,7 +263,7 @@ class Base(object):
|
||||
## Defined by subclasses to write log messages.
|
||||
def log(self,*msg): pass
|
||||
## Called for each connection to the MTA. Called by the
|
||||
# <a href="milter_api/xxfi_connect.html">
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_connect">
|
||||
# xxfi_connect</a> callback.
|
||||
# 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.
|
||||
@@ -347,22 +299,8 @@ class Base(object):
|
||||
# this almost always results in terminating the connection.
|
||||
@nocallback
|
||||
def hello(self,hostname): return CONTINUE
|
||||
## Called with bytes by default global envfrom callback.
|
||||
# @since 1.0.5
|
||||
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
|
||||
# to pass bytes to @link #header the header callback @endlink instead,
|
||||
# or trap utf-8 conversion exception, etc.
|
||||
def envfrom_bytes(self,*b):
|
||||
try:
|
||||
e = getattr(self.envfrom,'error_strategy','surrogateescape')
|
||||
if e == 'bytes':
|
||||
#self.envfrom_bytes = self.envfrom
|
||||
return self.envfrom(*b)
|
||||
s = [v.decode(encoding='utf-8',errors=e) for v in b]
|
||||
except UnicodeDecodeError: s = b
|
||||
return self.envfrom(s[0],*s[1:])
|
||||
## Called when the SMTP client says MAIL FROM. Called by the
|
||||
# <a href="milter_api/xxfi_envfrom.html">
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_envfrom">
|
||||
# xxfi_envfrom</a> callback.
|
||||
# Returning REJECT rejects the message, but not the connection.
|
||||
# The sender is the "envelope" from as defined by
|
||||
@@ -371,23 +309,9 @@ class Base(object):
|
||||
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
||||
# see @link #header the header callback @endlink.
|
||||
@nocallback
|
||||
def envfrom(self,f,*s): return CONTINUE
|
||||
## Called with bytes by default global envrcpt callback.
|
||||
# @since 1.0.5
|
||||
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
|
||||
# to pass bytes to @link #header the header callback @endlink instead,
|
||||
# or trap utf-8 conversion exception, etc.
|
||||
def envrcpt_bytes(self,*b):
|
||||
try:
|
||||
e = getattr(self.envrcpt,'error_strategy','surrogateescape')
|
||||
if e == 'bytes':
|
||||
#self.envrcpt_bytes = self.envrcpt
|
||||
return self.envrcpt(*b)
|
||||
s = [v.decode(encoding='utf-8',errors=e) for v in b]
|
||||
except UnicodeDecodeError: s = b
|
||||
return self.envrcpt(s[0],*s[1:])
|
||||
def envfrom(self,f,*str): return CONTINUE
|
||||
## Called when the SMTP client says RCPT TO. Called by the
|
||||
# <a href="milter_api/xxfi_envrcpt.html">
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
|
||||
# xxfi_envrcpt</a> callback.
|
||||
# Returning REJECT rejects the current recipient, not the entire message.
|
||||
# The recipient is the "envelope" recipient as defined by
|
||||
@@ -403,30 +327,7 @@ class Base(object):
|
||||
# @since 0.9.2
|
||||
@nocallback
|
||||
def data(self): return CONTINUE
|
||||
## Called with bytes by default global header callback.
|
||||
# @param fld name decoded as ascii
|
||||
# @param val field value as bytes
|
||||
# @since 1.0.5
|
||||
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
|
||||
# to pass bytes to @link #header the header callback @endlink instead,
|
||||
# e.g. by assignment:
|
||||
# <pre>
|
||||
# mymilter.header_bytes = mymilter.header
|
||||
# </pre>
|
||||
# The <code>@decode('bytes')</code> decorator will also do this.
|
||||
#
|
||||
def header_bytes(self,fld,val):
|
||||
try:
|
||||
e = getattr(self.header,'error_strategy','surrogateescape')
|
||||
if e == 'bytes':
|
||||
self.header_bytes = self.header
|
||||
return self.header(fld,val)
|
||||
s = val.decode(encoding='utf-8',errors=e)
|
||||
except UnicodeDecodeError: s = val
|
||||
return self.header(fld,s)
|
||||
## Called for each header field in the message body.
|
||||
# @param field name decoded as ascii
|
||||
# @param value field value decoded as utf-8 on python3
|
||||
@nocallback
|
||||
def header(self,field,value): return CONTINUE
|
||||
## Called at the blank line that terminates the header fields.
|
||||
@@ -470,13 +371,13 @@ class Base(object):
|
||||
for func,(nr,nc) in OPTIONAL_CALLBACKS.items():
|
||||
func = getattr(klass,func)
|
||||
ca = getattr(func,'milter_protocol',0)
|
||||
#print(func,hex(nr),hex(nc),hex(ca))
|
||||
#print func,hex(nr),hex(nc),hex(ca)
|
||||
p |= (nr|nc) & ~ca
|
||||
klass._protocol_mask = p
|
||||
return p
|
||||
|
||||
## Negotiate milter protocol options. Called by the
|
||||
# <a href="milter_api/xxfi_negotiate.html">
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
|
||||
# xffi_negotiate</a> callback. This is an advanced callback,
|
||||
# do not override unless you know what you are doing. Most
|
||||
# negotiation can be done simply by using the supplied
|
||||
@@ -493,16 +394,11 @@ class Base(object):
|
||||
def negotiate(self,opts):
|
||||
try:
|
||||
self._actions,p,f1,f2 = opts
|
||||
for func,stage in MACRO_CALLBACKS.items():
|
||||
func = getattr(self,func)
|
||||
syms = getattr(func,'_symlist',None)
|
||||
if syms is not None:
|
||||
self.setsymlist(stage,*syms)
|
||||
opts[1] = self._protocol = p & ~self.protocol_mask()
|
||||
opts[2] = 0
|
||||
opts[3] = 0
|
||||
#self.log("Negotiated:",opts)
|
||||
except Exception as x:
|
||||
except:
|
||||
# don't change anything if something went wrong
|
||||
return ALL_OPTS
|
||||
return CONTINUE
|
||||
@@ -512,7 +408,7 @@ class Base(object):
|
||||
## Return the value of an MTA macro. Sendmail macro names
|
||||
# are either single chars (e.g. "j") or multiple chars enclosed
|
||||
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
|
||||
# See <a href="milter_api/smfi_getsymval.html">
|
||||
# See <a href="https://www.milter.org/developers/api/smfi_getsymval">
|
||||
# smfi_getsymval</a> for default sendmail macros.
|
||||
# @param sym the macro name
|
||||
def getsymval(self,sym):
|
||||
@@ -520,21 +416,12 @@ class Base(object):
|
||||
|
||||
## Set the SMTP reply code and message.
|
||||
# If the MTA does not support setmlreply, then only the
|
||||
# first msg line is used. Any '%%' in a message line
|
||||
# first msg line is used. Any '%' in a message line
|
||||
# must be doubled, or libmilter will silently ignore the setreply.
|
||||
# Beginning with 0.9.6, we test for that case and throw ValueError to avoid
|
||||
# head scratching. What will <i>really</i> irritate you, however,
|
||||
# is that if you carefully double any '%%', your message will be
|
||||
# sent - but with the '%%' still doubled!
|
||||
# See <a href="milter_api/smfi_setreply.html">
|
||||
# smfi_setreply</a> for more information.
|
||||
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
|
||||
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
|
||||
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
|
||||
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
|
||||
# @param msg The text part of the SMTP reply. If msg is None,
|
||||
# an empty message is used.
|
||||
# @param ml Optional additional message lines.
|
||||
# is that if you carefully double any '%', your message will be
|
||||
# sent - but with the '%' still doubled!
|
||||
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)]:
|
||||
@@ -542,38 +429,22 @@ class Base(object):
|
||||
return self._ctx.setreply(rcode,xcode,msg,*ml)
|
||||
|
||||
## Tell the MTA which macro names will be used.
|
||||
# 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.
|
||||
# The <code>Milter.SETSMLIST</code> action flag must be set.
|
||||
#
|
||||
# May only be called from negotiate callback. Hence, this is an advanced
|
||||
# feature. Use the @@symlist function decorator to conviently set
|
||||
# the macros used by a callback.
|
||||
# @since 0.9.8, previous version was misspelled!
|
||||
# @param stage the protocol stage to set to macro list for,
|
||||
# one of the M_* constants defined in Milter
|
||||
# @param macros space separated and/or lists of strings
|
||||
def setsymlist(self,stage,*macros):
|
||||
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
|
||||
if len(macros) > 5:
|
||||
raise ValueError('setsymlist limited to 5 macros by MTA')
|
||||
a = []
|
||||
for m in macros:
|
||||
try:
|
||||
m = m.encode('utf8')
|
||||
except: pass
|
||||
try:
|
||||
m = m.split(b' ')
|
||||
a += m
|
||||
except: pass
|
||||
return self._ctx.setsymlist(stage,b' '.join(a))
|
||||
# May only be called from negotiate callback.
|
||||
# @since 0.9.2
|
||||
# @param stage the protocol stage to set to macro list for
|
||||
# @param macros a string with a space delimited list of macros
|
||||
def setsmlist(self,stage,macros):
|
||||
if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST")
|
||||
if type(macros) in (list,tuple):
|
||||
macros = ' '.join(macros)
|
||||
return self._ctx.setsmlist(stage,macros)
|
||||
|
||||
# Milter methods which can only be called from eom callback.
|
||||
|
||||
## Add a mail header field.
|
||||
# Calls <a href="milter_api/smfi_addheader.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_addheader">
|
||||
# smfi_addheader</a>.
|
||||
# The <code>Milter.ADDHDRS</code> action flag must be set.
|
||||
#
|
||||
@@ -587,7 +458,7 @@ class Base(object):
|
||||
return self._ctx.addheader(field,value,idx)
|
||||
|
||||
## Change the value of a mail header field.
|
||||
# Calls <a href="milter_api/smfi_chgheader.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">
|
||||
# smfi_chgheader</a>.
|
||||
# The <code>Milter.CHGHDRS</code> action flag must be set.
|
||||
#
|
||||
@@ -601,7 +472,7 @@ class Base(object):
|
||||
return self._ctx.chgheader(field,idx,value)
|
||||
|
||||
## Add a recipient to the message.
|
||||
# Calls <a href="milter_api/smfi_addrcpt.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">
|
||||
# smfi_addrcpt</a>.
|
||||
# If no corresponding mail header is added, this is like a Bcc.
|
||||
# The syntax of the recipient is the same as used in the SMTP
|
||||
@@ -621,7 +492,7 @@ class Base(object):
|
||||
raise DisabledAction("ADDRCPT_PAR")
|
||||
return self._ctx.addrcpt(rcpt,params)
|
||||
## Delete a recipient from the message.
|
||||
# Calls <a href="milter_api/smfi_delrcpt.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">
|
||||
# smfi_delrcpt</a>.
|
||||
# The recipient should match one passed to the envrcpt callback.
|
||||
# The <code>Milter.DELRCPT</code> action flag must be set.
|
||||
@@ -634,7 +505,7 @@ class Base(object):
|
||||
return self._ctx.delrcpt(rcpt)
|
||||
|
||||
## Replace the message body.
|
||||
# Calls <a href="milter_api/smfi_replacebody.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">
|
||||
# smfi_replacebody</a>.
|
||||
# The entire message body must be replaced.
|
||||
# Call repeatedly with blocks of data until the entire body is transferred.
|
||||
@@ -648,7 +519,7 @@ class Base(object):
|
||||
return self._ctx.replacebody(body)
|
||||
|
||||
## Change the SMTP envelope sender address.
|
||||
# Calls <a href="milter_api/smfi_chgfrom.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">
|
||||
# smfi_chgfrom</a>.
|
||||
# The syntax of the sender is that same as used in the SMTP
|
||||
# MAIL FROM command (and as delivered to the envfrom callback),
|
||||
@@ -665,7 +536,7 @@ class Base(object):
|
||||
return self._ctx.chgfrom(sender,params)
|
||||
|
||||
## Quarantine the message.
|
||||
# Calls <a href="milter_api/smfi_quarantine.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">
|
||||
# smfi_quarantine</a>.
|
||||
# When quarantined, a message goes into the mailq as if to be delivered,
|
||||
# but delivery is deferred until the message is unquarantined.
|
||||
@@ -679,7 +550,7 @@ class Base(object):
|
||||
return self._ctx.quarantine(reason)
|
||||
|
||||
## Tell the MTA to wait a bit longer.
|
||||
# Calls <a href="milter_api/smfi_progress.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_progress">
|
||||
# smfi_progress</a>.
|
||||
# Resets timeouts in the MTA that detect a "hung" milter.
|
||||
def progress(self):
|
||||
@@ -693,9 +564,9 @@ class Milter(Base):
|
||||
|
||||
## Provide simple logging to sys.stdout
|
||||
def log(self,*msg):
|
||||
print('Milter:',end=None)
|
||||
for i in msg: print(i,end=None)
|
||||
print()
|
||||
print 'Milter:',
|
||||
for i in msg: print i,
|
||||
print
|
||||
|
||||
@noreply
|
||||
def connect(self,hostname,family,hostaddr):
|
||||
@@ -832,7 +703,28 @@ def envcallback(c,args):
|
||||
# @param socketname the socket to be passed to milter.setconn()
|
||||
# @param timeout the time in secs the MTA should wait for a response before
|
||||
# considering this %milter dead
|
||||
def runmilter(name,socketname,timeout = 0,rmsock=True):
|
||||
def runmilter(name,socketname,timeout = 0):
|
||||
# This bit is here on the assumption that you will be starting this filter
|
||||
# before sendmail. If sendmail is not running and the socket already exists,
|
||||
# libmilter will throw a warning. If sendmail is running, this is still
|
||||
# safe if there are no messages currently being processed. It's safer to
|
||||
# shutdown sendmail, kill the filter process, restart the filter, and then
|
||||
# restart sendmail.
|
||||
pos = socketname.find(':')
|
||||
if pos > 1:
|
||||
s = socketname[:pos]
|
||||
fname = socketname[pos+1:]
|
||||
else:
|
||||
s = "unix"
|
||||
fname = socketname
|
||||
if s == "unix" or s == "local":
|
||||
print "Removing %s" % fname
|
||||
try:
|
||||
os.unlink(fname)
|
||||
except os.error, x:
|
||||
import errno
|
||||
if x.errno != errno.ENOENT:
|
||||
raise milter.error(x)
|
||||
|
||||
# The default flags set include everything
|
||||
# milter.set_flags(milter.ADDHDRS)
|
||||
@@ -842,14 +734,9 @@ def runmilter(name,socketname,timeout = 0,rmsock=True):
|
||||
# parms, but then all existing users would have to include **kw to accept
|
||||
# arbitrary keywords without crashing. We do provide envcallback and
|
||||
# dictfromlist to make parsing the ESMTP args convenient.
|
||||
if sys.version < '3.0.0':
|
||||
milter.set_envfrom_callback(lambda ctx,*s: ctx.getpriv().envfrom(*s))
|
||||
milter.set_envrcpt_callback(lambda ctx,*s: ctx.getpriv().envrcpt(*s))
|
||||
milter.set_header_callback(lambda ctx,f,v: ctx.getpriv().header(f,v))
|
||||
else:
|
||||
milter.set_envfrom_callback(lambda ctx,*b: ctx.getpriv().envfrom_bytes(*b))
|
||||
milter.set_envrcpt_callback(lambda ctx,*b: ctx.getpriv().envrcpt_bytes(*b))
|
||||
milter.set_header_callback(lambda ctx,f,v: ctx.getpriv().header_bytes(f,v))
|
||||
milter.set_envfrom_callback(lambda ctx,*str: ctx.getpriv().envfrom(*str))
|
||||
milter.set_envrcpt_callback(lambda ctx,*str: ctx.getpriv().envrcpt(*str))
|
||||
milter.set_header_callback(lambda ctx,fld,val: ctx.getpriv().header(fld,val))
|
||||
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
|
||||
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
|
||||
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
|
||||
@@ -868,14 +755,6 @@ def runmilter(name,socketname,timeout = 0,rmsock=True):
|
||||
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
|
||||
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
|
||||
try:
|
||||
milter.main()
|
||||
|
||||
+5
-11
@@ -10,9 +10,6 @@
|
||||
# CBV results.
|
||||
#
|
||||
# $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
|
||||
# Delete unparseable timestamps when loading address cache. These have
|
||||
# arisen because of failure to parse MAIL FROM properly. Will have to
|
||||
@@ -46,9 +43,8 @@
|
||||
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
|
||||
# This code is under the GNU General Public License. See COPYING for details.
|
||||
|
||||
from __future__ import print_function
|
||||
import time
|
||||
from Milter.plock import PLock
|
||||
from plock import PLock
|
||||
|
||||
class AddrCache(object):
|
||||
time_format = '%Y%b%d %H:%M:%S %Z'
|
||||
@@ -132,8 +128,8 @@ class AddrCache(object):
|
||||
if not ts or ts > too_old:
|
||||
return res
|
||||
del self.cache[lsender]
|
||||
raise KeyError(sender)
|
||||
except KeyError as x:
|
||||
raise KeyError, sender
|
||||
except KeyError,x:
|
||||
try:
|
||||
user,host = sender.split('@',1)
|
||||
return self.__getitem__(host)
|
||||
@@ -148,8 +144,7 @@ class AddrCache(object):
|
||||
if not ts: return # already permanent
|
||||
self.cache[lsender] = (None,res)
|
||||
if not res:
|
||||
with open(self.fname,'a') as fp:
|
||||
print(sender,file=fp)
|
||||
print >>open(self.fname,'a'),sender
|
||||
|
||||
def __setitem__(self,sender,res):
|
||||
lsender = sender.lower()
|
||||
@@ -157,8 +152,7 @@ class AddrCache(object):
|
||||
self.cache[lsender] = (now,res)
|
||||
if not res and self.fname:
|
||||
s = time.strftime(AddrCache.time_format,time.localtime(now))
|
||||
with open(self.fname,'a') as fp:
|
||||
print(sender,s,file=fp) # log refreshed senders
|
||||
print >>open(self.fname,'a'),sender,s # log refreshed senders
|
||||
|
||||
def __len__(self):
|
||||
return len(self.cache)
|
||||
|
||||
+9
-16
@@ -1,8 +1,4 @@
|
||||
try:
|
||||
from configparser import ConfigParser
|
||||
except:
|
||||
from ConfigParser import ConfigParser
|
||||
import os.path
|
||||
from ConfigParser import ConfigParser
|
||||
|
||||
class MilterConfigParser(ConfigParser):
|
||||
|
||||
@@ -14,17 +10,17 @@ class MilterConfigParser(ConfigParser):
|
||||
# which screws up iterating over all options in a section.
|
||||
# Worse, passing "defaults" with vars= overrides the config file!
|
||||
# So we roll our own defaults.
|
||||
def get(self,sect,opt,fallback=None,**kwds):
|
||||
if not self.has_option(sect,opt) and not fallback and opt in self.defaults:
|
||||
def get(self,sect,opt):
|
||||
if not self.has_option(sect,opt) and opt in self.defaults:
|
||||
return self.defaults[opt]
|
||||
return ConfigParser.get(self,sect,opt,fallback=fallback,**kwds)
|
||||
return ConfigParser.get(self,sect,opt)
|
||||
|
||||
def getlist(self,sect,opt):
|
||||
if self.has_option(sect,opt):
|
||||
return [q.strip() for q in self.get(sect,opt).split(',')]
|
||||
return []
|
||||
|
||||
def getaddrset(self,sect,opt,dir=''):
|
||||
def getaddrset(self,sect,opt):
|
||||
if not self.has_option(sect,opt):
|
||||
return {}
|
||||
s = self.get(sect,opt)
|
||||
@@ -33,15 +29,13 @@ class MilterConfigParser(ConfigParser):
|
||||
q = q.strip()
|
||||
if q.startswith('file:'):
|
||||
domain = q[5:].lower()
|
||||
fname = os.path.join(dir,domain)
|
||||
with open(fname,'r') as fp:
|
||||
d[domain] = d.setdefault(domain,[]) + fp.read().split()
|
||||
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
|
||||
else:
|
||||
user,domain = q.split('@')
|
||||
d.setdefault(domain.lower(),[]).append(user)
|
||||
return d
|
||||
|
||||
def getaddrdict(self,sect,opt,dir=''):
|
||||
def getaddrdict(self,sect,opt):
|
||||
if not self.has_option(sect,opt):
|
||||
return {}
|
||||
d = {}
|
||||
@@ -52,9 +46,8 @@ class MilterConfigParser(ConfigParser):
|
||||
for addr in l.split(','):
|
||||
addr = addr.strip()
|
||||
if addr.startswith('file:'):
|
||||
fname = os.path.join(dir,addr[5:])
|
||||
with open(fname,'r') as fp:
|
||||
for a in fp.read().split():
|
||||
fname = addr[5:]
|
||||
for a in open(fname,'r').read().split():
|
||||
d[a] = q
|
||||
else:
|
||||
d[addr] = q
|
||||
|
||||
+5
-7
@@ -1,7 +1,6 @@
|
||||
## @package Milter.dns
|
||||
# Provide a higher level interface to pydns.
|
||||
|
||||
from __future__ import print_function
|
||||
import DNS
|
||||
from DNS import DNSError
|
||||
|
||||
@@ -26,8 +25,8 @@ def DNSLookup(name, qtype):
|
||||
# A RR as dotted quad. For consistency, this driver should
|
||||
# return both as binary string.
|
||||
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
||||
except IOError as x:
|
||||
raise DNSError(str(x))
|
||||
except IOError, x:
|
||||
raise DNSError, str(x)
|
||||
|
||||
class Session(object):
|
||||
"""A Session object has a simple cache with no TTL that is valid
|
||||
@@ -74,7 +73,6 @@ class Session(object):
|
||||
if name.endswith('.'): name = name[:-1]
|
||||
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
|
||||
return [] # invalid DNS name (too long or empty)
|
||||
name = name.lower()
|
||||
result = self.cache.get( (name, qtype) )
|
||||
cname = None
|
||||
if result: return result
|
||||
@@ -98,7 +96,7 @@ class Session(object):
|
||||
#return result # if too many == NX_DOMAIN
|
||||
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
||||
cnames[name] = cname
|
||||
if cname.lower().rstrip('.') in cnames:
|
||||
if cname in cnames:
|
||||
raise DNSError('CNAME loop')
|
||||
result = self.dns(cname, qtype, cnames=cnames)
|
||||
if result:
|
||||
@@ -121,5 +119,5 @@ if __name__ == '__main__':
|
||||
import sys
|
||||
s = Session()
|
||||
for n,t in zip(*[iter(sys.argv[1:])]*2):
|
||||
print(n,t)
|
||||
print(s.dns(n,t))
|
||||
print n,t
|
||||
print s.dns(n,t)
|
||||
|
||||
+8
-15
@@ -5,9 +5,6 @@
|
||||
# Send DSNs, do call back verification,
|
||||
# and generate DSN messages from a template
|
||||
# $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
|
||||
# Release 0.9.4
|
||||
#
|
||||
@@ -69,16 +66,12 @@
|
||||
# a DSN or use a null MAIL FROM with an email address obtained from
|
||||
# anywhere else.
|
||||
#
|
||||
from __future__ import print_function
|
||||
import smtplib
|
||||
import socket
|
||||
try:
|
||||
from email.message import Message
|
||||
except:
|
||||
from email.Message import Message
|
||||
from email.Message import Message
|
||||
import Milter
|
||||
import Milter.dns as dns
|
||||
import time
|
||||
import dns
|
||||
|
||||
## Send DSN.
|
||||
# Try the published MX names in order, rejecting obviously bogus entries
|
||||
@@ -145,13 +138,13 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
|
||||
if badrcpts:
|
||||
return badrcpts
|
||||
return None # success
|
||||
except smtplib.SMTPRecipientsRefused as x:
|
||||
except smtplib.SMTPRecipientsRefused,x:
|
||||
if len(x.recipients) == 1:
|
||||
return x.recipients.values()[0] # permanent error
|
||||
return x.recipients
|
||||
except smtplib.SMTPSenderRefused as x:
|
||||
except smtplib.SMTPSenderRefused,x:
|
||||
return x.args[:2] # does not accept DSN
|
||||
except smtplib.SMTPDataError as x:
|
||||
except smtplib.SMTPDataError,x:
|
||||
return x.args # permanent error
|
||||
except smtplib.SMTPException:
|
||||
pass # any other error, try next MX
|
||||
@@ -234,6 +227,6 @@ Subject: Test
|
||||
Test DSN template
|
||||
"""
|
||||
)
|
||||
print(msg.as_string())
|
||||
# print(send_dsn(f,msg.as_string()))
|
||||
# print(send_dsn(q.s,'mail.example.com',msg.as_string()))
|
||||
print msg.as_string()
|
||||
# print send_dsn(f,msg.as_string())
|
||||
# print send_dsn(q.s,'mail.example.com',msg.as_string())
|
||||
|
||||
+4
-5
@@ -9,7 +9,6 @@
|
||||
# wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810)
|
||||
# cbl-sd-02-79.aster.com.do at ('200.88.62.79', 4153)
|
||||
|
||||
from __future__ import print_function
|
||||
import re
|
||||
|
||||
ip3 = re.compile('[0-9]{1,3}')
|
||||
@@ -54,11 +53,11 @@ def is_dynip(host,addr):
|
||||
if host.find(addr) >= 0: return True
|
||||
if addr.find(':') >= 0: return False # IP6
|
||||
a = addr.split('.')
|
||||
ia = list(map(int,a))
|
||||
ia = map(int,a)
|
||||
h = host
|
||||
m = ip3.findall(host)
|
||||
if m:
|
||||
g = list(map(int,m))[:4]
|
||||
g = map(int,m)[:4]
|
||||
ia3 = (ia[1:],ia[:3])
|
||||
if g[-3:] in ia3: return True
|
||||
if g[0] == ia[3] and g[1:3] == ia[:2]: return True
|
||||
@@ -92,6 +91,6 @@ if __name__ == '__main__':
|
||||
if ip in seen: continue
|
||||
seen.add(ip)
|
||||
if is_dynip(host,ip):
|
||||
print('%s\t%s DYN' % (ip,host))
|
||||
print '%s\t%s DYN' % (ip,host)
|
||||
else:
|
||||
print('%s\t%s' % (ip,host))
|
||||
print '%s\t%s' % (ip,host)
|
||||
|
||||
+9
-59
@@ -1,10 +1,6 @@
|
||||
from __future__ import print_function
|
||||
import time
|
||||
import shelve
|
||||
try:
|
||||
import thread
|
||||
except:
|
||||
import _thread as thread
|
||||
import thread
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
@@ -22,19 +18,13 @@ def quoteAddress(s):
|
||||
class Record(object):
|
||||
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
|
||||
|
||||
def __init__(self,timeinc=0):
|
||||
now = time.time() + timeinc
|
||||
def __init__(self):
|
||||
now = time.time()
|
||||
self.firstseen = now
|
||||
self.lastseen = now
|
||||
self.cnt = 0
|
||||
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):
|
||||
|
||||
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
||||
@@ -45,37 +35,7 @@ class Greylist(object):
|
||||
self.dbp = shelve.open(dbname,'c',protocol=2)
|
||||
self.lock = thread.allocate_lock()
|
||||
|
||||
def export_csv(self,fp,timeinc=0):
|
||||
"Export records to csv."
|
||||
import csv
|
||||
dbp = self.dbp
|
||||
w = csv.writer(fp)
|
||||
now = time.time() + timeinc
|
||||
for key, r in dbp.iteritems():
|
||||
if now > r.lastseen + self.greylist_retain: continue
|
||||
ip,sender,recipient = key.rsplit(':',2)
|
||||
w.writerow([ip,sender,recipient,r.firstseen,r.lastseen,r.cnt,r.umis])
|
||||
|
||||
def clean(self,timeinc=0):
|
||||
"Delete records past the retention limit."
|
||||
now = time.time() + timeinc
|
||||
cnt = 0
|
||||
dbp = self.dbp
|
||||
for key, r in dbp.iteritems():
|
||||
#print(key,r,time.ctime(now))
|
||||
if now > r.lastseen + self.greylist_retain:
|
||||
self.lock.acquire()
|
||||
try:
|
||||
r = dbp[key]
|
||||
now = time.time() + timeinc
|
||||
if now > r.lastseen + self.greylist_retain:
|
||||
del dbp[key]
|
||||
cnt += 1
|
||||
finally:
|
||||
self.lock.release()
|
||||
return cnt
|
||||
|
||||
def check(self,ip,sender,recipient,timeinc=0):
|
||||
def check(self,ip,sender,recipient):
|
||||
"Return number of allowed messages for greylist triple."
|
||||
sender = quoteAddress(sender)
|
||||
recipient = quoteAddress(recipient)
|
||||
@@ -85,15 +45,15 @@ class Greylist(object):
|
||||
dbp = self.dbp
|
||||
try:
|
||||
r = dbp[key]
|
||||
now = time.time() + timeinc
|
||||
now = time.time()
|
||||
if now > r.lastseen + self.greylist_retain:
|
||||
# expired
|
||||
log.debug('Expired greylist: %s',key)
|
||||
r = Record(timeinc)
|
||||
r = Record()
|
||||
elif now < r.firstseen + self.greylist_time + 5:
|
||||
# still greylisted
|
||||
log.debug('Early greylist: %s',key)
|
||||
#r = Record(timeinc)
|
||||
#r = Record()
|
||||
r.lastseen = now
|
||||
elif r.cnt or now < r.firstseen + self.greylist_expire:
|
||||
# in greylist window or active
|
||||
@@ -103,22 +63,12 @@ class Greylist(object):
|
||||
else:
|
||||
# passed greylist window
|
||||
log.debug('Late greylist: %s',key)
|
||||
r = Record(timeinc)
|
||||
r = Record()
|
||||
dbp[key] = r
|
||||
except:
|
||||
r = Record(timeinc)
|
||||
r = Record()
|
||||
dbp[key] = r
|
||||
dbp.sync()
|
||||
finally:
|
||||
self.lock.release()
|
||||
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()
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import time
|
||||
import logging
|
||||
import urllib
|
||||
import sqlite3
|
||||
try:
|
||||
import thread
|
||||
except:
|
||||
import _thread as thread
|
||||
from datetime import datetime
|
||||
|
||||
log = logging.getLogger('milter.greylist')
|
||||
|
||||
_db_lock = thread.allocate_lock()
|
||||
|
||||
class Greylist(object):
|
||||
|
||||
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
||||
self.ignoreLastByte = False
|
||||
self.greylist_time = grey_time * 60 # minutes
|
||||
self.greylist_expire = grey_expire * 3600 # hours
|
||||
self.greylist_retain = grey_retain * 24 * 3600 # days
|
||||
self.conn = sqlite3.connect(dbname)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
self.conn.execute('''create table greylist(
|
||||
ip text , sender text, recipient text,
|
||||
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
|
||||
primary key (ip,sender,recipient))''')
|
||||
except: pass
|
||||
|
||||
def import_csv(self,fp):
|
||||
import csv
|
||||
rdr = csv.reader(fp)
|
||||
cur = self.conn.execute('begin immediate')
|
||||
try:
|
||||
for r in rdr:
|
||||
cur.execute('''insert into
|
||||
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
|
||||
values(?,?,?,?,?,?,?)''', r)
|
||||
self.conn.commit()
|
||||
finally:
|
||||
cur.close();
|
||||
|
||||
def clean(self,timeinc=0):
|
||||
"Delete records past the retention limit."
|
||||
now = time.time() + timeinc - self.greylist_retain
|
||||
cur = self.conn.cursor()
|
||||
try:
|
||||
cur.execute('delete from greylist where lastseen < ?',(now,))
|
||||
cnt = cur.rowcount
|
||||
self.conn.commit()
|
||||
finally: cur.close()
|
||||
return cnt
|
||||
|
||||
def check(self,ip,sender,recipient,timeinc=0):
|
||||
"Return number of allowed messages for greylist triple."
|
||||
_db_lock.acquire()
|
||||
cur = self.conn.execute('begin immediate')
|
||||
try:
|
||||
cur.execute('''select firstseen,lastseen,cnt,umis from greylist where
|
||||
ip=? and sender=? and recipient=?''',(ip,sender,recipient))
|
||||
r = cur.fetchone()
|
||||
now = time.time() + timeinc
|
||||
cnt = 0
|
||||
if not r:
|
||||
cur.execute('''insert into
|
||||
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
|
||||
values(?,?,?,?,?,?,?)''', (ip,sender,recipient,now,now,0,None))
|
||||
elif now > r['lastseen'] + self.greylist_retain:
|
||||
# expired
|
||||
log.debug('Expired greylist: %s:%s:%s',ip,sender,recipient)
|
||||
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
||||
where ip=? and sender=? and recipient=?''',
|
||||
(now,now,0,None,ip,sender,recipient))
|
||||
elif now < r['firstseen'] + self.greylist_time + 5:
|
||||
# still greylisted
|
||||
log.debug('Early greylist: %s:%s:%s',ip,sender,recipient)
|
||||
#r = Record()
|
||||
cur.execute('''update greylist set lastseen=?
|
||||
where ip=? and sender=? and recipient=?''',
|
||||
(now,ip,sender,recipient))
|
||||
elif r['cnt'] or now < r['firstseen'] + self.greylist_expire:
|
||||
# in greylist window or active
|
||||
cnt = r['cnt'] + 1
|
||||
cur.execute('''update greylist set lastseen=?,cnt=?
|
||||
where ip=? and sender=? and recipient=?''',
|
||||
(now,cnt,ip,sender,recipient))
|
||||
log.debug('Active greylist(%d): %s:%s:%s',cnt,ip,sender,recipient)
|
||||
else:
|
||||
# passed greylist window
|
||||
log.debug('Late greylist: %s:%s:%s',ip,sender,recipient)
|
||||
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
||||
where ip=? and sender=? and recipient=?''',
|
||||
(now,now,0,None,ip,sender,recipient))
|
||||
self.conn.commit()
|
||||
finally:
|
||||
cur.close()
|
||||
_db_lock.release()
|
||||
return cnt
|
||||
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
g = Greylist(sys.argv[1])
|
||||
try:
|
||||
g.import_csv(sys.stdin)
|
||||
finally: g.close()
|
||||
+3
-3
@@ -11,7 +11,7 @@ class PLock(object):
|
||||
self.basename = basename
|
||||
self.fp = None
|
||||
|
||||
def lock(self,lockname=None,mode=0o660,strict_perms=False):
|
||||
def lock(self,lockname=None,mode=0660,strict_perms=False):
|
||||
"Start an update transaction. Return FILE to write new version."
|
||||
self.unlock()
|
||||
if not lockname:
|
||||
@@ -21,7 +21,7 @@ class PLock(object):
|
||||
st = os.stat(self.basename)
|
||||
mode |= st.st_mode
|
||||
except OSError: pass
|
||||
u = os.umask(0o2)
|
||||
u = os.umask(0002)
|
||||
try:
|
||||
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
|
||||
finally:
|
||||
@@ -46,7 +46,7 @@ class PLock(object):
|
||||
def commit(self,backname=None):
|
||||
"Commit update transaction with optional backup file."
|
||||
if not self.fp:
|
||||
raise IOError("File not locked")
|
||||
raise IOError,"File not locked"
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
if backname:
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
try:
|
||||
try:
|
||||
from berkeleydb import db
|
||||
except:
|
||||
from bsddb3 import db
|
||||
class DB(object):
|
||||
def open(self,fname,mode):
|
||||
if mode == 'r': flags = db.DB_RDONLY
|
||||
else: raise RuntimeException('unsupported mode')
|
||||
self.f = db.DB()
|
||||
self.f.open(fname,flags=flags)
|
||||
def __contains__(self,key):
|
||||
return not not self.f.get(key)
|
||||
def __getitem__(self,key):
|
||||
v = self.f.get(key)
|
||||
if not v: raise KeyError(key)
|
||||
return v
|
||||
def close(self):
|
||||
self.f.close()
|
||||
def dbmopen(fname,mode):
|
||||
f = DB()
|
||||
f.open(fname,mode)
|
||||
return f
|
||||
except ModuleNotFoundError: raise
|
||||
except:
|
||||
import anydbm as dbm
|
||||
dbmopen = dbm.open
|
||||
|
||||
class MTAPolicy(object):
|
||||
"Get SPF policy by result from sendmail style access file."
|
||||
def __init__(self,sender,conf,access_file=None):
|
||||
if not access_file:
|
||||
access_file = conf.access_file
|
||||
self.use_nulls = conf.access_file_nulls
|
||||
try:
|
||||
self.use_colon = conf.access_file_colon
|
||||
except:
|
||||
self.use_colon = True
|
||||
self.sender = sender
|
||||
self.domain = sender.split('@')[-1].lower()
|
||||
self.acf = None
|
||||
self.access_file = access_file
|
||||
|
||||
def close(self):
|
||||
if self.acf:
|
||||
self.acf.close()
|
||||
|
||||
def __enter__(self):
|
||||
self.acf = None
|
||||
if self.access_file:
|
||||
try:
|
||||
self.acf = dbmopen(self.access_file,'r')
|
||||
except:
|
||||
print('%s: Cannot open for reading'%self.access_file)
|
||||
raise
|
||||
return self
|
||||
def __exit__(self,t,v,b): self.close()
|
||||
|
||||
def getPolicy(self,pfx):
|
||||
acf = self.acf
|
||||
if not acf: return None
|
||||
if self.use_nulls: sfx = b'\x00'
|
||||
else: sfx = b''
|
||||
if self.use_colon:
|
||||
sep = b':'
|
||||
else:
|
||||
sep = b'!'
|
||||
pfx = pfx.encode() + sep
|
||||
try: # try with localpart@domain
|
||||
return acf[pfx + self.sender.encode() + sfx].rstrip(b'\x00').decode()
|
||||
except KeyError:
|
||||
try: # try with domain
|
||||
d = self.domain.encode()
|
||||
k = pfx + d + sfx
|
||||
while not k in acf and b'.' in d:
|
||||
# check partial domains
|
||||
d = b'.'.join(d.split(b'.')[1:])
|
||||
k = pfx + b'.' + d + sfx
|
||||
return acf[k].rstrip(b'\x00').decode()
|
||||
except KeyError:
|
||||
try: # try bare prefix
|
||||
return acf[pfx + sfx].rstrip(b'\x00').decode()
|
||||
except KeyError:
|
||||
try:
|
||||
return acf[pfx[:-1] + sfx].rstrip(b'\x00').decode()
|
||||
except KeyError:
|
||||
return None
|
||||
+3
-4
@@ -6,7 +6,6 @@ This module is free software, and you may redistribute it and/or modify
|
||||
it under the same terms as Python itself, so long as this copyright message
|
||||
and disclaimer are retained in their original form.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
import struct
|
||||
#from spf import RE_IP4
|
||||
import re
|
||||
@@ -81,11 +80,11 @@ def inet_pton(p):
|
||||
(0, 0, 0, 0, 0, 65535, 258, 772)
|
||||
|
||||
>>> try: inet_pton('::1.2.3.4.5')
|
||||
... except ValueError as x: print(x)
|
||||
... except ValueError,x: print x
|
||||
::1.2.3.4.5
|
||||
"""
|
||||
if p == '::':
|
||||
return b'\0'*16
|
||||
return '\0'*16
|
||||
s = p
|
||||
m = RE_IP4.search(s)
|
||||
try:
|
||||
@@ -115,4 +114,4 @@ def inet_pton(p):
|
||||
return struct.pack('!HHHHHHHH',
|
||||
*[int(s,16) for s in a[0].split(':')])
|
||||
except ValueError: pass
|
||||
raise ValueError(p)
|
||||
raise ValueError,p
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
"""A parser for SGML, using the derived class as a static DTD."""
|
||||
|
||||
# XXX This only supports those SGML features used by HTML.
|
||||
|
||||
# XXX There should be a way to distinguish between PCDATA (parsed
|
||||
# character data -- the normal case), RCDATA (replaceable character
|
||||
# data -- only char and entity references and end tags are special)
|
||||
# and CDATA (character data -- only end tags are special). RCDATA is
|
||||
# not supported at all.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
try:
|
||||
import _markupbase
|
||||
except:
|
||||
import markupbase as _markupbase
|
||||
import re
|
||||
|
||||
__all__ = ["SGMLParser", "SGMLParseError"]
|
||||
|
||||
# Regular expressions used for parsing
|
||||
|
||||
interesting = re.compile('[&<]')
|
||||
incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|'
|
||||
'<([a-zA-Z][^<>]*|'
|
||||
'/([a-zA-Z][^<>]*)?|'
|
||||
'![^<>]*)?')
|
||||
|
||||
entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
|
||||
charref = re.compile('&#([0-9]+)[^0-9]')
|
||||
|
||||
starttagopen = re.compile('<[>a-zA-Z]')
|
||||
shorttagopen = re.compile('<[a-zA-Z][-.a-zA-Z0-9]*/')
|
||||
shorttag = re.compile('<([a-zA-Z][-.a-zA-Z0-9]*)/([^/]*)/')
|
||||
piclose = re.compile('>')
|
||||
endbracket = re.compile('[<>]')
|
||||
tagfind = re.compile('[a-zA-Z][-_.a-zA-Z0-9]*')
|
||||
attrfind = re.compile(
|
||||
r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*'
|
||||
r'(\'[^\']*\'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?')
|
||||
|
||||
|
||||
class SGMLParseError(RuntimeError):
|
||||
"""Exception raised for all parse errors."""
|
||||
pass
|
||||
|
||||
|
||||
# SGML parser base class -- find tags and call handler functions.
|
||||
# Usage: p = SGMLParser(); p.feed(data); ...; p.close().
|
||||
# The dtd is defined by deriving a class which defines methods
|
||||
# with special names to handle tags: start_foo and end_foo to handle
|
||||
# <foo> and </foo>, respectively, or do_foo to handle <foo> by itself.
|
||||
# (Tags are converted to lower case for this purpose.) The data
|
||||
# between tags is passed to the parser by calling self.handle_data()
|
||||
# with some data as argument (the data may be split up in arbitrary
|
||||
# chunks). Entity references are passed by calling
|
||||
# self.handle_entityref() with the entity reference as argument.
|
||||
|
||||
class SGMLParser(_markupbase.ParserBase):
|
||||
# Definition of entities -- derived classes may override
|
||||
entity_or_charref = re.compile('&(?:'
|
||||
'([a-zA-Z][-.a-zA-Z0-9]*)|#([0-9]+)'
|
||||
')(;?)')
|
||||
|
||||
def __init__(self, verbose=0):
|
||||
"""Initialize and reset this instance."""
|
||||
self.verbose = verbose
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
"""Reset this instance. Loses all unprocessed data."""
|
||||
self.__starttag_text = None
|
||||
self.rawdata = ''
|
||||
self.stack = []
|
||||
self.lasttag = '???'
|
||||
self.nomoretags = 0
|
||||
self.literal = 0
|
||||
_markupbase.ParserBase.reset(self)
|
||||
|
||||
def setnomoretags(self):
|
||||
"""Enter literal mode (CDATA) till EOF.
|
||||
|
||||
Intended for derived classes only.
|
||||
"""
|
||||
self.nomoretags = self.literal = 1
|
||||
|
||||
def setliteral(self, *args):
|
||||
"""Enter literal mode (CDATA).
|
||||
|
||||
Intended for derived classes only.
|
||||
"""
|
||||
self.literal = 1
|
||||
|
||||
def feed(self, data):
|
||||
"""Feed some data to the parser.
|
||||
|
||||
Call this as often as you want, with as little or as much text
|
||||
as you want (may include '\n'). (This just saves the text,
|
||||
all the processing is done by goahead().)
|
||||
"""
|
||||
|
||||
self.rawdata = self.rawdata + data
|
||||
self.goahead(0)
|
||||
|
||||
def close(self):
|
||||
"""Handle the remaining data."""
|
||||
self.goahead(1)
|
||||
|
||||
def error(self, message):
|
||||
raise SGMLParseError(message)
|
||||
|
||||
# Internal -- handle data as far as reasonable. May leave state
|
||||
# and data to be processed by a subsequent call. If 'end' is
|
||||
# true, force handling all data as if followed by EOF marker.
|
||||
def goahead(self, end):
|
||||
rawdata = self.rawdata
|
||||
i = 0
|
||||
n = len(rawdata)
|
||||
while i < n:
|
||||
if self.nomoretags:
|
||||
self.handle_data(rawdata[i:n])
|
||||
i = n
|
||||
break
|
||||
match = interesting.search(rawdata, i)
|
||||
if match: j = match.start()
|
||||
else: j = n
|
||||
if i < j:
|
||||
self.handle_data(rawdata[i:j])
|
||||
i = j
|
||||
if i == n: break
|
||||
if rawdata[i] == '<':
|
||||
if starttagopen.match(rawdata, i):
|
||||
if self.literal:
|
||||
self.handle_data(rawdata[i])
|
||||
i = i+1
|
||||
continue
|
||||
k = self.parse_starttag(i)
|
||||
if k < 0: break
|
||||
i = k
|
||||
continue
|
||||
if rawdata.startswith("</", i):
|
||||
k = self.parse_endtag(i)
|
||||
if k < 0: break
|
||||
i = k
|
||||
self.literal = 0
|
||||
continue
|
||||
if self.literal:
|
||||
if n > (i + 1):
|
||||
self.handle_data("<")
|
||||
i = i+1
|
||||
else:
|
||||
# incomplete
|
||||
break
|
||||
continue
|
||||
if rawdata.startswith("<!--", i):
|
||||
# Strictly speaking, a comment is --.*--
|
||||
# within a declaration tag <!...>.
|
||||
# This should be removed,
|
||||
# and comments handled only in parse_declaration.
|
||||
k = self.parse_comment(i)
|
||||
if k < 0: break
|
||||
i = k
|
||||
continue
|
||||
if rawdata.startswith("<?", i):
|
||||
k = self.parse_pi(i)
|
||||
if k < 0: break
|
||||
i = i+k
|
||||
continue
|
||||
if rawdata.startswith("<!", i):
|
||||
# This is some sort of declaration; in "HTML as
|
||||
# deployed," this should only be the document type
|
||||
# declaration ("<!DOCTYPE html...>").
|
||||
k = self.parse_declaration(i)
|
||||
if k < 0: break
|
||||
i = k
|
||||
continue
|
||||
elif rawdata[i] == '&':
|
||||
if self.literal:
|
||||
self.handle_data(rawdata[i])
|
||||
i = i+1
|
||||
continue
|
||||
match = charref.match(rawdata, i)
|
||||
if match:
|
||||
name = match.group(1)
|
||||
self.handle_charref(name)
|
||||
i = match.end(0)
|
||||
if rawdata[i-1] != ';': i = i-1
|
||||
continue
|
||||
match = entityref.match(rawdata, i)
|
||||
if match:
|
||||
name = match.group(1)
|
||||
self.handle_entityref(name)
|
||||
i = match.end(0)
|
||||
if rawdata[i-1] != ';': i = i-1
|
||||
continue
|
||||
else:
|
||||
self.error('neither < nor & ??')
|
||||
# We get here only if incomplete matches but
|
||||
# nothing else
|
||||
match = incomplete.match(rawdata, i)
|
||||
if not match:
|
||||
self.handle_data(rawdata[i])
|
||||
i = i+1
|
||||
continue
|
||||
j = match.end(0)
|
||||
if j == n:
|
||||
break # Really incomplete
|
||||
self.handle_data(rawdata[i:j])
|
||||
i = j
|
||||
# end while
|
||||
if end and i < n:
|
||||
self.handle_data(rawdata[i:n])
|
||||
i = n
|
||||
self.rawdata = rawdata[i:]
|
||||
# XXX if end: check for empty stack
|
||||
|
||||
# Extensions for the DOCTYPE scanner:
|
||||
_decl_otherchars = '='
|
||||
|
||||
# Internal -- parse processing instr, return length or -1 if not terminated
|
||||
def parse_pi(self, i):
|
||||
rawdata = self.rawdata
|
||||
if rawdata[i:i+2] != '<?':
|
||||
self.error('unexpected call to parse_pi()')
|
||||
match = piclose.search(rawdata, i+2)
|
||||
if not match:
|
||||
return -1
|
||||
j = match.start(0)
|
||||
self.handle_pi(rawdata[i+2: j])
|
||||
j = match.end(0)
|
||||
return j-i
|
||||
|
||||
def get_starttag_text(self):
|
||||
return self.__starttag_text
|
||||
|
||||
# Internal -- handle starttag, return length or -1 if not terminated
|
||||
def parse_starttag(self, i):
|
||||
self.__starttag_text = None
|
||||
start_pos = i
|
||||
rawdata = self.rawdata
|
||||
if shorttagopen.match(rawdata, i):
|
||||
# SGML shorthand: <tag/data/ == <tag>data</tag>
|
||||
# XXX Can data contain &... (entity or char refs)?
|
||||
# XXX Can data contain < or > (tag characters)?
|
||||
# XXX Can there be whitespace before the first /?
|
||||
match = shorttag.match(rawdata, i)
|
||||
if not match:
|
||||
return -1
|
||||
tag, data = match.group(1, 2)
|
||||
self.__starttag_text = '<%s/' % tag
|
||||
tag = tag.lower()
|
||||
k = match.end(0)
|
||||
self.finish_shorttag(tag, data)
|
||||
self.__starttag_text = rawdata[start_pos:match.end(1) + 1]
|
||||
return k
|
||||
# XXX The following should skip matching quotes (' or ")
|
||||
# As a shortcut way to exit, this isn't so bad, but shouldn't
|
||||
# be used to locate the actual end of the start tag since the
|
||||
# < or > characters may be embedded in an attribute value.
|
||||
match = endbracket.search(rawdata, i+1)
|
||||
if not match:
|
||||
return -1
|
||||
j = match.start(0)
|
||||
# Now parse the data between i+1 and j into a tag and attrs
|
||||
attrs = []
|
||||
if rawdata[i:i+2] == '<>':
|
||||
# SGML shorthand: <> == <last open tag seen>
|
||||
k = j
|
||||
tag = self.lasttag
|
||||
else:
|
||||
match = tagfind.match(rawdata, i+1)
|
||||
if not match:
|
||||
self.error('unexpected call to parse_starttag')
|
||||
k = match.end(0)
|
||||
tag = rawdata[i+1:k].lower()
|
||||
self.lasttag = tag
|
||||
while k < j:
|
||||
match = attrfind.match(rawdata, k)
|
||||
if not match: break
|
||||
attrname, rest, attrvalue = match.group(1, 2, 3)
|
||||
if not rest:
|
||||
attrvalue = attrname
|
||||
else:
|
||||
if (attrvalue[:1] == "'" == attrvalue[-1:] or
|
||||
attrvalue[:1] == '"' == attrvalue[-1:]):
|
||||
# strip quotes
|
||||
attrvalue = attrvalue[1:-1]
|
||||
attrvalue = self.entity_or_charref.sub(
|
||||
self._convert_ref, attrvalue)
|
||||
attrs.append((attrname.lower(), attrvalue))
|
||||
k = match.end(0)
|
||||
if rawdata[j] == '>':
|
||||
j = j+1
|
||||
self.__starttag_text = rawdata[start_pos:j]
|
||||
self.finish_starttag(tag, attrs)
|
||||
return j
|
||||
|
||||
# Internal -- convert entity or character reference
|
||||
def _convert_ref(self, match):
|
||||
if match.group(2):
|
||||
return self.convert_charref(match.group(2)) or \
|
||||
'&#%s%s' % match.groups()[1:]
|
||||
elif match.group(3):
|
||||
return self.convert_entityref(match.group(1)) or \
|
||||
'&%s;' % match.group(1)
|
||||
else:
|
||||
return '&%s' % match.group(1)
|
||||
|
||||
# Internal -- parse endtag
|
||||
def parse_endtag(self, i):
|
||||
rawdata = self.rawdata
|
||||
match = endbracket.search(rawdata, i+1)
|
||||
if not match:
|
||||
return -1
|
||||
j = match.start(0)
|
||||
tag = rawdata[i+2:j].strip().lower()
|
||||
if rawdata[j] == '>':
|
||||
j = j+1
|
||||
self.finish_endtag(tag)
|
||||
return j
|
||||
|
||||
# Internal -- finish parsing of <tag/data/ (same as <tag>data</tag>)
|
||||
def finish_shorttag(self, tag, data):
|
||||
self.finish_starttag(tag, [])
|
||||
self.handle_data(data)
|
||||
self.finish_endtag(tag)
|
||||
|
||||
# Internal -- finish processing of start tag
|
||||
# Return -1 for unknown tag, 0 for open-only tag, 1 for balanced tag
|
||||
def finish_starttag(self, tag, attrs):
|
||||
try:
|
||||
method = getattr(self, 'start_' + tag)
|
||||
except AttributeError:
|
||||
try:
|
||||
method = getattr(self, 'do_' + tag)
|
||||
except AttributeError:
|
||||
self.unknown_starttag(tag, attrs)
|
||||
return -1
|
||||
else:
|
||||
self.handle_starttag(tag, method, attrs)
|
||||
return 0
|
||||
else:
|
||||
self.stack.append(tag)
|
||||
self.handle_starttag(tag, method, attrs)
|
||||
return 1
|
||||
|
||||
# Internal -- finish processing of end tag
|
||||
def finish_endtag(self, tag):
|
||||
if not tag:
|
||||
found = len(self.stack) - 1
|
||||
if found < 0:
|
||||
self.unknown_endtag(tag)
|
||||
return
|
||||
else:
|
||||
if tag not in self.stack:
|
||||
try:
|
||||
method = getattr(self, 'end_' + tag)
|
||||
except AttributeError:
|
||||
self.unknown_endtag(tag)
|
||||
else:
|
||||
self.report_unbalanced(tag)
|
||||
return
|
||||
found = len(self.stack)
|
||||
for i in range(found):
|
||||
if self.stack[i] == tag: found = i
|
||||
while len(self.stack) > found:
|
||||
tag = self.stack[-1]
|
||||
try:
|
||||
method = getattr(self, 'end_' + tag)
|
||||
except AttributeError:
|
||||
method = None
|
||||
if method:
|
||||
self.handle_endtag(tag, method)
|
||||
else:
|
||||
self.unknown_endtag(tag)
|
||||
del self.stack[-1]
|
||||
|
||||
# Overridable -- handle start tag
|
||||
def handle_starttag(self, tag, method, attrs):
|
||||
method(attrs)
|
||||
|
||||
# Overridable -- handle end tag
|
||||
def handle_endtag(self, tag, method):
|
||||
method()
|
||||
|
||||
# Example -- report an unbalanced </...> tag.
|
||||
def report_unbalanced(self, tag):
|
||||
if self.verbose:
|
||||
print('*** Unbalanced </' + tag + '>')
|
||||
print('*** Stack:', self.stack)
|
||||
|
||||
def convert_charref(self, name):
|
||||
"""Convert character reference, may be overridden."""
|
||||
try:
|
||||
n = int(name)
|
||||
except ValueError:
|
||||
return
|
||||
if not 0 <= n <= 127:
|
||||
return
|
||||
return self.convert_codepoint(n)
|
||||
|
||||
def convert_codepoint(self, codepoint):
|
||||
return chr(codepoint)
|
||||
|
||||
def handle_charref(self, name):
|
||||
"""Handle character reference, no need to override."""
|
||||
replacement = self.convert_charref(name)
|
||||
if replacement is None:
|
||||
self.unknown_charref(name)
|
||||
else:
|
||||
self.handle_data(replacement)
|
||||
|
||||
# Definition of entities -- derived classes may override
|
||||
entitydefs = \
|
||||
{'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\''}
|
||||
|
||||
def convert_entityref(self, name):
|
||||
"""Convert entity references.
|
||||
|
||||
As an alternative to overriding this method; one can tailor the
|
||||
results by setting up the self.entitydefs mapping appropriately.
|
||||
"""
|
||||
table = self.entitydefs
|
||||
if name in table:
|
||||
return table[name]
|
||||
else:
|
||||
return
|
||||
|
||||
def handle_entityref(self, name):
|
||||
"""Handle entity references, no need to override."""
|
||||
replacement = self.convert_entityref(name)
|
||||
if replacement is None:
|
||||
self.unknown_entityref(name)
|
||||
else:
|
||||
self.handle_data(replacement)
|
||||
|
||||
# Example -- handle data, should be overridden
|
||||
def handle_data(self, data):
|
||||
pass
|
||||
|
||||
# Example -- handle comment, could be overridden
|
||||
def handle_comment(self, data):
|
||||
pass
|
||||
|
||||
# Example -- handle declaration, could be overridden
|
||||
def handle_decl(self, decl):
|
||||
pass
|
||||
|
||||
# Example -- handle processing instruction, could be overridden
|
||||
def handle_pi(self, data):
|
||||
pass
|
||||
|
||||
# To be overridden -- handlers for unknown objects
|
||||
def unknown_starttag(self, tag, attrs): pass
|
||||
def unknown_endtag(self, tag): pass
|
||||
def unknown_charref(self, ref): pass
|
||||
def unknown_entityref(self, ref): pass
|
||||
|
||||
|
||||
class TestSGMLParser(SGMLParser):
|
||||
|
||||
def __init__(self, verbose=0):
|
||||
self.testdata = ""
|
||||
SGMLParser.__init__(self, verbose)
|
||||
|
||||
def handle_data(self, data):
|
||||
self.testdata = self.testdata + data
|
||||
if len(repr(self.testdata)) >= 70:
|
||||
self.flush()
|
||||
|
||||
def flush(self):
|
||||
data = self.testdata
|
||||
if data:
|
||||
self.testdata = ""
|
||||
print('data:', repr(data))
|
||||
|
||||
def handle_comment(self, data):
|
||||
self.flush()
|
||||
r = repr(data)
|
||||
if len(r) > 68:
|
||||
r = r[:32] + '...' + r[-32:]
|
||||
print('comment:', r)
|
||||
|
||||
def unknown_starttag(self, tag, attrs):
|
||||
self.flush()
|
||||
if not attrs:
|
||||
print('start tag: <' + tag + '>')
|
||||
else:
|
||||
print('start tag: <' + tag, end=' ')
|
||||
for name, value in attrs:
|
||||
print(name + '=' + '"' + value + '"', end=' ')
|
||||
print('>')
|
||||
|
||||
def unknown_endtag(self, tag):
|
||||
self.flush()
|
||||
print('end tag: </' + tag + '>')
|
||||
|
||||
def unknown_entityref(self, ref):
|
||||
self.flush()
|
||||
print('*** unknown entity ref: &' + ref + ';')
|
||||
|
||||
def unknown_charref(self, ref):
|
||||
self.flush()
|
||||
print('*** unknown char ref: &#' + ref + ';')
|
||||
|
||||
def unknown_decl(self, data):
|
||||
self.flush()
|
||||
print('*** unknown decl: [' + data + ']')
|
||||
|
||||
def close(self):
|
||||
SGMLParser.close(self)
|
||||
self.flush()
|
||||
|
||||
|
||||
def test(args = None):
|
||||
import sys
|
||||
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
if args and args[0] == '-s':
|
||||
args = args[1:]
|
||||
klass = SGMLParser
|
||||
else:
|
||||
klass = TestSGMLParser
|
||||
|
||||
if args:
|
||||
file = args[0]
|
||||
else:
|
||||
file = 'test.html'
|
||||
|
||||
if file == '-':
|
||||
f = sys.stdin
|
||||
else:
|
||||
try:
|
||||
f = open(file, 'r')
|
||||
except IOError as msg:
|
||||
print(file, ":", msg)
|
||||
sys.exit(1)
|
||||
|
||||
data = f.read()
|
||||
if f is not sys.stdin:
|
||||
f.close()
|
||||
|
||||
x = klass()
|
||||
for c in data:
|
||||
x.feed(c)
|
||||
x.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
-247
@@ -1,247 +0,0 @@
|
||||
## @package Milter.test
|
||||
# A test framework for milters
|
||||
|
||||
from __future__ import print_function
|
||||
import mime
|
||||
try:
|
||||
from io import BytesIO
|
||||
except:
|
||||
from StringIO import StringIO as BytesIO
|
||||
import Milter
|
||||
|
||||
Milter.NOREPLY = Milter.CONTINUE
|
||||
|
||||
## Test mixin for unit testing %milter applications.
|
||||
# This mixin overrides many Milter.MilterBase methods
|
||||
# with stub versions that simply record what was done.
|
||||
# @deprecated Use Milter.test.TestCtx
|
||||
# @since 0.9.8
|
||||
class TestBase(object):
|
||||
|
||||
def __init__(self,logfile='test/milter.log'):
|
||||
self._protocol = 0
|
||||
self.logfp = open(logfile,"a")
|
||||
## The MAIL FROM for the current email being fed to the %milter
|
||||
self._sender = None
|
||||
## List of recipients deleted
|
||||
self._delrcpt = []
|
||||
## List of recipients added
|
||||
self._addrcpt = []
|
||||
## Macros defined
|
||||
self._macros = { }
|
||||
## The message body.
|
||||
self._body = None
|
||||
## True if the %milter replaced the message body.
|
||||
self._bodyreplaced = False
|
||||
## True if the %milter changed any headers.
|
||||
self._headerschanged = False
|
||||
## True if the %milter changed the envelope from.
|
||||
self._envfromchanged = False
|
||||
## Reply codes and messages set by the %milter
|
||||
self._reply = None
|
||||
## The rfc822 message object for the current email being fed to the %milter.
|
||||
self._msg = None
|
||||
## The protocol stage for macros returned
|
||||
self._stage = None
|
||||
## The macros returned by protocol stage
|
||||
self._symlist = [ None, None, None, None, None, None, None ]
|
||||
|
||||
def _close(self):
|
||||
if self.logfp:
|
||||
self.logfp.close()
|
||||
self.logfp = None
|
||||
|
||||
def log(self,*msg):
|
||||
for i in msg: print(i,file=self.logfp,end=None)
|
||||
print(file=self.logfp)
|
||||
|
||||
## Set a macro value.
|
||||
# These are retrieved by the %milter with getsymval.
|
||||
# @param name the macro name, as passed to getsymval
|
||||
# @param val the macro value
|
||||
def setsymval(self,name,val):
|
||||
self._macros[name] = val
|
||||
|
||||
def getsymval(self,name):
|
||||
stage = self._stage
|
||||
if stage is not None and stage >= 0:
|
||||
syms = self._symlist[stage]
|
||||
if syms is not None and name not in syms:
|
||||
return None
|
||||
return self._macros.get(name,None)
|
||||
|
||||
def replacebody(self,chunk):
|
||||
if self._body:
|
||||
self._body.write(chunk)
|
||||
self._bodyreplaced = True
|
||||
else:
|
||||
raise IOError("replacebody not called from eom()")
|
||||
|
||||
def chgfrom(self,sender,params=None):
|
||||
if not self._body:
|
||||
raise IOError("chgfrom not called from eom()")
|
||||
self.log('chgfrom: sender=%s' % (sender))
|
||||
self._envfromchanged = True
|
||||
self._sender = sender
|
||||
|
||||
# TODO: write implement quarantine()
|
||||
def quarantine(self,reason):
|
||||
raise NotImplemented
|
||||
|
||||
# TODO: measure time between milter calls
|
||||
def progress(self):
|
||||
pass
|
||||
|
||||
# FIXME: rfc822 indexing does not really reflect the way chg/add header
|
||||
# work for a %milter
|
||||
def chgheader(self,field,idx,value):
|
||||
if not self._body:
|
||||
raise IOError("chgheader not called from eom()")
|
||||
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
|
||||
if value == '':
|
||||
del self._msg[field]
|
||||
else:
|
||||
self._msg[field] = value
|
||||
self._headerschanged = True
|
||||
|
||||
def addheader(self,field,value,idx=-1):
|
||||
if not self._body:
|
||||
raise IOError("addheader not called from eom()")
|
||||
self.log('addheader: %s=%s' % (field,value))
|
||||
self._msg[field] = value
|
||||
self._headerschanged = True
|
||||
|
||||
def delrcpt(self,rcpt):
|
||||
if not self._body:
|
||||
raise IOError("delrcpt not called from eom()")
|
||||
self._delrcpt.append(rcpt)
|
||||
|
||||
def addrcpt(self,rcpt):
|
||||
if not self._body:
|
||||
raise IOError("addrcpt not called from eom()")
|
||||
self._addrcpt.append(rcpt)
|
||||
|
||||
## Save the reply codes and messages in self._reply.
|
||||
def setreply(self,rcode,xcode,*msg):
|
||||
self._reply = (rcode,xcode) + msg
|
||||
|
||||
def setsymlist(self,stage,macros):
|
||||
if not self._actions & Milter.SETSYMLIST:
|
||||
raise DisabledAction("SETSYMLIST")
|
||||
if self._stage != -1:
|
||||
raise RuntimeError("setsymlist may only be called from negotiate")
|
||||
# not used yet, but just for grins we save the data
|
||||
a = []
|
||||
for m in macros:
|
||||
try:
|
||||
m = m.encode('utf8')
|
||||
except: pass
|
||||
try:
|
||||
m = m.split(b' ')
|
||||
except: pass
|
||||
a += m
|
||||
if len(a) > 5:
|
||||
raise ValueError('setsymlist limited to 5 macros by MTA')
|
||||
if self._symlist[stage] is not None:
|
||||
raise ValueError('setsymlist already called for stage:'+stage)
|
||||
print('setsymlist',stage,a)
|
||||
self._symlist[stage] = set(a)
|
||||
|
||||
## Feed a file like object to the %milter. Calls envfrom, envrcpt for
|
||||
# each recipient, header for each header field, body for each body
|
||||
# block, and finally eom. A return code from the %milter other than
|
||||
# CONTINUE returns immediately with that return code.
|
||||
#
|
||||
# This is a convenience method, a test could invoke the callbacks
|
||||
# in sequence on its own - and for some complex tests, this may
|
||||
# be necessary.
|
||||
# @param fp the file with rfc2822 message stream
|
||||
# @param sender the MAIL FROM
|
||||
# @param rcpt RCPT TO - additional recipients may follow
|
||||
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
|
||||
self._body = None
|
||||
self._bodyreplaced = False
|
||||
self._headerschanged = False
|
||||
self._reply = None
|
||||
self._sender = '<%s>'%sender
|
||||
msg = mime.message_from_file(fp)
|
||||
# envfrom
|
||||
self._stage = Milter.M_ENVFROM
|
||||
rc = self.envfrom(self._sender)
|
||||
self._stage = None
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# envrcpt
|
||||
for rcpt in (rcpt,) + rcpts:
|
||||
self._stage = Milter.M_ENVRCPT
|
||||
rc = self.envrcpt('<%s>'%rcpt)
|
||||
self._stage = None
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# data
|
||||
self._stage = Milter.M_DATA
|
||||
rc = self.data()
|
||||
self._stage = None
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# header
|
||||
for h,val in msg.items():
|
||||
rc = self.header_bytes(h,val.encode())
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# eoh
|
||||
self._stage = Milter.M_EOH
|
||||
rc = self.eoh()
|
||||
self._stage = None
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# body
|
||||
header,body = msg.as_bytes().split(b'\n\n',1)
|
||||
bfp = BytesIO(body)
|
||||
while 1:
|
||||
buf = bfp.read(8192)
|
||||
if len(buf) == 0: break
|
||||
rc = self.body(buf)
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
self._msg = msg
|
||||
self._body = BytesIO()
|
||||
self._stage = Milter.M_EOM
|
||||
rc = self.eom()
|
||||
self._stage = None
|
||||
if self._bodyreplaced:
|
||||
body = self._body.getvalue()
|
||||
self._body = BytesIO()
|
||||
self._body.write(header)
|
||||
self._body.write(b'\n\n')
|
||||
self._body.write(body)
|
||||
self.close()
|
||||
self._close()
|
||||
return rc
|
||||
|
||||
## Feed an email contained in a file to the %milter.
|
||||
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
||||
# @param sender MAIL FROM
|
||||
# @param rcpts RCPT TO, multiple recipients may be supplied
|
||||
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
||||
with open('test/'+fname,'rb') as fp:
|
||||
return self.feedFile(fp,sender,*rcpts)
|
||||
|
||||
## Call the connect and helo callbacks.
|
||||
# The helo callback is not called if connect does not return CONTINUE.
|
||||
# @param host the hostname passed to the connect callback
|
||||
# @param helo the hostname passed to the helo callback
|
||||
# @param ip the IP address passed to the connect callback
|
||||
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
|
||||
self._body = None
|
||||
self._bodyreplaced = False
|
||||
self._setctx(None)
|
||||
opts = [ Milter.CURR_ACTS,~0,0,0 ]
|
||||
self._stage = -1
|
||||
rc = self.negotiate(opts)
|
||||
self._stage = Milter.M_CONNECT
|
||||
rc = super(TestBase,self).connect(host,1,(ip,1234))
|
||||
if rc != Milter.CONTINUE:
|
||||
self._stage = None
|
||||
self.close()
|
||||
return rc
|
||||
self._stage = Milter.M_HELO
|
||||
rc = self.hello(helo)
|
||||
self._stage = None
|
||||
if rc != Milter.CONTINUE:
|
||||
self.close()
|
||||
return rc
|
||||
@@ -1,312 +0,0 @@
|
||||
## @package Milter.testctx
|
||||
# A test framework for milters that replaces milterContext rather
|
||||
# than Milter.Base. Since miltermodule.c doesn't currently export
|
||||
# a way to query callbacks set (and we might want to run without
|
||||
# loading milter), we assume the callbacks set by Milter.runmilter().
|
||||
|
||||
from __future__ import print_function
|
||||
from socket import AF_INET,AF_INET6
|
||||
from sys import version as VERSION
|
||||
import time
|
||||
import mime
|
||||
try:
|
||||
from io import BytesIO
|
||||
except:
|
||||
from StringIO import StringIO as BytesIO
|
||||
import Milter
|
||||
from Milter import utils
|
||||
|
||||
## Milter context for unit testing %milter applications.
|
||||
# A substitute for milter.milterContext that can be passed to
|
||||
# Milter.Base._setctx().
|
||||
# @since 1.0.3
|
||||
class TestCtx(object):
|
||||
default_opts = [Milter.CURR_ACTS,0x1fffff,0,0]
|
||||
def __init__(self,logfile='test/milter.log'):
|
||||
## Usually the Milter application derived from Milter.Base
|
||||
self._priv = None
|
||||
## List of recipients deleted
|
||||
self._delrcpt = []
|
||||
## List of recipients added
|
||||
self._addrcpt = []
|
||||
## Macros defined
|
||||
self._macros = { }
|
||||
## Reply codes and messages set by the %milter
|
||||
self._reply = None
|
||||
## The macros returned by protocol stage
|
||||
self._symlist = [ None, None, None, None, None, None, None ]
|
||||
## 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
|
||||
## The rfc822 message object for the current email being fed to the %milter.
|
||||
self._msg = None
|
||||
## The MAIL FROM for the current email being fed to the %milter
|
||||
self._sender = None
|
||||
## True if the %milter changed the envelope from.
|
||||
self._envfromchanged = False
|
||||
## List of recipients added
|
||||
self._addrcpt = []
|
||||
## Negotiated options
|
||||
self._opts = TestCtx.default_opts
|
||||
## Last activity
|
||||
self._activity = time.time()
|
||||
|
||||
def getpriv(self):
|
||||
return self._priv
|
||||
|
||||
def setpriv(self,priv):
|
||||
self._priv = priv
|
||||
|
||||
def getsymval(self,name):
|
||||
stage = self._stage
|
||||
if stage >= 0:
|
||||
try:
|
||||
s = name.encode('utf8')
|
||||
except: pass
|
||||
syms = self._symlist[stage]
|
||||
if syms is not None and s not in syms:
|
||||
return None
|
||||
return self._macros.get(name,None)
|
||||
|
||||
def _setsymval(self,name,val):
|
||||
self._macros[name] = val
|
||||
|
||||
def setreply(self,rcode,xcode,*msg):
|
||||
self._reply = (rcode,xcode) + msg
|
||||
|
||||
def setsymlist(self,stage,macros):
|
||||
if self._stage != -1:
|
||||
raise RuntimeError("setsymlist may only be called from negotiate")
|
||||
# Records which macros are available to getsymval()
|
||||
m = macros
|
||||
try:
|
||||
m = m.encode('utf8')
|
||||
except: pass
|
||||
try:
|
||||
m = m.split(b' ')
|
||||
except: pass
|
||||
if len(m) > 5:
|
||||
raise ValueError('setsymlist limited to 5 macros by MTA')
|
||||
if self._symlist[stage] is not None:
|
||||
raise ValueError('setsymlist already called for stage:'+stage)
|
||||
if not m:
|
||||
raise ValueError('setsymlist with empty list for stage:'+stage)
|
||||
self._symlist[stage] = set(m)
|
||||
|
||||
def addheader(self,field,value,idx):
|
||||
if not self._body:
|
||||
raise IOError("addheader not called from eom()")
|
||||
self._msg[field] = value
|
||||
self._headerschanged = True
|
||||
|
||||
def chgheader(self,field,idx,value):
|
||||
if not self._body:
|
||||
raise IOError("chgheader not called from eom()")
|
||||
if value == '':
|
||||
del self._msg[field]
|
||||
else:
|
||||
self._msg[field] = value
|
||||
self._headerschanged = True
|
||||
|
||||
def addrcpt(self,rcpt,params):
|
||||
if not self._body:
|
||||
raise IOError("addrcpt not called from eom()")
|
||||
self._addrcpt.append((rcpt,params))
|
||||
|
||||
def delrcpt(self,rcpt):
|
||||
if not self._body:
|
||||
raise IOError("delrcpt not called from eom()")
|
||||
self._delrcpt.append(rcpt)
|
||||
|
||||
def replacebody(self,chunk):
|
||||
if self._body:
|
||||
self._body.write(chunk)
|
||||
self._bodyreplaced = True
|
||||
else:
|
||||
raise IOError("replacebody not called from eom()")
|
||||
|
||||
def chgfrom(self,sender,params=None):
|
||||
if not self._body:
|
||||
raise IOError("chgfrom not called from eom()")
|
||||
self._envfromchanged = True
|
||||
self._sender = sender
|
||||
|
||||
def quarantine(self,reason):
|
||||
raise NotImplemented
|
||||
|
||||
## Reset activity timer.
|
||||
def progress(self):
|
||||
self._activity = time.time()
|
||||
|
||||
def _abort(self):
|
||||
"What Milter sets for abort_callback"
|
||||
self._priv.abort()
|
||||
self._close()
|
||||
|
||||
def _close(self):
|
||||
Milter.close_callback(self)
|
||||
|
||||
def _negotiate(self):
|
||||
self._body = None
|
||||
self._bodyreplaced = False
|
||||
self._priv = None
|
||||
self._opts = TestCtx.default_opts
|
||||
self._stage = -1
|
||||
rc = Milter.negotiate_callback(self,self._opts)
|
||||
if rc == Milter.ALL_OPTS:
|
||||
self._opts = TestCtx.default_opts
|
||||
elif rc != Milter.CONTINUE:
|
||||
self._abort()
|
||||
self._close()
|
||||
self._protocol = self._opts[1]
|
||||
return rc
|
||||
|
||||
def _connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
|
||||
rc = self._negotiate()
|
||||
# FIXME: what if not CONTINUE or ALL_OPTS?
|
||||
if self._protocol & Milter.P_NOCONNECT:
|
||||
return Milter.CONTINUE
|
||||
if utils.ip4re.match(ip):
|
||||
af = AF_INET
|
||||
elif utils.ip6re.match(ip):
|
||||
af = AF_INET6
|
||||
else:
|
||||
raise ValueError('TestCtx.connect: invalid ip address: '+ip)
|
||||
self._stage = Milter.M_CONNECT
|
||||
rc = Milter.connect_callback(self,host,af,ip)
|
||||
self._stage = None
|
||||
if rc != Milter.CONTINUE:
|
||||
self._close()
|
||||
return rc
|
||||
return self._helo(helo)
|
||||
|
||||
def _helo(self,helo):
|
||||
if self._protocol & Milter.P_NOHELO:
|
||||
return Milter.CONTINUE
|
||||
self._stage = Milter.M_HELO
|
||||
rc = self._priv.hello(helo)
|
||||
self._stage = None
|
||||
if rc != Milter.CONTINUE:
|
||||
self._close()
|
||||
return rc
|
||||
|
||||
def _envfrom(self,*s):
|
||||
self._sender = s[0]
|
||||
if self._protocol & Milter.P_NOMAIL:
|
||||
return Milter.CONTINUE
|
||||
self._stage = Milter.M_ENVFROM
|
||||
rc = self._priv.envfrom(*s)
|
||||
self._stage = None
|
||||
return rc
|
||||
|
||||
def _envrcpt(self,s):
|
||||
if self._protocol & Milter.P_NORCPT:
|
||||
return Milter.CONTINUE
|
||||
self._stage = Milter.M_ENVRCPT
|
||||
rc = self._priv.envrcpt(s)
|
||||
self._stage = None
|
||||
return rc
|
||||
|
||||
def _data(self):
|
||||
if self._protocol & Milter.P_NODATA:
|
||||
return Milter.CONTINUE
|
||||
self._stage = Milter.M_DATA
|
||||
rc = self._priv.data()
|
||||
self._stage = None
|
||||
return rc
|
||||
|
||||
def _header(self,fld,val):
|
||||
if VERSION < '3.0.0':
|
||||
return self._priv.header(fld,val)
|
||||
# email.message_from_binary_file uses surrogateescape to
|
||||
# preserve original bytes in unicode string for decoding errors.
|
||||
# convert str or Header back to original bytes
|
||||
if hasattr(val, '_chunks'):
|
||||
# val is a Header object for invalid header values
|
||||
v = b''
|
||||
for s,charset in val._chunks:
|
||||
# recover the original bytes
|
||||
b = s.encode(encoding='ascii',errors='surrogateescape')
|
||||
v += b
|
||||
else:
|
||||
v = val.encode(encoding='ascii',errors='surrogateescape')
|
||||
# invoke the Milter header_callback
|
||||
return self._priv.header_bytes(fld,v)
|
||||
|
||||
def _eoh(self):
|
||||
if self._protocol & Milter.P_NOEOH:
|
||||
return Milter.CONTINUE
|
||||
self._stage = Milter.M_EOH
|
||||
rc = self._priv.eoh()
|
||||
self._stage = None
|
||||
return rc
|
||||
|
||||
def _feed_body(self,bfp):
|
||||
if self._protocol & Milter.P_NOBODY:
|
||||
return Milter.CONTINUE
|
||||
while True:
|
||||
buf = bfp.read(8192)
|
||||
if len(buf) == 0: break
|
||||
rc = self._priv.body(buf)
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
return Milter.CONTINUE
|
||||
|
||||
def _eom(self):
|
||||
self._body = BytesIO()
|
||||
self._stage = Milter.M_EOM
|
||||
rc = self._priv.eom()
|
||||
self._stage = None
|
||||
return rc
|
||||
|
||||
## Feed a file like object to the ctx. Calls the callbacks in
|
||||
# the same sequence as libmilter.
|
||||
# @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 = mime.message_from_file(fp)
|
||||
self._msg = msg
|
||||
# envfrom
|
||||
rc = self._envfrom('<%s>'%sender)
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# envrcpt
|
||||
for rcpt in (rcpt,) + rcpts:
|
||||
rc = self._envrcpt('<%s>'%rcpt)
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# data
|
||||
rc = self._data()
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# header
|
||||
for h,val in msg.items():
|
||||
rc = self._header(h,val)
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# eoh
|
||||
rc = self._eoh()
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
# body
|
||||
header,body = msg.as_bytes().split(b'\n\n',1)
|
||||
rc = self._feed_body(BytesIO(body))
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
rc = self._eom()
|
||||
if self._bodyreplaced:
|
||||
body = self._body.getvalue()
|
||||
self._body = BytesIO()
|
||||
self._body.write(header)
|
||||
self._body.write(b'\n\n')
|
||||
self._body.write(body)
|
||||
return rc
|
||||
|
||||
## Feed an email contained in a file to the %milter.
|
||||
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
||||
# @param sender MAIL FROM
|
||||
# @param rcpts RCPT TO, multiple recipients may be supplied
|
||||
def _feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
||||
with open('test/'+fname,'rb') as fp:
|
||||
return self._feedFile(fp,sender,*rcpts)
|
||||
+19
-46
@@ -5,14 +5,12 @@
|
||||
import re
|
||||
import struct
|
||||
import socket
|
||||
import email.errors
|
||||
from email.header import decode_header
|
||||
import email.base64mime
|
||||
import email.utils
|
||||
import email.Errors
|
||||
from fnmatch import fnmatchcase
|
||||
from binascii import a2b_base64
|
||||
from email.Header import decode_header
|
||||
#import email.Utils
|
||||
import rfc822
|
||||
|
||||
dnsre = re.compile(r'^[a-z][-a-z\d.]+$', re.IGNORECASE)
|
||||
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
||||
ip4re = re.compile(PAT_IP4+'$')
|
||||
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
|
||||
@@ -55,8 +53,8 @@ if hasattr(socket,'has_ipv6') and socket.has_ipv6:
|
||||
else:
|
||||
from pyip6 import inet_ntop, inet_pton
|
||||
|
||||
MASK = 0xFFFFFFFF
|
||||
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
||||
MASK = 0xFFFFFFFFL
|
||||
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
|
||||
|
||||
def cidr(i,n,mask=MASK):
|
||||
return ~(mask >> n) & mask & i
|
||||
@@ -69,12 +67,6 @@ def iniplist(ipaddr,iplist):
|
||||
True
|
||||
>>> iniplist('192.168.0.45',['192.168.0.*'])
|
||||
True
|
||||
>>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
|
||||
True
|
||||
>>> iniplist('2606:2800:220:1::',['example.com/40'])
|
||||
True
|
||||
>>> iniplist('4.2.2.2',['nothing.example.com'])
|
||||
False
|
||||
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
||||
True
|
||||
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
||||
@@ -83,10 +75,8 @@ def iniplist(ipaddr,iplist):
|
||||
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
|
||||
"""
|
||||
if ip4re.match(ipaddr):
|
||||
fam = socket.AF_INET
|
||||
ipnum = addr2bin(ipaddr)
|
||||
elif ip6re.match(ipaddr):
|
||||
fam = socket.AF_INET6
|
||||
ipnum = bin2long6(inet_pton(ipaddr))
|
||||
else:
|
||||
raise ValueError('Invalid ip syntax:'+ipaddr)
|
||||
@@ -106,21 +96,13 @@ def iniplist(ipaddr,iplist):
|
||||
n = 128
|
||||
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
|
||||
return True
|
||||
elif dnsre.match(p[0]):
|
||||
try:
|
||||
sfx = '/'.join(['']+p[1:])
|
||||
addrlist = [r[4][0]+sfx for r in socket.getaddrinfo(p[0],25,fam)]
|
||||
if iniplist(ipaddr,addrlist):
|
||||
return True
|
||||
except socket.gaierror: pass
|
||||
elif fnmatchcase(ipaddr,pat):
|
||||
return True
|
||||
return False
|
||||
|
||||
## Split email into Fullname and address.
|
||||
# This replaces <code>email.utils.parseaddr</code> but fixes
|
||||
# This replaces <code>email.Utils.parseaddr</code> but fixes
|
||||
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
|
||||
# Additional tricky cases are still broken. Patches welcome.
|
||||
#
|
||||
def parseaddr(t):
|
||||
"""Split email into Fullname and address.
|
||||
@@ -134,10 +116,12 @@ def parseaddr(t):
|
||||
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
|
||||
('God@heaven', 'jeff@spec.org')
|
||||
>>> parseaddr('Real Name ((comment)) <addr...@example.com>')
|
||||
('Real Name (comment)', 'addr...@example.com')
|
||||
('Real Name', 'addr...@example.com')
|
||||
>>> parseaddr('a(WRONG)@b')
|
||||
('WRONG', 'a@b')
|
||||
"""
|
||||
#return email.utils.parseaddr(t)
|
||||
res = email.utils.parseaddr(t)
|
||||
#return email.Utils.parseaddr(t)
|
||||
res = rfc822.parseaddr(t)
|
||||
# dirty fix for some broken cases
|
||||
if not res[0]:
|
||||
pos = t.find('<')
|
||||
@@ -146,7 +130,7 @@ def parseaddr(t):
|
||||
pos1 = addrspec.rfind(':')
|
||||
if pos1 > 0:
|
||||
addrspec = addrspec[pos1+1:]
|
||||
return email.utils.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
|
||||
return rfc822.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
|
||||
if not res[1]:
|
||||
pos = t.find('<')
|
||||
if pos > 0 and t[-1] == '>':
|
||||
@@ -154,19 +138,9 @@ def parseaddr(t):
|
||||
pos1 = addrspec.rfind(':')
|
||||
if pos1 > 0:
|
||||
addrspec = addrspec[pos1+1:]
|
||||
return email.utils.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
|
||||
return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
|
||||
return res
|
||||
|
||||
## Fix email.base64mime.decode to add any missing padding
|
||||
def decode(s, convert_eols=None):
|
||||
if not s: return s
|
||||
while len(s) % 4: s += '=' # add missing padding
|
||||
dec = a2b_base64(s)
|
||||
if convert_eols:
|
||||
return dec.replace(CRLF, convert_eols)
|
||||
return dec
|
||||
|
||||
email.base64mime.decode = decode
|
||||
|
||||
def parse_addr(t):
|
||||
"""Split email into user,domain.
|
||||
@@ -211,19 +185,18 @@ def parse_header(val):
|
||||
for s,enc in h:
|
||||
if enc:
|
||||
try:
|
||||
u.append(s.decode(enc,'replace'))
|
||||
u.append(unicode(s,enc,'replace'))
|
||||
except LookupError:
|
||||
u.append(s.decode())
|
||||
u.append(unicode(s))
|
||||
else:
|
||||
u.append(s.decode())
|
||||
u.append(unicode(s))
|
||||
u = ''.join(u)
|
||||
if type(u) is str: return u
|
||||
for enc in ('us-ascii','iso-8859-1','utf-8'):
|
||||
for enc in ('us-ascii','iso-8859-1','utf8'):
|
||||
try:
|
||||
return u.encode(enc)
|
||||
except UnicodeError: continue
|
||||
except UnicodeDecodeError: pass
|
||||
except LookupError: pass
|
||||
except ValueError: pass
|
||||
except email.errors.HeaderParseError: pass
|
||||
except email.Errors.HeaderParseError: pass
|
||||
return val
|
||||
|
||||
+94
-42
@@ -1,4 +1,5 @@
|
||||
# Abstract
|
||||
Abstract
|
||||
--------
|
||||
|
||||
This is a python extension module to enable python scripts to attach to
|
||||
Sendmail's libmilter API, enabling filtering of messages as they arrive.
|
||||
@@ -6,44 +7,64 @@ Since it's a script, you can do anything you want to the message - screen
|
||||
out viruses, collect statistics, add or modify headers, etc. You can, at
|
||||
any point, tell Sendmail to reject, discard, or accept the message.
|
||||
|
||||
Additional python modules provide for navigating and modifying MIME parts, and
|
||||
sending DSNs or doing CBVs.
|
||||
|
||||
# Requirements
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Python milter extension: https://pypi.org/project/pymilter/
|
||||
This python milter extension: http://www.bmsi.com/python/milter.html
|
||||
Python: http://www.python.org
|
||||
Sendmail: http://www.sendmail.org
|
||||
or
|
||||
Postfix: http://www.postfix.org/MILTER_README.html
|
||||
NB: From Sendmail's libmilter/README:
|
||||
|
||||
# Quick Installation
|
||||
libmilter requires pthread support in the operating system. Moreover, it
|
||||
requires that the library functions it uses are thread safe; which is true
|
||||
for the operating systems libmilter has been developed and tested on. On
|
||||
some operating systems this requires special compile time options (e.g.,
|
||||
not just -pthread). libmilter is currently known to work on (modulo
|
||||
problems in the pthread support of some specific versions):
|
||||
|
||||
1. Build and install Sendmail, enabling libmilter (see libmilter/README).
|
||||
2. Build and install Python, enabling threading.
|
||||
3. Install this module: python setup.py --help
|
||||
4. Add these two lines to sendmail.cf[a]:
|
||||
```
|
||||
O InputMailFilters=pythonfilter
|
||||
Xpythonfilter, S=local:/home/username/pythonsock
|
||||
```
|
||||
5. Run the sample.py example milter with: python sample.py
|
||||
Note that milters should almost certainly not run as root.
|
||||
FreeBSD 3.x, 4.x
|
||||
SunOS 5.x (x >= 5)
|
||||
AIX 4.3.x
|
||||
HP UX 11.x
|
||||
Linux (recent versions/distributions)
|
||||
OpenBSD
|
||||
AIX 4.1.5
|
||||
|
||||
libmilter is currently not supported on:
|
||||
|
||||
IRIX 6.x
|
||||
Ultrix
|
||||
|
||||
Quick Installation
|
||||
------------------
|
||||
|
||||
1. Build and install Sendmail, enabling libmilter (see libmilter/README).
|
||||
2. Build and install Python, enabling threading.
|
||||
3. Install this module: python setup.py --help
|
||||
4. Add these two lines to sendmail.cf[*]:
|
||||
|
||||
O InputMailFilters=pythonfilter
|
||||
Xpythonfilter, S=local:/home/username/pythonsock
|
||||
|
||||
5. Run the sample.py example milter with: python sample.py
|
||||
Note that milters should almost certainly not run as root.
|
||||
|
||||
That's it. Incoming mail will cause the milter to print some things, and
|
||||
some email will be rejected (see the "header" method). Edit and play.
|
||||
See spfmilter.py for a functional SPF milter, or see bms.py for an complex
|
||||
milter used in production.
|
||||
|
||||
[a] This is for a quick test. Your sendmail.cf in most distros will get
|
||||
[*] This is for a quick test. Your sendmail.cf in most distros will get
|
||||
overwritten whenever sendmail.mc is updated. To make a milter permanent,
|
||||
add something like:
|
||||
```
|
||||
|
||||
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
|
||||
```
|
||||
|
||||
to sendmail.mc instead.
|
||||
|
||||
# Not-so-quick Installation
|
||||
Not-so-quick Installation
|
||||
-------------------------
|
||||
|
||||
First install Sendmail. Make sure you read libmilter/README in the Sendmail
|
||||
source directory, and make sure you enable libmilter before you build. The
|
||||
@@ -56,13 +77,18 @@ Install this miltermodule package; DistUtils Automatic Installation:
|
||||
|
||||
$ python setup.py --help
|
||||
|
||||
For versions of python prior to 2.0, you will need to download distutils
|
||||
separately or build manually. You will need to download unittest
|
||||
separately to run the test programs. The bdist_rpm distutils option seems
|
||||
not to work for python 2.0; upgrade to at least 2.1.1.
|
||||
|
||||
Now that everything is installed, we need to tell sendmail that we're going
|
||||
to filter incoming email. Add lines similar to the following to
|
||||
sendmail.cf:
|
||||
```
|
||||
|
||||
O InputMailFilters=pythonfilter
|
||||
Xpythonfilter, S=local:/home/username/pythonsock
|
||||
```
|
||||
|
||||
The "O" line tells sendmail which filters to use in what order; here we're
|
||||
telling sendmail to use the filter named "pythonfilter".
|
||||
|
||||
@@ -76,24 +102,49 @@ NB: The name is specified in two places: here, in sendmail's cf file, and
|
||||
in the milter itself. Make sure the two match.
|
||||
|
||||
NB: The above lines can be added in your .mc file with this line:
|
||||
```
|
||||
|
||||
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock')
|
||||
```
|
||||
|
||||
For versions of sendmail prior to 8.12, you will need to enable
|
||||
`_FFR_MILTER` for the cf macros. For example,
|
||||
```
|
||||
_FFR_MILTER for the cf macros. For example,
|
||||
|
||||
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
|
||||
```
|
||||
# IPv6 Notes
|
||||
|
||||
|
||||
RedHat 6.2 Notes
|
||||
----------------
|
||||
|
||||
The Redhat 6.2 sendmail RPM does not enable milter. You can obtain a
|
||||
modified spec file at
|
||||
|
||||
http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec
|
||||
|
||||
use it to rebuild the Redhat 7.2 SRPM. The RH6.2 SRPM does not have
|
||||
recent sendmail security patches.
|
||||
|
||||
RedHat 7.2 Notes
|
||||
----------------
|
||||
|
||||
The Redhat 7.2 sendmail RPM enables milter in sendmail - but does not include
|
||||
the headers needed for compiling a milter. You can obtain a modified spec
|
||||
file with a sendmail-devel package that includes the needed static libraries
|
||||
and headers at
|
||||
|
||||
http://www.bmsi.com/linux/sendmail-rh72.spec
|
||||
|
||||
IPv6 Notes
|
||||
----------
|
||||
|
||||
IPv6 is still experimental.
|
||||
|
||||
The IPv6 protocol is supported if your operation system supports it
|
||||
and if sendmail was compiled with IPv6 support. To determine if your
|
||||
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
|
||||
compilation option. To compile sendmail with IPv6 support, add this
|
||||
declaration to your site.config.m4 before building it:
|
||||
```
|
||||
|
||||
APPENDDEF(`confENVDEF', `-DNETINET6=1')
|
||||
```
|
||||
|
||||
IPv6 support can show up in two places; the communications socket
|
||||
between the milter and sendmail processes and in the host address
|
||||
argument to the connect() callback method.
|
||||
@@ -104,26 +155,26 @@ want to allow both IPv4 and IPv6 connections, some operating systems
|
||||
will require that each listens to different port numbers. For an
|
||||
IPv6-only setup, your sendmail configuration should contain a line
|
||||
similar to (first line is for sendmail.mc, second is sendmail.cf):
|
||||
```
|
||||
|
||||
DAEMON_OPTIONS(`Name=MTA-v6, Family=inet6, Modify=C, Port=25')
|
||||
O DaemonPortOptions=Name=MTA-v6, Family=inet6, Modify=C, Port=25
|
||||
```
|
||||
|
||||
To allow sendmail and the milter process to communicate with each
|
||||
other over IPv6, you may use the "inet6" socket name prefix, as in:
|
||||
```
|
||||
|
||||
Xpythonfilter, S=inet6:1234@fec0:0:0:7::5c
|
||||
```
|
||||
|
||||
The connect() callback method in the milter class will pass the
|
||||
IPv6-specific information in the 'hostaddr' argument as a tuple. Note
|
||||
that the type of this value is dependent upon the protocol family, and
|
||||
is not compatible with IPv4 connections. Therefore you should always
|
||||
check the family argument before attempting to use the hostaddr
|
||||
argument. A quick example showing this follows:
|
||||
```
|
||||
|
||||
import socket
|
||||
|
||||
...
|
||||
class ipv6awareMilter(Milter.Milter):
|
||||
|
||||
...
|
||||
def connect(self,hostname,family,hostaddr):
|
||||
if family==socket.AF_INET:
|
||||
ipaddress, port = hostaddr
|
||||
@@ -131,7 +182,7 @@ argument. A quick example showing this follows:
|
||||
ip6address, port, flowinfo, scopeid = hostaddr
|
||||
elif family==socket.AF_UNIX:
|
||||
socketpath = hostaddr
|
||||
```
|
||||
|
||||
The hostname argument is always safe to use without interpreting the
|
||||
protocol family. For IPv6 connections for which the hostname can not
|
||||
be determined the hostname will appear similar to the string
|
||||
@@ -139,10 +190,11 @@ be determined the hostname will appear similar to the string
|
||||
RFC 2553 for information on interpreting and using the flowinfo and
|
||||
scopeid socket attributes, both of which are integers.
|
||||
|
||||
# Authors
|
||||
Authors
|
||||
-------
|
||||
|
||||
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
||||
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
|
||||
and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that
|
||||
kludge and added threading and context objects to it, wrote a proper OO
|
||||
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
||||
it with distutils, and generally transformed it from a quick hack to a
|
||||
@@ -1,4 +1,6 @@
|
||||
Test case for Milter/dsn.py
|
||||
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
|
||||
Milter.utils.parse_addr() pass all unit tests.
|
||||
|
||||
+14
-59
@@ -1,48 +1,46 @@
|
||||
## @mainpage Writing Milters in Python
|
||||
#
|
||||
# At the lowest level, the <code>milter</code> module provides a thin wrapper
|
||||
# around the <a href="milter_api/index.html"> sendmail
|
||||
# around the <a href="https://www.milter.org/developers/api/index"> sendmail
|
||||
# libmilter API</a>. This API lets you register callbacks for a number of
|
||||
# events in the process of sendmail receiving a message via SMTP. These
|
||||
# events include the initial connection from a MTA, the envelope sender and
|
||||
# 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
|
||||
# the %milter.
|
||||
# the milter.
|
||||
#
|
||||
# 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
|
||||
# use the Milter module, an application registers a 'factory' to create an
|
||||
# object for each connection from a MTA to sendmail. These connection objects
|
||||
# must provide methods corresponding to the libmilter event callbacks.
|
||||
# must provide methods corresponding to the libmilter callback events.
|
||||
#
|
||||
# Each callback method returns a code to tell sendmail whether to proceed with
|
||||
# Each event method returns a code to tell sendmail whether to proceed with
|
||||
# processing the message. This is a big advantage of milters over other mail
|
||||
# filtering systems. Unwanted mail can be stopped in its tracks at the
|
||||
# earliest possible point. The callback return codes are
|
||||
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
|
||||
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
|
||||
# earliest possible point.
|
||||
#
|
||||
# The Milter.Base class provides default implementations for
|
||||
# The <code>Milter.Base</code> class provides default implementations for
|
||||
# event methods that do nothing, and also provides wrappers for the libmilter
|
||||
# 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.
|
||||
#
|
||||
# The Milter.Milter class provides an alternate default
|
||||
# implementation that logs the main milter callbacks, but otherwise does
|
||||
# nothing. It is provided for compatibility.
|
||||
# The <code>Milter.Milter</code> class provides an alternate default
|
||||
# implementation that logs the main milter events, but otherwise does nothing.
|
||||
# It is provided for compatibility.
|
||||
#
|
||||
# The mime module provides a wrapper for the Python email package
|
||||
# The <code>mime</code> module provides a wrapper for the Python email package
|
||||
# that fixes some bugs, and simplifies modifying selected parts of a MIME
|
||||
# message.
|
||||
#
|
||||
# @section threading
|
||||
#
|
||||
# The libmilter library which pymilter wraps
|
||||
# <a href="milter_overview#SignalHandling">handles
|
||||
# <a href="https://www.milter.org/developers/overview#SignalHandling">handles
|
||||
# all signals</a> itself, and expects to be called from a single main thread.
|
||||
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
|
||||
# <a href="milter_api/smfi_stop.html">smfi_stop</a>
|
||||
# <a href="https://www.milter.org/developers/api/smfi_stop">smfi_stop</a>
|
||||
# and the last to an internal ABORT.
|
||||
#
|
||||
# If you use python threads or threading modules, then signal handling gets
|
||||
@@ -52,47 +50,4 @@
|
||||
# You may find the
|
||||
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
|
||||
# multiprocessing</a> module useful. It can be a drop-in
|
||||
# replacement for threading as illustrated in
|
||||
# <a href="milter-template_8py-example.html">milter-template.py</a>.
|
||||
#
|
||||
# @section Useful python packages for milters
|
||||
#
|
||||
# <a href="https://github.com/sdgathman/pymilter">pymilter</a> - this package.
|
||||
#
|
||||
# <a href="https://github.com/sdgathman/pyspf">pyspf</a> checks the
|
||||
# SMTP envelope sender (MAIL FROM, passed to the Milter.Base.envfrom callback)
|
||||
# against a Sender Policy published in DNS by the sending domain. This
|
||||
# can prevent forgery of the MAIL FROM. SPF is Sender Policy Framework.
|
||||
#
|
||||
# <a href="https://launchpad.net/dkimpy">pydkim</a> checks a DKIM signature
|
||||
# of the email body and headers against a public key published in DNS by
|
||||
# the signing domain. DKIM is DomainKeys Identified Mail.
|
||||
#
|
||||
# The <a href="https://pypi.python.org/pypi/authres/">authres</a> module
|
||||
# parses and formats the Authentication-Results email header, providing
|
||||
# a standard place to summarize the results from DKIM, SPF, rDNS, SMTP AUTH,
|
||||
# and other email authentication methods.
|
||||
#
|
||||
# <a href="https://github.com/sdgathman/pydspam/">pydspam</a> wraps
|
||||
# the libdspam API of the <a href="http://dspam.sourceforge.net/">DSPAM</a>
|
||||
# project.
|
||||
#
|
||||
# <a href="https://github.com/sdgathman/pysrs/">pysrs</a> rewrites
|
||||
# MAIL FROM to include a timestamped signature so that "bounce spam"
|
||||
# can be immediately rejected.
|
||||
#
|
||||
# <a href="https://github.com/sdgathman/pygossip/">pygossip</a> is a
|
||||
# system to track reputation by domain and authentication level and type,
|
||||
# and a simple protocol to gossip about reputations with other mail servers.
|
||||
#
|
||||
# @section Milters written with pymilter
|
||||
#
|
||||
# <a href="https://github.com/croessner/vrfydmn">Verify Domain</a> is a
|
||||
# Postfix milter that rejects/fixes manipulated From: header
|
||||
# on a mail host with multiple virtual domains.
|
||||
#
|
||||
# <a href="https://github.com/sdgathman/milter/">BMS Milter</a> has several
|
||||
# milters, a big complicated spam filter that integrates multiple
|
||||
# authentication protocols with pydspam, and two simple ones: spfmilter.py and
|
||||
# dkim-milter.py.
|
||||
#
|
||||
# replacement for threading as illustrated in @ref milter-template.py.
|
||||
|
||||
+37
-121
@@ -3,65 +3,10 @@
|
||||
|
||||
## @package milter
|
||||
#
|
||||
# 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.
|
||||
# A thin wrapper around libmilter.
|
||||
#
|
||||
|
||||
## 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.
|
||||
## Hold context for a milter connection.
|
||||
# Each connection to sendmail creates a new <code>SMFICTX</code> struct within
|
||||
# libmilter. The milter module in turn creates a milterContext
|
||||
# tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code>
|
||||
@@ -75,52 +20,46 @@ NOREPLY = 6
|
||||
# and converts function callbacks to instance method invocations.
|
||||
#
|
||||
class milterContext(object):
|
||||
## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_getsymval">smfi_getsymval</a>.
|
||||
def getsymval(self,sym): pass
|
||||
## Calls <a href="milter_api/smfi_setreply.html">
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_setreply">
|
||||
# smfi_setreply</a> or
|
||||
# <a href="milter_api/smfi_setmlreply.html">
|
||||
# <a href="https://www.milter.org/developers/api/smfi_setmlreply">
|
||||
# smfi_setmlreply</a>.
|
||||
# @param rcode SMTP response code
|
||||
# @param xcode extended SMTP response code
|
||||
# @param msg one or more message lines. If the MTA does not support
|
||||
# multiline messages, only the first is used.
|
||||
def setreply(self,rcode,xcode,*msg): pass
|
||||
## Calls <a href="milter_api/smfi_addheader.html">smfi_addheader</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_addheader">smfi_addheader</a>.
|
||||
def addheader(self,name,value,idx=-1): pass
|
||||
## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">smfi_chgheader</a>.
|
||||
def chgheader(self,name,idx,value): pass
|
||||
## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">smfi_addrcpt</a>.
|
||||
def addrcpt(self,rcpt,params=None): pass
|
||||
## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">smfi_delrcpt</a>.
|
||||
def delrcpt(self,rcpt): pass
|
||||
## Calls <a href="milter_api/smfi_replacebody.html">smfi_replacebody</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">smfi_replacebody</a>.
|
||||
def replacebody(self,data): pass
|
||||
## Attach a Python object to this connection context.
|
||||
# @return the old value or None
|
||||
def setpriv(self,priv): pass
|
||||
## Return the Python object attached to this connection context.
|
||||
def getpriv(self): pass
|
||||
## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">smfi_quarantine</a>.
|
||||
def quarantine(self,reason): pass
|
||||
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_progress">smfi_progress</a>.
|
||||
def progress(self): pass
|
||||
## Calls <a href="milter_api/smfi_chgfrom.html">smfi_chgfrom</a>.
|
||||
## Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">smfi_chgfrom</a>.
|
||||
def chgfrom(self,sender,param=None): pass
|
||||
## 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.
|
||||
# It may only be called from the negotiate callback.
|
||||
# The protocol stages are
|
||||
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
|
||||
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>.
|
||||
# @param stage protocol stage in which the macro list should be used
|
||||
# @param macrolist a space separated list of macro names
|
||||
def setsymlist(self,stage,macrolist): pass
|
||||
def setsmlist(self,stage,macrolist): pass
|
||||
|
||||
class error(Exception): pass
|
||||
|
||||
## Enable optional %milter actions.
|
||||
# Certain %milter actions need to be enabled before calling main()
|
||||
## Enable optional milter actions.
|
||||
# Certain milter actions need to be enabled before calling main()
|
||||
# or they throw an exception. Pymilter enables them all by
|
||||
# default (since 0.9.2), but you may wish to disable unneeded
|
||||
# actions as an optimization.
|
||||
@@ -138,72 +77,54 @@ def set_abort_callback(cb): pass
|
||||
def set_close_callback(cb): pass
|
||||
|
||||
## Sets the return code for untrapped Python exceptions during a callback.
|
||||
# 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
|
||||
# Must be one of TEMPFAIL,REJECT,CONTINUE
|
||||
def set_exception_policy(code): pass
|
||||
|
||||
## Register python %milter with libmilter.
|
||||
# The name we pass is used to identify the %milter in the MTA configuration.
|
||||
## Register python milter with libmilter.
|
||||
# The name we pass is used to identify the milter in the MTA configuration.
|
||||
# Callback functions must be set using the set_*_callback() functions before
|
||||
# registering the %milter.
|
||||
# registering the milter.
|
||||
# Three additional callbacks are specified as keyword parameters. These
|
||||
# were added by recent versions of libmilter. The keyword parameters is
|
||||
# a nicer way to do it, I think, since it makes clear that you have to do
|
||||
# it before registering. I may move all the callbacks in the future (perhaps
|
||||
# keeping the set functions for compatibility). Note that Milter.Base
|
||||
# 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
|
||||
# it before registering. I may move all the callbacks
|
||||
# in the future (perhaps keeping the set functions for compatibility).
|
||||
# @param name the milter name by which the MTA finds us
|
||||
# @param negotiate the
|
||||
# <a href="milter_api/xxfi_negotiate.html">
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
|
||||
# xxfi_negotiate</a> callback, called to negotiate supported
|
||||
# actions, callbacks, and protocol steps.
|
||||
# @param unknown the
|
||||
# <a href="milter_api/xxfi_unknown.html">
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_unknown">
|
||||
# xxfi_unknown</a> callback, called when for SMTP commands
|
||||
# not recognized by the MTA. (Extend SMTP in your milter!)
|
||||
# @param data the
|
||||
# <a href="milter_api/xxfi_data.html">
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_data">
|
||||
# xxfi_data</a> callback, called when the DATA
|
||||
# SMTP command is received.
|
||||
def register(name,negotiate=None,unknown=None,data=None): pass
|
||||
|
||||
## Attempt to create the socket used to communicate with the MTA.
|
||||
# milter.opensocket() attempts to create the socket specified previously by a
|
||||
# call to milter.setconn() which will be the interface between MTAs and the
|
||||
# %milter. This allows the calling application to ensure that the socket can be
|
||||
# created. If this is not called, milter.main() will do so implicitly.
|
||||
# Calls <a href="milter_api/smfi_opensocket.html">
|
||||
# smfi_opensocket</a>. While not documented for libmilter, my experiments
|
||||
# indicate that you must call register() before calling opensocket().
|
||||
# @param rmsock Try to remove an existing unix domain socket if true.
|
||||
def opensocket(rmsock): pass
|
||||
|
||||
## Transfer control to libmilter.
|
||||
# Calls <a href="milter_api/smfi_main.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_main">
|
||||
# smfi_main</a>.
|
||||
def main(): pass
|
||||
|
||||
## Set the libmilter debugging level.
|
||||
# <a href="milter_api/smfi_setdbg.html">smfi_setdbg</a>
|
||||
# sets the %milter library's internal debugging level to a new level
|
||||
# <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
|
||||
# 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
|
||||
# current, highest, useful value. Must be called before calling main().
|
||||
def setdbg(lev): pass
|
||||
|
||||
## Set timeout for MTA communication.
|
||||
# Calls <a href="milter_api/smfi_settimeout.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_settimeout">
|
||||
# smfi_settimeout</a>. Must be called before calling main().
|
||||
def settimeout(secs): pass
|
||||
|
||||
## Set socket backlog.
|
||||
# Calls <a href="milter_api/smfi_setbacklog.html">
|
||||
# Calls <a href="https://www.milter.org/developers/api/smfi_setbacklog">
|
||||
# smfi_setbacklog</a>. Must be called before calling main().
|
||||
def setbacklog(n): pass
|
||||
|
||||
@@ -212,21 +133,16 @@ def setbacklog(n): pass
|
||||
# unix, inet, or inet6 socket. By default, a unix domain socket
|
||||
# is used. It must not exist,
|
||||
# and sendmail will throw warnings if, eg, the file is under a
|
||||
# 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
|
||||
# group or world writable directory.
|
||||
# <pre>
|
||||
# milter.setconn('unix:/var/run/pythonfilter') # a named pipe
|
||||
# milter.setconn('local:/var/run/pythonfilter') # a named pipe
|
||||
# milter.setconn('inet:8800') # listen on ANY interface
|
||||
# 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
|
||||
# setconn('unix:/var/run/pythonfilter')
|
||||
# setconn('inet:8800') # listen on ANY interface
|
||||
# setconn('inet:7871@@publichost') # listen on a specific interface
|
||||
# setconn('inet6:8020')
|
||||
# </pre>
|
||||
def setconn(s): pass
|
||||
|
||||
## Stop the %milter gracefully.
|
||||
## Stop the milter gracefully.
|
||||
def stop(): pass
|
||||
|
||||
## Retrieve diagnostic info.
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
web:
|
||||
doxygen
|
||||
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-milter-devel doc/html/milter_api
|
||||
rsync -ravKk doc/html/ pymilter.org:/var/www/html/milter/pymilter
|
||||
cd doc/html; zip -r ../../doc .
|
||||
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
|
||||
|
||||
VERSION=1.0.6
|
||||
VERSION=0.9.6
|
||||
CVSTAG=pymilter-0_9_6
|
||||
PKG=pymilter-$(VERSION)
|
||||
SRCTAR=$(PKG).tar.gz
|
||||
|
||||
$(SRCTAR):
|
||||
git archive --format=tar.gz --prefix=$(PKG)/ -o $(SRCTAR) $(PKG)
|
||||
cvs export -r$(CVSTAG) -d $(PKG) pymilter
|
||||
tar cvfz $(PKG).tar.gz $(PKG)
|
||||
rm -r $(PKG)
|
||||
|
||||
# add extra copy of name like github so annoyingly does...
|
||||
github:
|
||||
git archive --format=tar.gz --prefix=pymilter-$(PKG)/ -o $(SRCTAR) $(PKG)
|
||||
|
||||
gittar: $(SRCTAR)
|
||||
cvstar: $(SRCTAR)
|
||||
|
||||
+4
-5
@@ -1,8 +1,7 @@
|
||||
## A very simple sample milter to prevent mixing of internal and external mail.
|
||||
## 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.
|
||||
|
||||
from __future__ import print_function
|
||||
import Milter
|
||||
import time
|
||||
import sys
|
||||
@@ -14,7 +13,7 @@ internal_tlds = ["corp", "personal"]
|
||||
# True if internal, False otherwise
|
||||
def is_internal(hostname):
|
||||
components = hostname.split(".")
|
||||
return components.pop() in internal_tlds
|
||||
return components.pop() in internal_tlds:
|
||||
|
||||
# Determine if internal and external hosts are mixed based on a list
|
||||
# of hostnames
|
||||
@@ -69,12 +68,12 @@ def main():
|
||||
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'))
|
||||
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'))
|
||||
print "%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
## To roll your own milter, create a class that extends Milter.
|
||||
# This is a useless example to show basic features of Milter.
|
||||
# See the pymilter project at https://pymilter.org based
|
||||
# on Sendmail's milter API
|
||||
# See the pymilter project at http://bmsi.com/python/milter.html
|
||||
# based on Sendmail's milter API http://www.milter.org/milter_api/api.html
|
||||
# This code is open-source on the same terms as Python.
|
||||
|
||||
## Milter calls methods of your class at milter events.
|
||||
## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
|
||||
## You can also add/del recipients, replacebody, add/del headers, etc.
|
||||
|
||||
from __future__ import print_function
|
||||
import Milter
|
||||
try:
|
||||
from StringIO import StringIO as BytesIO
|
||||
except:
|
||||
from io import BytesIO
|
||||
import StringIO
|
||||
import time
|
||||
import email
|
||||
from email import message_from_binary_file
|
||||
from email import policy
|
||||
import mimetypes
|
||||
import os
|
||||
import sys
|
||||
from socket import AF_INET, AF_INET6
|
||||
from Milter.utils import parse_addr
|
||||
if True:
|
||||
# for logging process - usually not needed
|
||||
from multiprocessing import Process as Thread, Queue
|
||||
else:
|
||||
from threading import Thread
|
||||
from Queue import Queue
|
||||
|
||||
logq = None
|
||||
logq = Queue(maxsize=4)
|
||||
|
||||
class myMilter(Milter.Base):
|
||||
|
||||
@@ -81,13 +71,9 @@ class myMilter(Milter.Base):
|
||||
self.fromparms = Milter.dictfromlist(str) # ESMTP parms
|
||||
self.user = self.getsymval('{auth_authen}') # authenticated user
|
||||
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 = BytesIO()
|
||||
self.fp = StringIO.StringIO()
|
||||
self.canon_from = '@'.join(parse_addr(mailfrom))
|
||||
self.fp.write(b'From %s %s\n' % (self.canon_from.encode(),
|
||||
time.ctime().encode()))
|
||||
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
|
||||
return Milter.CONTINUE
|
||||
|
||||
|
||||
@@ -102,12 +88,12 @@ class myMilter(Milter.Base):
|
||||
|
||||
@Milter.noreply
|
||||
def header(self, name, hval):
|
||||
self.fp.write(b'%s: %s\n' % (name.encode(),hval.encode())) # add header to buffer
|
||||
self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
|
||||
return Milter.CONTINUE
|
||||
|
||||
@Milter.noreply
|
||||
def eoh(self):
|
||||
self.fp.write(b'\n') # terminate headers
|
||||
self.fp.write("\n") # terminate headers
|
||||
return Milter.CONTINUE
|
||||
|
||||
@Milter.noreply
|
||||
@@ -117,16 +103,8 @@ class myMilter(Milter.Base):
|
||||
|
||||
def eom(self):
|
||||
self.fp.seek(0)
|
||||
msg = email.message_from_binary_file(self.fp, policy=policy.default)
|
||||
|
||||
#example on how to iterate through attachments
|
||||
for attachment in msg.iter_attachments():
|
||||
#attachment holds the attachment object so that it can be used with a new MIMEMultipart() message
|
||||
self.log("Attachment filename is %s" % (attachment.get_filename(),))
|
||||
self.log("Attachment content/type is %s" % (attachment.get_content_type(),))
|
||||
data = attachment.get_content()
|
||||
self.log("Attachment content is %s" % (data,))
|
||||
|
||||
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()
|
||||
# example of adding a Bcc:
|
||||
self.addrcpt('<%s>' % 'spy@example.com')
|
||||
@@ -144,35 +122,24 @@ class myMilter(Milter.Base):
|
||||
## === Support Functions ===
|
||||
|
||||
def log(self,*msg):
|
||||
t = (msg,self.id,time.time())
|
||||
if logq:
|
||||
logq.put(t)
|
||||
else:
|
||||
# logmsg(*t)
|
||||
pass
|
||||
|
||||
def logmsg(msg,id,ts):
|
||||
print("%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
|
||||
end=None)
|
||||
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
|
||||
for i in msg: print(i,end=None)
|
||||
print()
|
||||
sys.stdout.flush()
|
||||
logq.put((msg,self.id,time.time()))
|
||||
|
||||
def background():
|
||||
while True:
|
||||
t = logq.get()
|
||||
if not t: break
|
||||
logmsg(*t)
|
||||
msg,id,ts = t
|
||||
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
|
||||
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
|
||||
for i in msg: print i,
|
||||
print
|
||||
|
||||
## ===
|
||||
|
||||
def main():
|
||||
bt = Thread(target=background)
|
||||
bt.start()
|
||||
# This is NOT a good socket location for production, it is for
|
||||
# playing around. I suggest /var/run/milter/myappnamesock for production.
|
||||
socketname = os.path.expanduser('~/pythonsock')
|
||||
socketname = "/home/stuart/pythonsock"
|
||||
timeout = 600
|
||||
# Register to have the Milter factory create instances of your class:
|
||||
Milter.factory = myMilter
|
||||
@@ -180,15 +147,12 @@ def main():
|
||||
flags += Milter.ADDRCPT
|
||||
flags += Milter.DELRCPT
|
||||
Milter.set_flags(flags) # tell Sendmail which features we use
|
||||
print("%s milter startup" % time.strftime('%Y%b%d %H:%M:%S'))
|
||||
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
|
||||
sys.stdout.flush()
|
||||
Milter.runmilter("pythonfilter",socketname,timeout)
|
||||
logq.put(None)
|
||||
bt.join()
|
||||
print("%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S'))
|
||||
print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
|
||||
|
||||
if __name__ == "__main__":
|
||||
# You probably do not need a logging process, but if you do, this
|
||||
# is one way to do it.
|
||||
logq = Queue(maxsize=4)
|
||||
main()
|
||||
+266
-173
@@ -1,6 +1,6 @@
|
||||
/* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org)
|
||||
* Portions Copyright (C) 2001,2002,2003,2004,2005,2006,2007
|
||||
* Stuart Gathman (stuart@gathman.org)
|
||||
* Stuart Gathman (stuart@bmsi.com)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
@@ -34,6 +34,201 @@ $ python setup.py help
|
||||
|
||||
libraries=["milter","smutil","resolv"]
|
||||
|
||||
* $Log$
|
||||
* Revision 1.30 2012/04/12 23:08:06 customdesigned
|
||||
* Support RFC2553 on BSD
|
||||
*
|
||||
* Revision 1.29 2011/06/09 15:45:27 customdesigned
|
||||
* Print callback name for non-int return error.
|
||||
*
|
||||
* Revision 1.28 2011/06/08 23:13:48 customdesigned
|
||||
* Generate special exception when callback return not int.
|
||||
*
|
||||
* Revision 1.27 2009/07/28 21:45:54 customdesigned
|
||||
* Add getversion() to return runtime version.
|
||||
*
|
||||
* Revision 1.26 2009/07/28 21:08:20 customdesigned
|
||||
* Increment del count.
|
||||
*
|
||||
* Revision 1.25 2009/07/28 20:58:55 customdesigned
|
||||
* getdiag method
|
||||
*
|
||||
* Revision 1.24 2009/06/09 01:54:44 customdesigned
|
||||
* Forgot to initialize optional parameter.
|
||||
*
|
||||
* Revision 1.23 2009/05/29 20:44:58 customdesigned
|
||||
* Typo SMFIP_NO constants.
|
||||
*
|
||||
* Revision 1.22 2009/05/29 19:53:36 customdesigned
|
||||
* Typo SMFIS_ALL_OPTS
|
||||
*
|
||||
* Revision 1.21 2009/05/29 19:49:40 customdesigned
|
||||
* Typo calling helo instead of negotiate.
|
||||
*
|
||||
* Revision 1.20 2009/05/29 18:25:59 customdesigned
|
||||
* Null terminate keyword list.
|
||||
*
|
||||
* Revision 1.19 2009/05/28 18:36:42 customdesigned
|
||||
* Support new callbacks, including negotiate
|
||||
*
|
||||
* Revision 1.18 2009/05/21 21:53:05 customdesigned
|
||||
* First cut at support unknown, data, negotiate callbacks.
|
||||
*
|
||||
* Revision 1.17 2009/02/06 04:28:08 customdesigned
|
||||
* Oops! Missing options argument pointer for addrcpt.
|
||||
*
|
||||
* Revision 1.16 2008/12/16 04:21:05 customdesigned
|
||||
* Fedora release
|
||||
*
|
||||
* Revision 1.15 2008/12/13 20:29:56 customdesigned
|
||||
* Split off milter applications.
|
||||
*
|
||||
* Revision 1.14 2008/12/04 19:43:00 customdesigned
|
||||
* Doc updates.
|
||||
*
|
||||
* Revision 1.13 2008/11/23 03:06:47 customdesigned
|
||||
* Milter support for chgfrom.
|
||||
*
|
||||
* Revision 1.12 2008/11/21 20:42:52 customdesigned
|
||||
* Support smfi_chgfrom and smfi_addrcpt_par.
|
||||
*
|
||||
* Revision 1.11 2007/09/25 02:26:29 customdesigned
|
||||
* Update license.
|
||||
*
|
||||
* Revision 1.10 2006/02/12 02:00:42 customdesigned
|
||||
* Resolve FIXME for wrap_close.
|
||||
*
|
||||
* Revision 1.9 2005/12/23 21:46:36 customdesigned
|
||||
* Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER)
|
||||
*
|
||||
* Revision 1.8 2005/10/20 23:23:36 customdesigned
|
||||
* Include smfi_progress is SMFIR_PROGRESS defined
|
||||
*
|
||||
* Revision 1.7 2005/10/20 23:04:46 customdesigned
|
||||
* Add optional idx for position of added header.
|
||||
*
|
||||
* Revision 1.6 2005/07/15 22:18:17 customdesigned
|
||||
* Support callback exception policy
|
||||
*
|
||||
* Revision 1.5 2005/06/24 04:20:07 customdesigned
|
||||
* Report context allocation error.
|
||||
*
|
||||
* Revision 1.4 2005/06/24 04:12:43 customdesigned
|
||||
* Remove unused name argument to generic wrappers.
|
||||
*
|
||||
* Revision 1.3 2005/06/24 03:57:35 customdesigned
|
||||
* Handle close called before connect.
|
||||
*
|
||||
* Revision 1.2 2005/06/02 04:18:55 customdesigned
|
||||
* Update copyright notices after reading article on /.
|
||||
*
|
||||
* Revision 1.1.1.2 2005/05/31 18:09:06 customdesigned
|
||||
* Release 0.7.1
|
||||
*
|
||||
* Revision 2.31 2004/08/23 02:24:36 stuart
|
||||
* Support setbacklog
|
||||
*
|
||||
* Revision 2.30 2004/08/21 20:29:53 stuart
|
||||
* Support option of 11 lines max for mlreply.
|
||||
*
|
||||
* Revision 2.29 2004/08/21 04:14:29 stuart
|
||||
* mlreply support
|
||||
*
|
||||
* Revision 2.28 2004/08/21 02:45:21 stuart
|
||||
* Don't leak int constants if module unloaded.
|
||||
*
|
||||
* Revision 2.27 2004/04/06 03:19:59 stuart
|
||||
* Release 0.6.8
|
||||
*
|
||||
* Revision 2.26 2004/03/04 21:43:06 stuart
|
||||
* Fix memory leak by removing unused dynamic template buffer,
|
||||
* thanks again to Alexander Kourakos.
|
||||
*
|
||||
* Revision 2.25 2004/03/01 19:45:03 stuart
|
||||
* Release 0.6.5
|
||||
*
|
||||
* Revision 2.24 2004/03/01 18:56:50 stuart
|
||||
* Support progress reporting.
|
||||
*
|
||||
* Revision 2.23 2004/03/01 18:36:09 stuart
|
||||
* Plug memory leak. Thanks to Alexander Kourakos.
|
||||
*
|
||||
* Revision 2.22 2003/11/02 03:01:46 stuart
|
||||
* Adjust SMTP error codes after careful reading of standard.
|
||||
*
|
||||
* Revision 2.21 2003/06/24 19:57:04 stuart
|
||||
* Allow removing a python milter callback by setting to None.
|
||||
*
|
||||
* Revision 2.20 2003/02/13 17:08:57 stuart
|
||||
* IPV6 support
|
||||
*
|
||||
* Revision 2.19 2003/02/13 16:58:29 stuart
|
||||
* Support passing None to setreply and chgheader.
|
||||
*
|
||||
* Revision 2.18 2002/12/11 16:44:06 stuart
|
||||
* Support QUARANTINE if supported by libmilter.
|
||||
*
|
||||
* Revision 2.17 2002/04/18 20:20:35 stuart
|
||||
* Fix for NULL hostaddr in connect callback from Jason Erickson.
|
||||
*
|
||||
* Revision 2.16 2001/09/26 13:29:09 stuart
|
||||
* sa_len not supported by linux.
|
||||
*
|
||||
* Revision 2.15 2001/09/25 17:28:40 stuart
|
||||
* Copyrights, documentation, release 0.3.1
|
||||
*
|
||||
* Revision 2.14 2001/09/25 00:36:57 stuart
|
||||
* Pass hostaddr to python code in format used by standard socket module.
|
||||
*
|
||||
* Revision 2.13 2001/09/24 23:44:55 stuart
|
||||
* Return old callback from setcallback functions.
|
||||
*
|
||||
* Revision 2.12 2001/09/24 20:02:30 stuart
|
||||
* Remove redundant setpriv
|
||||
*
|
||||
* Revision 2.11 2001/09/23 22:26:35 stuart
|
||||
* Update docs. Streamline Milter.py
|
||||
* update testbms.py to reflect actual sendmail behaviour with multiple
|
||||
* messages per connection.
|
||||
*
|
||||
* Revision 2.10 2001/09/22 15:33:42 stuart
|
||||
* More doc comment updates.
|
||||
*
|
||||
* Revision 2.9 2001/09/22 14:52:27 stuart
|
||||
* Actually return retval in _generic_return.
|
||||
* Go over doc comments.
|
||||
*
|
||||
* Revision 2.8 2001/09/22 01:59:32 stuart
|
||||
* Prevent reentrant call of milter_main, which libmilter doesn't support.
|
||||
*
|
||||
* Revision 2.7 2001/09/22 01:47:37 stuart
|
||||
* Forgot to set milter interp.
|
||||
*
|
||||
* Revision 2.6 2001/09/22 01:23:53 stuart
|
||||
* Added proper threading after research in python docs.
|
||||
*
|
||||
* Revision 2.5 2001/09/21 20:08:51 stuart
|
||||
* Release 0.2.3
|
||||
*
|
||||
* Revision 2.4 2001/09/20 16:18:16 stuart
|
||||
* libmilter checks in_eom state, so we don't have to.
|
||||
*
|
||||
* Revision 2.3 2001/09/19 06:02:33 stuart
|
||||
* Make more stuff static.
|
||||
*
|
||||
* Revision 2.1 2001/09/19 04:24:13 stuart
|
||||
* Use extension type to track context in python.
|
||||
*
|
||||
* Revision 1.4 2001/09/18 18:48:28 stuart
|
||||
* clear private data reference in _clear_context
|
||||
*
|
||||
* Revision 1.3 2001/09/15 04:19:37 stuart
|
||||
* nasty off by 1 mem overwrite bugs in wrap_env
|
||||
* generic_set_callback
|
||||
*
|
||||
* Revision 1.2 2001/09/15 03:15:39 stuart
|
||||
* several bugs fixed, works smoothly
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MAX_ML_REPLY
|
||||
@@ -43,7 +238,6 @@ $ python setup.py help
|
||||
#error MAX_ML_REPLY must be 1 or 11 or 32
|
||||
#endif
|
||||
#define _FFR_MULTILINE (MAX_ML_REPLY > 1)
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
|
||||
//#include <pthread.h> // shouldn't be needed - use Python API
|
||||
#include <Python.h> // Python C API
|
||||
@@ -72,7 +266,7 @@ $ python setup.py help
|
||||
* published. Unfortunately I know of no good way to do this
|
||||
* other than with OS-specific tests.
|
||||
*/
|
||||
#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__linux__) || defined(__sun__) || defined(__GLIBC__) || (defined(__APPLE__) && defined(__MACH__))
|
||||
#if defined(__FreeBSD_kernel__) || defined(__linux__)
|
||||
#define HAVE_IPV6_RFC2553
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
@@ -133,13 +327,7 @@ static struct MilterCallback {
|
||||
{ NULL , NULL }
|
||||
};
|
||||
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
static struct smfiDesc description; /* forward declaration */
|
||||
static PyTypeObject milter_ContextType;
|
||||
#else
|
||||
staticforward struct smfiDesc description; /* forward declaration */
|
||||
staticforward PyTypeObject milter_ContextType;
|
||||
#endif
|
||||
staticforward struct smfiDesc description; /* forward declaration */
|
||||
|
||||
static PyObject *MilterError;
|
||||
/* The interpreter instance that called milter.main */
|
||||
@@ -151,6 +339,8 @@ typedef struct {
|
||||
|
||||
static milter_Diag diag;
|
||||
|
||||
staticforward PyTypeObject milter_ContextType;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
SMFICTX *ctx; /* libmilter thread state */
|
||||
@@ -246,12 +436,12 @@ _thread_return(PyThreadState *t,int val,char *errstr) {
|
||||
return _generic_return(val,errstr);
|
||||
}
|
||||
|
||||
static const char milter_set_flags__doc__[] =
|
||||
static char milter_set_flags__doc__[] =
|
||||
"set_flags(int) -> None\n\
|
||||
Set flags for filter capabilities; OR of one or more of:\n\
|
||||
ADDHDRS - filter may add headers\n\
|
||||
CHGBODY - filter may replace body\n\
|
||||
CHGFROM - filter may replace sender\n\
|
||||
CHGFROM - filter may replace body\n\
|
||||
ADDRCPT - filter may add recipients\n\
|
||||
DELRCPT - filter may delete recipients\n\
|
||||
CHGHDRS - filter may change/delete headers";
|
||||
@@ -287,7 +477,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
static const char milter_set_connect_callback__doc__[] =
|
||||
static char milter_set_connect_callback__doc__[] =
|
||||
"set_connect_callback(Function) -> None\n\
|
||||
Sets the Python function invoked when a connection is made to sendmail.\n\
|
||||
Function takes args (ctx, hostname, integer, hostaddr) -> int\n\
|
||||
@@ -314,7 +504,7 @@ milter_set_connect_callback(PyObject *self, PyObject *args) {
|
||||
"O:set_connect_callback", &connect_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_helo_callback__doc__[] =
|
||||
static char milter_set_helo_callback__doc__[] =
|
||||
"set_helo_callback(Function) -> None\n\
|
||||
Sets the Python function invoked upon SMTP HELO.\n\
|
||||
Function takes args (ctx, hostname) -> int\n\
|
||||
@@ -325,7 +515,7 @@ milter_set_helo_callback(PyObject *self, PyObject *args) {
|
||||
return generic_set_callback(args, "O:set_helo_callback", &helo_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_envfrom_callback__doc__[] =
|
||||
static char milter_set_envfrom_callback__doc__[] =
|
||||
"set_envfrom_callback(Function) -> None\n\
|
||||
Sets the Python function invoked on envelope from.\n\
|
||||
Function takes args (ctx, from, *str) -> int\n\
|
||||
@@ -338,7 +528,7 @@ milter_set_envfrom_callback(PyObject *self, PyObject *args) {
|
||||
&envfrom_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_envrcpt_callback__doc__[] =
|
||||
static char milter_set_envrcpt_callback__doc__[] =
|
||||
"set_envrcpt_callback(Function) -> None\n\
|
||||
Sets the Python function invoked on each envelope recipient.\n\
|
||||
Function takes args (ctx, rcpt, *str) -> int\n\
|
||||
@@ -351,7 +541,7 @@ milter_set_envrcpt_callback(PyObject *self, PyObject *args) {
|
||||
&envrcpt_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_header_callback__doc__[] =
|
||||
static char milter_set_header_callback__doc__[] =
|
||||
"set_header_callback(Function) -> None\n\
|
||||
Sets the Python function invoked on each message header.\n\
|
||||
Function takes args (ctx, field, value) ->int\n\
|
||||
@@ -364,7 +554,7 @@ milter_set_header_callback(PyObject *self, PyObject *args) {
|
||||
&header_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_eoh_callback__doc__[] =
|
||||
static char milter_set_eoh_callback__doc__[] =
|
||||
"set_eoh_callback(Function) -> None\n\
|
||||
Sets the Python function invoked at end of header.\n\
|
||||
Function takes args (ctx) -> int";
|
||||
@@ -374,20 +564,20 @@ milter_set_eoh_callback(PyObject *self, PyObject *args) {
|
||||
return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_body_callback__doc__[] =
|
||||
static char milter_set_body_callback__doc__[] =
|
||||
"set_body_callback(Function) -> None\n\
|
||||
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\
|
||||
represented as received from SMTP (normally Carriage-Return/Line-Feed).\n\
|
||||
Function takes args (ctx, chunk) -> int\n\
|
||||
chunk -> bytes - body data";
|
||||
chunk -> String - body data";
|
||||
|
||||
static PyObject *
|
||||
milter_set_body_callback(PyObject *self, PyObject *args) {
|
||||
return generic_set_callback(args, "O:set_body_callback", &body_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_eom_callback__doc__[] =
|
||||
static char milter_set_eom_callback__doc__[] =
|
||||
"set_eom_callback(Function) -> None\n\
|
||||
Sets the Python function invoked at end of message.\n\
|
||||
This routine is the only place where special operations\n\
|
||||
@@ -400,7 +590,7 @@ milter_set_eom_callback(PyObject *self, PyObject *args) {
|
||||
return generic_set_callback(args, "O:set_eom_callback", &eom_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_abort_callback__doc__[] =
|
||||
static char milter_set_abort_callback__doc__[] =
|
||||
"set_abort_callback(Function) -> None\n\
|
||||
Sets the Python function invoked if message is aborted\n\
|
||||
outside of the control of the filter, for example,\n\
|
||||
@@ -414,7 +604,7 @@ milter_set_abort_callback(PyObject *self, PyObject *args) {
|
||||
return generic_set_callback(args, "O:set_abort_callback", &abort_callback);
|
||||
}
|
||||
|
||||
static const char milter_set_close_callback__doc__[] =
|
||||
static char milter_set_close_callback__doc__[] =
|
||||
"set_close_callback(Function) -> None\n\
|
||||
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\
|
||||
@@ -427,7 +617,7 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
|
||||
|
||||
static int exception_policy = SMFIS_TEMPFAIL;
|
||||
|
||||
static const char milter_set_exception_policy__doc__[] =
|
||||
static char milter_set_exception_policy__doc__[] =
|
||||
"set_exception_policy(i) -> None\n\
|
||||
Sets the policy for untrapped Python exceptions during a callback.\n\
|
||||
Must be one of TEMPFAIL,REJECT,CONTINUE";
|
||||
@@ -438,8 +628,7 @@ milter_set_exception_policy(PyObject *self, PyObject *args) {
|
||||
if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i))
|
||||
return NULL;
|
||||
switch (i) {
|
||||
case SMFIS_REJECT: case SMFIS_TEMPFAIL:
|
||||
case SMFIS_CONTINUE: case SMFIS_ACCEPT:
|
||||
case SMFIS_REJECT: case SMFIS_TEMPFAIL: case SMFIS_CONTINUE:
|
||||
exception_policy = i;
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
@@ -454,30 +643,23 @@ _release_thread(PyThreadState *t) {
|
||||
PyEval_ReleaseThread(t);
|
||||
}
|
||||
|
||||
|
||||
/** Report and clear any python exception before returning to libmilter.
|
||||
The interpreter is locked when we are called, and we unlock it. */
|
||||
static int _report_exception(milter_ContextObject *self) {
|
||||
char untrapped_msg[80];
|
||||
if (PyErr_Occurred()) {
|
||||
sprintf(untrapped_msg,"pymilter: untrapped exception in %.40s",
|
||||
description.xxfi_name);
|
||||
PyErr_Print();
|
||||
PyErr_Clear(); /* must clear since not returning to python */
|
||||
_release_thread(self->t);
|
||||
switch (exception_policy) {
|
||||
case SMFIS_REJECT:
|
||||
smfi_setreply(self->ctx, "554", "5.3.0", untrapped_msg);
|
||||
smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure");
|
||||
return SMFIS_REJECT;
|
||||
case SMFIS_TEMPFAIL:
|
||||
smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg);
|
||||
smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure");
|
||||
return SMFIS_TEMPFAIL;
|
||||
}
|
||||
return exception_policy;
|
||||
return SMFIS_CONTINUE;
|
||||
}
|
||||
/* This should never happen, _report_exception is only called when
|
||||
* the caller has already detected a python exception. If it
|
||||
* does somehow happen, pretend nothing is wrong... */
|
||||
_release_thread(self->t);
|
||||
return SMFIS_CONTINUE;
|
||||
}
|
||||
@@ -491,14 +673,10 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
|
||||
int retval;
|
||||
|
||||
if (arglist == NULL) return _report_exception(self);
|
||||
result = PyObject_CallObject(cb, arglist);
|
||||
result = PyEval_CallObject(cb, arglist);
|
||||
Py_DECREF(arglist);
|
||||
if (result == NULL) return _report_exception(self);
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
if (!PyLong_Check(result)) {
|
||||
#else
|
||||
if (!PyInt_Check(result)) {
|
||||
#endif
|
||||
const struct MilterCallback *p;
|
||||
const char *cbname = "milter";
|
||||
char buf[40];
|
||||
@@ -513,11 +691,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
|
||||
PyErr_SetString(MilterError,buf);
|
||||
return _report_exception(self);
|
||||
}
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
retval = PyLong_AS_LONG(result);
|
||||
#else
|
||||
retval = PyInt_AS_LONG(result);
|
||||
#endif
|
||||
Py_DECREF(result);
|
||||
_release_thread(self->t);
|
||||
return retval;
|
||||
@@ -534,11 +708,7 @@ makeipaddr(struct sockaddr_in *addr) {
|
||||
sprintf(buf, "%d.%d.%d.%d",
|
||||
(int) (x>>24) & 0xff, (int) (x>>16) & 0xff,
|
||||
(int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff);
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
return PyUnicode_FromString(buf);
|
||||
#else
|
||||
return PyString_FromString(buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef HAVE_IPV6_SUPPORT
|
||||
@@ -546,13 +716,8 @@ static PyObject *
|
||||
makeip6addr(struct sockaddr_in6 *addr) {
|
||||
char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */
|
||||
const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf);
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
if (s) return PyUnicode_FromString(s);
|
||||
return PyUnicode_FromString("inet6:unknown");
|
||||
#else
|
||||
if (s) return PyString_FromString(s);
|
||||
return PyString_FromString("inet6:unknown");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -643,11 +808,7 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
|
||||
for (i=0;i<count;i++) {
|
||||
/* There's some error checking performed in do_mkvalue() for a string */
|
||||
/* that's not currently done here - it probably should be */
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
PyObject *o = PyBytes_FromStringAndSize(argv[i], strlen(argv[i]));
|
||||
#else
|
||||
PyObject *o = PyString_FromStringAndSize(argv[i], strlen(argv[i]));
|
||||
#endif
|
||||
if (o == NULL) { /* out of memory */
|
||||
Py_DECREF(arglist);
|
||||
return _report_exception(self);
|
||||
@@ -675,12 +836,7 @@ milter_wrap_header(SMFICTX *ctx, char *headerf, char *headerv) {
|
||||
if (header_callback == NULL) return SMFIS_CONTINUE;
|
||||
c = _get_context(ctx);
|
||||
if (!c) return SMFIS_TEMPFAIL;
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
/* pass val as bytes so Milter.Base.header_bytes can do surrogate escape. */
|
||||
arglist = Py_BuildValue("(Osy)", c, headerf, headerv);
|
||||
#else
|
||||
arglist = Py_BuildValue("(Oss)", c, headerf, headerv);
|
||||
#endif
|
||||
return _generic_wrapper(c, header_callback, arglist);
|
||||
}
|
||||
|
||||
@@ -709,11 +865,7 @@ milter_wrap_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) {
|
||||
c = _get_context(ctx);
|
||||
if (!c) return SMFIS_TEMPFAIL;
|
||||
/* Unclear whether this should be s#, z#, or t# */
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
arglist = Py_BuildValue("(Oy#)", c, bodyp, (Py_ssize_t)bodylen);
|
||||
#else
|
||||
arglist = Py_BuildValue("(Os#)", c, bodyp, (Py_ssize_t)bodylen);
|
||||
#endif
|
||||
arglist = Py_BuildValue("(Os#)", c, bodyp, bodylen);
|
||||
return _generic_wrapper(c, body_callback, arglist);
|
||||
}
|
||||
|
||||
@@ -774,17 +926,20 @@ milter_wrap_negotiate(SMFICTX *ctx,
|
||||
rc = _generic_wrapper(c, negotiate_callback, arglist);
|
||||
c->t = t;
|
||||
if (rc == SMFIS_CONTINUE) {
|
||||
#if 0 // PyArgs_Parse deprecated and going away
|
||||
if (!PyArgs_Parse(optlist,"[kkkk]",pf0,pf1,pf2,pf3)) {
|
||||
PyErr_Print();
|
||||
PyErr_Clear(); /* must clear since not returning to python */
|
||||
rc = SMFIS_REJECT;
|
||||
}
|
||||
#else
|
||||
unsigned long *pa[4] = { pf0,pf1,pf2,pf3 };
|
||||
unsigned long fa[4] = { f0,f1,f2,f3 };
|
||||
int len = PyList_Size(optlist);
|
||||
int i;
|
||||
for (i = 0; i < 4; ++i) {
|
||||
*pa[i] = (i <= len)
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
? PyLong_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
|
||||
#else
|
||||
? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
|
||||
#endif
|
||||
: fa[i];
|
||||
}
|
||||
if (PyErr_Occurred()) {
|
||||
@@ -792,6 +947,7 @@ milter_wrap_negotiate(SMFICTX *ctx,
|
||||
PyErr_Clear();
|
||||
rc = SMFIS_REJECT;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (rc != SMFIS_ALL_OPTS)
|
||||
rc = SMFIS_REJECT;
|
||||
@@ -831,7 +987,7 @@ milter_wrap_close(SMFICTX *ctx) {
|
||||
return r;
|
||||
}
|
||||
|
||||
static const char milter_register__doc__[] =
|
||||
static char milter_register__doc__[] =
|
||||
"register(name,unknown=,data=,negotiate=) -> None\n\
|
||||
Registers the milter name with current callbacks, and flags.\n\
|
||||
Required before main() is called.";
|
||||
@@ -876,7 +1032,7 @@ milter_register(PyObject *self, PyObject *args, PyObject *kwds) {
|
||||
return _generic_return(smfi_register(description), "cannot register");
|
||||
}
|
||||
|
||||
static const char milter_opensocket__doc__[] =
|
||||
static char milter_opensocket__doc__[] =
|
||||
"opensocket(rmsock) -> None\n\
|
||||
Attempts to create and open the socket provided with setconn.\n\
|
||||
Removes the socket first if rmsock is True.";
|
||||
@@ -889,7 +1045,7 @@ milter_opensocket(PyObject *self, PyObject *args) {
|
||||
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
|
||||
}
|
||||
|
||||
static const char milter_main__doc__[] =
|
||||
static char milter_main__doc__[] =
|
||||
"main() -> None\n\
|
||||
Main milter routine. Set any callbacks, and flags desired, then call\n\
|
||||
setconn(), then call register(name), and finally call main().";
|
||||
@@ -904,10 +1060,7 @@ milter_main(PyObject *self, PyObject *args) {
|
||||
return NULL;
|
||||
}
|
||||
/* libmilter requires thread support */
|
||||
#if PY_VERSION_HEX < 0x03070000
|
||||
/* called in Py_Initialize beginning with 3.7 */
|
||||
PyEval_InitThreads();
|
||||
#endif
|
||||
/* let other threads run while in smfi_main() */
|
||||
interp = PyThreadState_Get()->interp;
|
||||
_main = PyEval_SaveThread(); /* must be done before smfi_main() */
|
||||
@@ -916,7 +1069,7 @@ milter_main(PyObject *self, PyObject *args) {
|
||||
return o;
|
||||
}
|
||||
|
||||
static const char milter_setdbg__doc__[] =
|
||||
static char milter_setdbg__doc__[] =
|
||||
"setdbg(int) -> None\n\
|
||||
Sets debug level in sendmail/libmilter source. Dubious usefulness.";
|
||||
|
||||
@@ -927,7 +1080,7 @@ milter_setdbg(PyObject *self, PyObject *args) {
|
||||
return _generic_return(smfi_setdbg(val), "cannot set debug value");
|
||||
}
|
||||
|
||||
static const char milter_setbacklog__doc__[] =
|
||||
static char milter_setbacklog__doc__[] =
|
||||
"setbacklog(int) -> None\n\
|
||||
Set the TCP connection queue size for the milter socket.";
|
||||
|
||||
@@ -939,7 +1092,7 @@ milter_setbacklog(PyObject *self, PyObject *args) {
|
||||
return _generic_return(smfi_setbacklog(val), "cannot set backlog");
|
||||
}
|
||||
|
||||
static const char milter_settimeout__doc__[] =
|
||||
static char milter_settimeout__doc__[] =
|
||||
"settimeout(int) -> None\n\
|
||||
Set the time (in seconds) that sendmail will wait before\n\
|
||||
considering this filter dead.";
|
||||
@@ -952,7 +1105,7 @@ milter_settimeout(PyObject *self, PyObject *args) {
|
||||
return _generic_return(smfi_settimeout(val), "cannot set timeout");
|
||||
}
|
||||
|
||||
static const char milter_setconn__doc__[] =
|
||||
static char milter_setconn__doc__[] =
|
||||
"setconn(filename) -> None\n\
|
||||
Sets the pathname to the unix, inet, or inet6 socket that\n\
|
||||
sendmail will use to communicate with this filter. By default,\n\
|
||||
@@ -972,7 +1125,7 @@ milter_setconn(PyObject *self, PyObject *args) {
|
||||
return _generic_return(smfi_setconn(str), "cannot set connection");
|
||||
}
|
||||
|
||||
static const char milter_stop__doc__[] =
|
||||
static char milter_stop__doc__[] =
|
||||
"stop() -> None\n\
|
||||
This function appears to be a controlled method to tell sendmail to\n\
|
||||
stop using this filter. It will close the socket.";
|
||||
@@ -985,7 +1138,7 @@ milter_stop(PyObject *self, PyObject *args) {
|
||||
return _thread_return(t,smfi_stop(), "cannot stop");
|
||||
}
|
||||
|
||||
static const char milter_getdiag__doc__[] =
|
||||
static char milter_getdiag__doc__[] =
|
||||
"getdiag() -> tuple\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.";
|
||||
@@ -995,7 +1148,7 @@ milter_getdiag(PyObject *self, PyObject *args) {
|
||||
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
|
||||
}
|
||||
|
||||
static const char milter_getversion__doc__[] =
|
||||
static char milter_getversion__doc__[] =
|
||||
"getversion() -> tuple\n\
|
||||
Return runtime libmilter version as a tuple of major,minor,patchlevel.";
|
||||
static PyObject *
|
||||
@@ -1009,7 +1162,7 @@ milter_getversion(PyObject *self, PyObject *args) {
|
||||
return Py_BuildValue("(kkk)", major,minor,patch);
|
||||
}
|
||||
|
||||
static const char milter_getsymval__doc__[] =
|
||||
static char milter_getsymval__doc__[] =
|
||||
"getsymval(String) -> String\n\
|
||||
Returns a symbol's value. Context-dependent, and unclear from the dox.";
|
||||
|
||||
@@ -1024,7 +1177,7 @@ milter_getsymval(PyObject *self, PyObject *args) {
|
||||
return Py_BuildValue("s", smfi_getsymval(ctx, str));
|
||||
}
|
||||
|
||||
static const char milter_setreply__doc__[] =
|
||||
static char milter_setreply__doc__[] =
|
||||
"setreply(rcode, xcode, message) -> None\n\
|
||||
Sets the specific reply code to be used in response\n\
|
||||
to the active command.\n\
|
||||
@@ -1088,7 +1241,7 @@ milter_setreply(PyObject *self, PyObject *args) {
|
||||
"cannot set reply");
|
||||
}
|
||||
|
||||
static const char milter_addheader__doc__[] =
|
||||
static char milter_addheader__doc__[] =
|
||||
"addheader(field, value, idx=-1) -> None\n\
|
||||
Add a header to the message. This header is not passed to other\n\
|
||||
filters. It is not checked for standards compliance;\n\
|
||||
@@ -1125,7 +1278,7 @@ milter_addheader(PyObject *self, PyObject *args) {
|
||||
}
|
||||
|
||||
#ifdef SMFIF_CHGFROM
|
||||
static const char milter_chgfrom__doc__[] =
|
||||
static char milter_chgfrom__doc__[] =
|
||||
"chgfrom(sender,params) -> None\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\
|
||||
@@ -1138,8 +1291,6 @@ milter_chgfrom(PyObject *self, PyObject *args) {
|
||||
SMFICTX *ctx;
|
||||
PyThreadState *t;
|
||||
|
||||
/* FIXME: use s# to transition to allow passing bytes, but milter api
|
||||
* requires NUL terminated bytes. */
|
||||
if (!PyArg_ParseTuple(args, "s|z:chgfrom", &sender, ¶ms))
|
||||
return NULL;
|
||||
ctx = _find_context(self);
|
||||
@@ -1150,13 +1301,13 @@ milter_chgfrom(PyObject *self, PyObject *args) {
|
||||
}
|
||||
#endif
|
||||
|
||||
static const char milter_chgheader__doc__[] =
|
||||
static char milter_chgheader__doc__[] =
|
||||
"chgheader(field, int, value) -> None\n\
|
||||
Change/delete a header in the message. \n\
|
||||
It is not checked for standards compliance; the mail filter\n\
|
||||
must ensure that no protocols are violated as a result of adding this header.\n\
|
||||
field - header field name\n\
|
||||
int - the Nth occurrence of this header\n\
|
||||
int - the Nth occurence of this header\n\
|
||||
value - header field value\n\
|
||||
field and value are strings.\n\
|
||||
This function can only be called from the EOM callback.";
|
||||
@@ -1178,7 +1329,7 @@ milter_chgheader(PyObject *self, PyObject *args) {
|
||||
"cannot change header");
|
||||
}
|
||||
|
||||
static const char milter_addrcpt__doc__[] =
|
||||
static char milter_addrcpt__doc__[] =
|
||||
"addrcpt(string,params=None) -> None\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\
|
||||
@@ -1208,7 +1359,7 @@ milter_addrcpt(PyObject *self, PyObject *args) {
|
||||
return _thread_return(t,rc, "cannot add recipient");
|
||||
}
|
||||
|
||||
static const char milter_delrcpt__doc__[] =
|
||||
static char milter_delrcpt__doc__[] =
|
||||
"delrcpt(string) -> None\n\
|
||||
Delete a recipient from the envelope.\n\
|
||||
This function can only be called from the EOM callback.";
|
||||
@@ -1226,7 +1377,7 @@ milter_delrcpt(PyObject *self, PyObject *args) {
|
||||
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
|
||||
}
|
||||
|
||||
static const char milter_replacebody__doc__[] =
|
||||
static char milter_replacebody__doc__[] =
|
||||
"replacebody(string) -> None\n\
|
||||
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\
|
||||
@@ -1236,7 +1387,7 @@ can only be called from the EOM callback.";
|
||||
static PyObject *
|
||||
milter_replacebody(PyObject *self, PyObject *args) {
|
||||
char *bodyp;
|
||||
Py_ssize_t bodylen;
|
||||
int bodylen;
|
||||
SMFICTX *ctx;
|
||||
PyThreadState *t;
|
||||
|
||||
@@ -1245,10 +1396,10 @@ milter_replacebody(PyObject *self, PyObject *args) {
|
||||
if (ctx == NULL) return NULL;
|
||||
t = PyEval_SaveThread();
|
||||
return _thread_return(t,smfi_replacebody(ctx,
|
||||
(unsigned char *)bodyp, (int)bodylen), "cannot replace message body");
|
||||
(unsigned char *)bodyp, bodylen), "cannot replace message body");
|
||||
}
|
||||
|
||||
static const char milter_setpriv__doc__[] =
|
||||
static char milter_setpriv__doc__[] =
|
||||
"setpriv(object) -> object\n\
|
||||
Associates any Python object with this context, and returns\n\
|
||||
the old value or None. Use this to\n\
|
||||
@@ -1274,7 +1425,7 @@ milter_setpriv(PyObject *self, PyObject *args) {
|
||||
return old;
|
||||
}
|
||||
|
||||
static const char milter_getpriv__doc__[] =
|
||||
static char milter_getpriv__doc__[] =
|
||||
"getpriv() -> None\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\
|
||||
@@ -1292,7 +1443,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
|
||||
}
|
||||
|
||||
#ifdef SMFIF_QUARANTINE
|
||||
static const char milter_quarantine__doc__[] =
|
||||
static char milter_quarantine__doc__[] =
|
||||
"quarantine(string) -> None\n\
|
||||
Place the message in quarantine. A string with a description of the reason\n\
|
||||
is the only argument.";
|
||||
@@ -1313,7 +1464,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
|
||||
#endif
|
||||
|
||||
#ifdef SMFIR_PROGRESS
|
||||
static const char milter_progress__doc__[] =
|
||||
static char milter_progress__doc__[] =
|
||||
"progress() -> None\n\
|
||||
Notify the MTA that we are working on a message so it will reset timeouts.";
|
||||
|
||||
@@ -1330,23 +1481,23 @@ milter_progress(PyObject *self, PyObject *args) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SMFIF_SETSYMLIST
|
||||
static const char milter_setsymlist__doc__[] =
|
||||
"setsymlist(stage,macrolist) -> None\n\
|
||||
#ifdef SMFIF_SETSMLIST
|
||||
static char milter_setsmlist__doc__[] =
|
||||
"setsmlist(stage,macrolist) -> None\n\
|
||||
Tell the MTA which macro values we are interested in for a given stage";
|
||||
|
||||
static PyObject *
|
||||
milter_setsymlist(PyObject *self, PyObject *args) {
|
||||
milter_setsmlist(PyObject *self, PyObject *args) {
|
||||
SMFICTX *ctx;
|
||||
PyThreadState *t;
|
||||
int stage = 0;
|
||||
char *smlist = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "is:setsymlist",&stage, &smlist)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "is:setsmlist",&stage, &smlist)) return NULL;
|
||||
ctx = _find_context(self);
|
||||
if (ctx == NULL) return NULL;
|
||||
t = PyEval_SaveThread();
|
||||
return _thread_return(t,smfi_setsymlist(ctx,stage,smlist),
|
||||
return _thread_return(t,smfi_setsmlist(ctx,stage,smlist),
|
||||
"cannot set macro list");
|
||||
}
|
||||
#endif
|
||||
@@ -1370,18 +1521,16 @@ static PyMethodDef context_methods[] = {
|
||||
#ifdef SMFIF_CHGFROM
|
||||
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
|
||||
#endif
|
||||
#ifdef SMFIF_SETSYMLIST
|
||||
{ "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__doc__},
|
||||
#ifdef SMFIF_SETSMLIST
|
||||
{ "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__doc__},
|
||||
#endif
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
#if PY_MAJOR_VERSION < 3
|
||||
static PyObject *
|
||||
milter_Context_getattr(PyObject *self, char *name) {
|
||||
return Py_FindMethod(context_methods, self, name);
|
||||
}
|
||||
#endif
|
||||
|
||||
static struct smfiDesc description = { /* Set some reasonable defaults */
|
||||
"pythonfilter",
|
||||
@@ -1431,23 +1580,14 @@ static PyMethodDef milter_methods[] = {
|
||||
};
|
||||
|
||||
static PyTypeObject milter_ContextType = {
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
PyVarObject_HEAD_INIT(&PyType_Type,0)
|
||||
"milter.Context",
|
||||
#else
|
||||
PyObject_HEAD_INIT(&PyType_Type)
|
||||
0,
|
||||
"milterContext",
|
||||
#endif
|
||||
sizeof(milter_ContextObject),
|
||||
0,
|
||||
milter_Context_dealloc, /* tp_dealloc */
|
||||
0, /* tp_print */
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
0, /* tp_getattr */
|
||||
#else
|
||||
milter_Context_getattr, /* tp_getattr */
|
||||
#endif
|
||||
0, /* tp_setattr */
|
||||
0, /* tp_compare */
|
||||
0, /* tp_repr */
|
||||
@@ -1461,63 +1601,26 @@ static PyTypeObject milter_ContextType = {
|
||||
0, /* tp_setattro */
|
||||
0, /* tp_as_buffer */
|
||||
Py_TPFLAGS_DEFAULT, /* tp_flags */
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
NULL, /* Documentation string */
|
||||
0, /* call function for all accessible objects */
|
||||
0, /* delete references to contained objects */
|
||||
0, /* rich comparisons */
|
||||
0, /* weak reference enabler */
|
||||
0, 0, /* Iterators */
|
||||
context_methods, /* Attribute descriptor and subclassing stuff */
|
||||
#endif
|
||||
};
|
||||
|
||||
static const char milter_documentation[] =
|
||||
static char milter_documentation[] =
|
||||
"This module interfaces with Sendmail's libmilter functionality,\n\
|
||||
allowing one to write email filters directly in Python.\n\
|
||||
Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
|
||||
See <sendmailsource>/libmilter/README for details on setting it up.\n";
|
||||
|
||||
static void setitem(PyObject *d,const char *name,long val) {
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
PyObject *v = PyLong_FromLong(val);
|
||||
#else
|
||||
PyObject *v = PyInt_FromLong(val);
|
||||
#endif
|
||||
PyDict_SetItemString(d,name,v);
|
||||
Py_DECREF(v);
|
||||
}
|
||||
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
|
||||
static struct PyModuleDef moduledef = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"milter", /* m_name */
|
||||
milter_documentation,/* m_doc */
|
||||
-1, /* m_size */
|
||||
milter_methods, /* m_methods */
|
||||
NULL, /* m_reload */
|
||||
NULL, /* m_traverse */
|
||||
NULL, /* m_clear */
|
||||
NULL, /* m_free */
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC PyInit_milter(void) {
|
||||
PyObject *m, *d;
|
||||
|
||||
if (PyType_Ready(&milter_ContextType) < 0)
|
||||
return NULL;
|
||||
|
||||
m = PyModule_Create(&moduledef);
|
||||
if (m == NULL) return NULL;
|
||||
#else
|
||||
|
||||
void initmilter(void) {
|
||||
void
|
||||
initmilter(void) {
|
||||
PyObject *m, *d;
|
||||
|
||||
m = Py_InitModule4("milter", milter_methods, milter_documentation,
|
||||
(PyObject*)NULL, PYTHON_API_VERSION);
|
||||
#endif
|
||||
d = PyModule_GetDict(m);
|
||||
MilterError = PyErr_NewException("milter.error", NULL, NULL);
|
||||
PyDict_SetItemString(d,"error", MilterError);
|
||||
@@ -1542,15 +1645,8 @@ void initmilter(void) {
|
||||
#ifdef SMFIF_CHGFROM
|
||||
setitem(d,"CHGFROM",SMFIF_CHGFROM);
|
||||
#endif
|
||||
#ifdef SMFIF_SETSYMLIST
|
||||
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 */
|
||||
#ifdef SMFIF_SETSMLIST
|
||||
setitem(d,"SETSMLIST",SMFIF_SETSMLIST);
|
||||
#endif
|
||||
#ifdef SMFIS_ALL_OPTS
|
||||
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
|
||||
@@ -1583,7 +1679,4 @@ void initmilter(void) {
|
||||
setitem(d,"DISCARD", SMFIS_DISCARD);
|
||||
setitem(d,"ACCEPT", SMFIS_ACCEPT);
|
||||
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
return m;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,3 +1,84 @@
|
||||
# $Log$
|
||||
# Revision 1.7 2009/06/13 21:15:12 customdesigned
|
||||
# Doxygen updates.
|
||||
#
|
||||
# Revision 1.6 2009/06/09 03:13:13 customdesigned
|
||||
# More doxygen docs.
|
||||
#
|
||||
# Revision 1.5 2005/07/20 14:49:43 customdesigned
|
||||
# Handle corrupt and empty ZIP files.
|
||||
#
|
||||
# Revision 1.4 2005/06/17 01:49:39 customdesigned
|
||||
# Handle zip within zip.
|
||||
#
|
||||
# Revision 1.3 2005/06/02 15:00:17 customdesigned
|
||||
# Configure banned extensions. Scan zipfile option with test case.
|
||||
#
|
||||
# Revision 1.2 2005/06/02 04:18:55 customdesigned
|
||||
# Update copyright notices after reading article on /.
|
||||
#
|
||||
# Revision 1.1.1.4 2005/05/31 18:23:49 customdesigned
|
||||
# Development changes since 0.7.2
|
||||
#
|
||||
# Revision 1.62 2005/02/14 22:31:17 stuart
|
||||
# _parseparam replacement not needed for python2.4
|
||||
#
|
||||
# Revision 1.61 2005/02/12 02:11:11 stuart
|
||||
# Pass unit tests with python2.4.
|
||||
#
|
||||
# Revision 1.60 2005/02/11 18:34:14 stuart
|
||||
# Handle garbage after quote in boundary.
|
||||
#
|
||||
# Revision 1.59 2005/02/10 01:10:59 stuart
|
||||
# Fixed MimeMessage.ismodified()
|
||||
#
|
||||
# Revision 1.58 2005/02/10 00:56:49 stuart
|
||||
# Runs with python2.4. Defang not working correctly - more work needed.
|
||||
#
|
||||
# Revision 1.57 2004/11/20 16:37:52 stuart
|
||||
# fix regex for splitting header and body
|
||||
#
|
||||
# Revision 1.56 2004/11/09 20:33:51 stuart
|
||||
# Recognize more dynamic PTR variations.
|
||||
#
|
||||
# Revision 1.55 2004/10/06 21:39:20 stuart
|
||||
# Handle message attachments with boundary errors by not parsing them
|
||||
# until needed.
|
||||
#
|
||||
# Revision 1.54 2004/08/18 01:59:46 stuart
|
||||
# Handle mislabeled multipart messages
|
||||
#
|
||||
# Revision 1.53 2004/04/24 22:53:20 stuart
|
||||
# Rename some local variables to avoid shadowing builtins
|
||||
#
|
||||
# Revision 1.52 2004/04/24 22:47:13 stuart
|
||||
# Convert header values to str
|
||||
#
|
||||
# Revision 1.51 2004/03/25 03:19:10 stuart
|
||||
# Correctly defang rfc822 attachments when boundary specified with
|
||||
# content-type message/rfc822.
|
||||
#
|
||||
# Revision 1.50 2003/10/15 22:01:00 stuart
|
||||
# Test for and work around email bug with encoded filenames.
|
||||
#
|
||||
# Revision 1.49 2003/09/04 18:48:13 stuart
|
||||
# Support python-2.2.3
|
||||
#
|
||||
# Revision 1.48 2003/09/02 00:27:27 stuart
|
||||
# Should have full milter based dspam support working
|
||||
#
|
||||
# Revision 1.47 2003/08/26 06:08:18 stuart
|
||||
# Use new python boolean since we now require 2.2.2
|
||||
#
|
||||
# Revision 1.46 2003/08/26 05:01:38 stuart
|
||||
# Release 0.6.0
|
||||
#
|
||||
# Revision 1.45 2003/08/26 04:01:24 stuart
|
||||
# Use new email module for parsing mail. Still need mime module to
|
||||
# provide various bug fixes to email module, and maintain some compatibility
|
||||
# with old milter code.
|
||||
#
|
||||
|
||||
## @package mime
|
||||
# This module provides a "defang" function to replace naughty attachments.
|
||||
#
|
||||
@@ -9,35 +90,26 @@
|
||||
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
|
||||
# This code is under the GNU General Public License. See COPYING for details.
|
||||
|
||||
from __future__ import print_function
|
||||
try:
|
||||
from io import BytesIO, StringIO
|
||||
except:
|
||||
from StringIO import StringIO
|
||||
BytesIO = StringIO
|
||||
import StringIO
|
||||
import socket
|
||||
import Milter
|
||||
import zipfile
|
||||
import sys
|
||||
|
||||
import email
|
||||
from email.message import Message
|
||||
try:
|
||||
from email.generator import BytesGenerator
|
||||
from email import message_from_binary_file, encoders
|
||||
except:
|
||||
from email.generator import Generator as BytesGenerator
|
||||
from email import message_from_file as message_from_binary_file
|
||||
from email import Encoders as encoders
|
||||
from email.utils import quote
|
||||
import email.Message
|
||||
from email.Message import Message
|
||||
from email.Generator import Generator
|
||||
from email.Utils import quote
|
||||
from email import Utils
|
||||
from email.Parser import Parser
|
||||
from email import Errors
|
||||
|
||||
if not getattr(Message,'as_bytes',None):
|
||||
Message.as_bytes = Message.as_string
|
||||
from types import ListType,StringType
|
||||
|
||||
## Return a list of filenames in a zip file.
|
||||
# Embedded zip files are recursively expanded.
|
||||
def zipnames(txt):
|
||||
fp = BytesIO(txt)
|
||||
fp = StringIO.StringIO(txt)
|
||||
zipf = zipfile.ZipFile(fp,'r')
|
||||
names = []
|
||||
for nm in zipf.namelist():
|
||||
@@ -48,7 +120,7 @@ def zipnames(txt):
|
||||
|
||||
## Fix multipart handling in email.Generator.
|
||||
#
|
||||
class MimeGenerator(BytesGenerator):
|
||||
class MimeGenerator(Generator):
|
||||
def _dispatch(self, msg):
|
||||
# Get the Content-Type: for the message, then try to dispatch to
|
||||
# self._handle_<maintype>_<subtype>(). If there's no handler for the
|
||||
@@ -58,7 +130,7 @@ class MimeGenerator(BytesGenerator):
|
||||
if msg.is_multipart() and main.lower() != 'multipart':
|
||||
self._handle_multipart(msg)
|
||||
else:
|
||||
BytesGenerator._dispatch(self,msg)
|
||||
Generator._dispatch(self,msg)
|
||||
|
||||
def unquote(s):
|
||||
"""Remove quotes from a string."""
|
||||
@@ -75,17 +147,19 @@ def unquote(s):
|
||||
return s[1:-1]
|
||||
return s
|
||||
|
||||
from types import TupleType
|
||||
|
||||
def _unquotevalue(value):
|
||||
if isinstance(value, tuple):
|
||||
if isinstance(value, TupleType):
|
||||
return value[0], value[1], unquote(value[2])
|
||||
else:
|
||||
return unquote(value)
|
||||
|
||||
#email.Message._unquotevalue = _unquotevalue
|
||||
|
||||
from email.message import _parseparam
|
||||
from email.Message import _parseparam
|
||||
|
||||
## Enhance email.message.Message
|
||||
## Enhance email.Message
|
||||
#
|
||||
# Tracks modifications to headers of body or any part independently.
|
||||
|
||||
@@ -125,8 +199,8 @@ class MimeMessage(Message):
|
||||
"""Return a list of (attr,name) pairs of attributes that IE might
|
||||
interpret as a name - and hence decide to execute this message."""
|
||||
names = []
|
||||
for attr,val in self.get_params([],'content-type',False):
|
||||
if isinstance(val, tuple):
|
||||
for attr,val in self._get_params_preserve([],'content-type'):
|
||||
if isinstance(val, TupleType):
|
||||
# It's an RFC 2231 encoded parameter
|
||||
newvalue = _unquotevalue(val)
|
||||
if val[0]:
|
||||
@@ -161,9 +235,9 @@ class MimeMessage(Message):
|
||||
g = MimeGenerator(file)
|
||||
g.flatten(self,unixfrom=unixfrom)
|
||||
|
||||
def as_bytes(self, unixfrom=False):
|
||||
def as_string(self, unixfrom=False):
|
||||
"Return the entire formatted message as a string."
|
||||
fp = BytesIO()
|
||||
fp = StringIO.StringIO()
|
||||
self.dump(fp,unixfrom=unixfrom)
|
||||
return fp.getvalue()
|
||||
|
||||
@@ -195,11 +269,6 @@ class MimeMessage(Message):
|
||||
|
||||
def get_payload(self,i=None,decode=False):
|
||||
msg = self.submsg
|
||||
if msg is None:
|
||||
t = self.get_content_type().lower()
|
||||
if t == 'message/rfc822' or t.startswith('multipart/'):
|
||||
msg = super().get_payload()
|
||||
self.submsg = msg
|
||||
if isinstance(msg,Message) and msg.ismodified():
|
||||
self.set_payload([msg])
|
||||
return Message.get_payload(self,i,decode)
|
||||
@@ -218,11 +287,7 @@ class MimeMessage(Message):
|
||||
if t == 'message/rfc822' or t.startswith('multipart/'):
|
||||
if not self.submsg:
|
||||
txt = self.get_payload()
|
||||
if type(txt) is bytes:
|
||||
self.submsg = email.message_from_bytes(txt,MimeMessage)
|
||||
for part in self.submsg.walk():
|
||||
part.modified = False
|
||||
elif type(txt) is str:
|
||||
if type(txt) == str:
|
||||
txt = self.get_payload(decode=True)
|
||||
self.submsg = email.message_from_string(txt,MimeMessage)
|
||||
for part in self.submsg.walk():
|
||||
@@ -233,7 +298,7 @@ class MimeMessage(Message):
|
||||
return None
|
||||
|
||||
def message_from_file(fp):
|
||||
msg = message_from_binary_file(fp,MimeMessage)
|
||||
msg = email.message_from_file(fp,MimeMessage)
|
||||
for part in msg.walk():
|
||||
part.modified = False
|
||||
assert not msg.ismodified()
|
||||
@@ -244,7 +309,7 @@ ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,inf,ins,isp,js,
|
||||
jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc,
|
||||
wsf,wsh
|
||||
""".split())
|
||||
bad_extensions = ['.' + x for x in extlist.split(',')]
|
||||
bad_extensions = map(lambda x:'.' + x,extlist.split(','))
|
||||
|
||||
def check_ext(name):
|
||||
"Check a name for dangerous Winblows extensions."
|
||||
@@ -283,24 +348,19 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
|
||||
msg["Content-Type"] = "text/plain; name="+name
|
||||
return Milter.CONTINUE
|
||||
|
||||
def check_attachments(msg,check,lev=None):
|
||||
import email.Iterators
|
||||
|
||||
def check_attachments(msg,check):
|
||||
"""Scan attachments.
|
||||
msg MimeMessage
|
||||
check function(MimeMessage): int
|
||||
Return CONTINUE, REJECT, ACCEPT
|
||||
"""
|
||||
if msg.is_multipart():
|
||||
if not lev: lev = []
|
||||
lev.append(1)
|
||||
if msg.get_content_type().endswith('/rfc822'):
|
||||
foo = 1
|
||||
for i in msg.get_payload():
|
||||
print('chkm',lev,msg.get_content_type())
|
||||
rc = check_attachments(i,check,lev=lev)
|
||||
rc = check_attachments(i,check)
|
||||
if rc != Milter.CONTINUE: return rc
|
||||
lev[-1] += 1
|
||||
return Milter.CONTINUE
|
||||
print('chk',lev,msg.get_content_type())
|
||||
return check(msg)
|
||||
|
||||
# save call context for Python without nested_scopes
|
||||
@@ -335,21 +395,18 @@ class _defang:
|
||||
# emulate old defang function
|
||||
defang = _defang()
|
||||
|
||||
if sys.version < '3.0.0':
|
||||
from sgmllib import SGMLParser as HTMLParser
|
||||
else:
|
||||
from Milter.sgmllib import SGMLParser as HTMLParser
|
||||
import sgmllib
|
||||
|
||||
import re
|
||||
declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*')
|
||||
declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*')
|
||||
|
||||
class SGMLFilter(HTMLParser):
|
||||
class SGMLFilter(sgmllib.SGMLParser):
|
||||
"""Parse HTML and pass through all constructs unchanged. It is intended for
|
||||
derived classes to implement exceptional processing for selected cases.
|
||||
"""
|
||||
def __init__(self,out):
|
||||
HTMLParser.__init__(self)
|
||||
sgmllib.SGMLParser.__init__(self)
|
||||
self.out = out
|
||||
|
||||
def handle_comment(self,comment):
|
||||
@@ -380,7 +437,7 @@ class SGMLFilter(HTMLParser):
|
||||
self.out.write("<!%s>" % data)
|
||||
|
||||
def write(self,buf):
|
||||
"Act like a writer. Why doesn't HTMLParser do this by default?"
|
||||
"Act like a writer. Why doesn't SGMLParser do this by default?"
|
||||
self.feed(buf)
|
||||
|
||||
# Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft
|
||||
@@ -423,14 +480,11 @@ class HTMLScriptFilter(SGMLFilter):
|
||||
self.modified = False
|
||||
self.msg = "<!-- WARNING: embedded script removed -->"
|
||||
def start_script(self,unused):
|
||||
#print('beg script',unused)
|
||||
self.ignoring += 1
|
||||
self.modified = True
|
||||
def end_script(self):
|
||||
#print('end script')
|
||||
self.ignoring -= 1
|
||||
if not self.ignoring:
|
||||
self.out.write(self.msg)
|
||||
def end_script(self):
|
||||
self.ignoring -= 1
|
||||
def handle_data(self,data):
|
||||
if not self.ignoring: SGMLFilter.handle_data(self,data)
|
||||
def handle_comment(self,comment):
|
||||
@@ -445,14 +499,14 @@ def check_html(msg,savname=None):
|
||||
if name and name.lower().endswith(".htm"):
|
||||
msgtype = 'text/html'
|
||||
if msgtype == 'text/html':
|
||||
out = StringIO()
|
||||
out = StringIO.StringIO()
|
||||
htmlfilter = HTMLScriptFilter(out)
|
||||
try:
|
||||
htmlfilter.write(msg.get_payload(decode=True).decode())
|
||||
htmlfilter.write(msg.get_payload(decode=True))
|
||||
htmlfilter.close()
|
||||
#except sgmllib.SGMLParseError:
|
||||
except:
|
||||
mimetools.copyliteral(msg.get_payload(),open('debug.out','wb'))
|
||||
#mimetools.copyliteral(msg.get_payload(),open('debug.out','w')
|
||||
htmlfilter.close()
|
||||
hostname = socket.gethostname()
|
||||
msg.set_payload(
|
||||
@@ -467,21 +521,22 @@ def check_html(msg,savname=None):
|
||||
if htmlfilter.modified:
|
||||
msg.set_payload(out) # remove embedded scripts
|
||||
del msg["content-transfer-encoding"]
|
||||
encoders.encode_quopri(msg)
|
||||
email.Encoders.encode_quopri(msg)
|
||||
return Milter.CONTINUE
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
def _list_attach(msg):
|
||||
t = msg.get_content_type()
|
||||
p = msg.get_payload(decode=True)
|
||||
print(msg.get_filename(),msg.get_content_type(),type(p))
|
||||
print msg.get_filename(),msg.get_content_type(),type(p)
|
||||
msg = msg.get_submsg()
|
||||
if isinstance(msg,Message):
|
||||
return check_attachments(msg,_list_attach)
|
||||
return Milter.CONTINUE
|
||||
|
||||
for fname in sys.argv[1:]:
|
||||
with open(fname,'rb') as fp:
|
||||
fp = open(fname)
|
||||
msg = message_from_file(fp)
|
||||
email.iterators._structure(msg)
|
||||
email.Iterators._structure(msg)
|
||||
check_attachments(msg,_list_attach)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Check Description Justification
|
||||
E111 req indent 4 Creates more continuation lines
|
||||
E114 req indent 4 cmnt Same
|
||||
E231 req space after , makes calls like print() harder to read
|
||||
E266 no ## Required by Doxygen
|
||||
W291 trailing spaces in cmnt Needed for space preserving para reformat
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
ignore=`awk -F\\\\t '{ print $1 }' pep8.dat | tail -n +2`
|
||||
a=(${ignore})
|
||||
list=$(echo "${a[@]}"|tr '[ ]' '[,]')
|
||||
echo python3 -m pep8 --ignore="$list" $@
|
||||
@@ -1,27 +0,0 @@
|
||||
diff -up ./Milter/utils.py.check ./Milter/utils.py
|
||||
--- ./Milter/utils.py.check 2018-08-04 23:01:23.858668412 -0400
|
||||
+++ ./Milter/utils.py 2018-08-04 23:01:39.460869588 -0400
|
||||
@@ -68,10 +68,6 @@ def iniplist(ipaddr,iplist):
|
||||
True
|
||||
>>> iniplist('192.168.0.45',['192.168.0.*'])
|
||||
True
|
||||
- >>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
|
||||
- True
|
||||
- >>> iniplist('2606:2800:220:1::',['example.com/40'])
|
||||
- True
|
||||
>>> iniplist('4.2.2.2',['nothing.example.com'])
|
||||
False
|
||||
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
||||
diff -up ./test.py.check ./test.py
|
||||
--- ./test.py.check 2018-08-04 23:04:58.609420815 -0400
|
||||
+++ ./test.py 2018-08-04 23:05:40.070949438 -0400
|
||||
@@ -14,6 +14,8 @@ def suite():
|
||||
return s
|
||||
|
||||
if __name__ == '__main__':
|
||||
+ import sys
|
||||
try: os.remove('test/milter.log')
|
||||
except: pass
|
||||
- unittest.TextTestRunner().run(suite())
|
||||
+ rc = unittest.TextTestRunner().run(suite())
|
||||
+ sys.exit(len(rc.failures))
|
||||
+60
-209
@@ -1,236 +1,87 @@
|
||||
# we don't want to provide private python extension libs
|
||||
%global sum Python interface to sendmail milter API
|
||||
%global __provides_exclude_from ^(%{python2_sitearch})/.*\\.so$
|
||||
%if 0%{?epel} == 7
|
||||
%global python3 python36
|
||||
%else
|
||||
%global python3 python3
|
||||
%endif
|
||||
%define __python python2.6
|
||||
%define pythonbase python26
|
||||
|
||||
Summary: %{sum}
|
||||
Name: python-pymilter
|
||||
Version: 1.0.4
|
||||
Release: 1%{?dist}
|
||||
Url: http://bmsi.com/pymilter
|
||||
Source: https://github.com/sdgathman/pymilter/archive/pymilter-%{version}.tar.gz
|
||||
#Source1: tmpfiles-python-pymilter.conf
|
||||
# remove unit tests that require network for check
|
||||
Patch: pymilter-check.patch
|
||||
%define libdir %{_libdir}/pymilter
|
||||
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
|
||||
|
||||
Summary: Python interface to sendmail milter API
|
||||
Name: %{pythonbase}-pymilter
|
||||
Version: 0.9.7
|
||||
Release: 1%{dist}
|
||||
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
|
||||
License: GPLv2+
|
||||
Group: Development/Libraries
|
||||
BuildRequires: python2-devel, %{python3}-devel, sendmail-devel >= 8.13
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||
Url: http://www.bmsi.com/python/milter.html
|
||||
# python-2.6.4 gets RuntimeError: not holding the import lock
|
||||
Requires: %{pythonbase} >= 2.6.5, sendmail >= 8.13
|
||||
# Need python2.6 specific pydns, not the version for system python
|
||||
BuildRequires: gcc
|
||||
Requires: %{pythonbase}-pydns
|
||||
# Needed for callbacks, not a core function but highly useful for milters
|
||||
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13
|
||||
|
||||
%global _description\
|
||||
This is a python extension module to enable python scripts to\
|
||||
attach to sendmail's libmilter functionality. Additional python\
|
||||
modules provide for navigating and modifying MIME parts, sending\
|
||||
%description
|
||||
This is a python extension module to enable python scripts to
|
||||
attach to sendmail's libmilter functionality. Additional python
|
||||
modules provide for navigating and modifying MIME parts, sending
|
||||
DSNs, and doing CBV.
|
||||
|
||||
%description %_description
|
||||
|
||||
%package -n python2-pymilter
|
||||
Summary: %{sum}
|
||||
%if 0%{?epel} >= 6
|
||||
Requires: python-pydns
|
||||
%else
|
||||
Requires: python2-pydns
|
||||
%endif
|
||||
Requires: %{name}-common = %{version}-%{release}
|
||||
%{?python_provide:%python_provide python2-pymilter}
|
||||
|
||||
%description -n python2-pymilter %_description
|
||||
|
||||
%package -n %{python3}-pymilter
|
||||
Summary: %{sum}
|
||||
%if 0%{?fedora} >= 26
|
||||
Requires: %{python3}-py3dns
|
||||
%endif
|
||||
Requires: %{name}-common = %{version}-%{release}
|
||||
%{?python_provide:%python_provide %{python3}-pymilter}
|
||||
|
||||
%description -n %{python3}-pymilter %_description
|
||||
|
||||
%package common
|
||||
Summary: Common files and directories for python milters
|
||||
BuildArch: noarch
|
||||
|
||||
%description common
|
||||
Common files and directories used for python milters
|
||||
|
||||
%package selinux
|
||||
Summary: SELinux policy module for pymilter
|
||||
Group: System Environment/Base
|
||||
Requires: policycoreutils, selinux-policy-targeted
|
||||
Requires: %{name} = %{version}-%{release}
|
||||
BuildArch: noarch
|
||||
BuildRequires: policycoreutils, checkpolicy, selinux-policy-devel
|
||||
%if 0%{?epel} >= 6
|
||||
BuildRequires: policycoreutils-python
|
||||
%else
|
||||
BuildRequires: policycoreutils-python-utils
|
||||
%endif
|
||||
|
||||
%description selinux
|
||||
Give sendmail_t additional access to stream sockets used to communicate
|
||||
with milters.
|
||||
|
||||
%prep
|
||||
%setup -q -n pymilter-pymilter-%{version}
|
||||
#patch -p1 -b .check
|
||||
%setup -q -n pymilter-%{version}
|
||||
|
||||
%build
|
||||
%py2_build
|
||||
%py3_build
|
||||
checkmodule -m -M -o pymilter.mod pymilter.te
|
||||
semodule_package -o pymilter.pp -m pymilter.mod
|
||||
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
|
||||
|
||||
%install
|
||||
%py2_install
|
||||
%py3_install
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
%{__python} setup.py install --root=$RPM_BUILD_ROOT
|
||||
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
|
||||
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
|
||||
mkdir -p $RPM_BUILD_ROOT%{libdir}
|
||||
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
|
||||
|
||||
mkdir -p %{buildroot}/run/milter
|
||||
mkdir -p %{buildroot}%{_localstatedir}/log/milter
|
||||
mkdir -p %{buildroot}%{_libexecdir}/milter
|
||||
#mkdir -p %{buildroot}%{_prefix}/lib/tmpfiles.d
|
||||
#install -m 0644 %{SOURCE1} %{buildroot}%{_prefix}/lib/tmpfiles.d/%{name}.conf
|
||||
|
||||
# install selinux modules
|
||||
mkdir -p %{buildroot}%{_datadir}/selinux/targeted
|
||||
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
|
||||
|
||||
%check
|
||||
py2path=$(ls -d build/lib.linux-*-2.*)
|
||||
py3path=$(ls -d build/lib.linux-*-3.*)
|
||||
PYTHONPATH=${py2path}:. python2 test.py &&
|
||||
PYTHONPATH=${py3path}:. python3 test.py
|
||||
|
||||
%files -n python2-pymilter
|
||||
%license COPYING
|
||||
# start.sh is used by spfmilter, srsmilter, and milter, and could be used by
|
||||
# other milters using pymilter.
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
|
||||
%{python2_sitearch}/*
|
||||
|
||||
%files -n %{python3}-pymilter
|
||||
%license COPYING
|
||||
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
|
||||
%{python3_sitearch}/*
|
||||
|
||||
%files common
|
||||
%dir %{_libexecdir}/milter
|
||||
%{_prefix}/lib/tmpfiles.d/%{name}.conf
|
||||
%{python_sitearch}/*
|
||||
%{libdir}
|
||||
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
|
||||
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
|
||||
%dir %attr(0755,mail,mail) /run/milter
|
||||
|
||||
%files selinux
|
||||
%doc pymilter.te
|
||||
%{_datadir}/selinux/targeted/*
|
||||
|
||||
%post selinux
|
||||
%{_sbindir}/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
|
||||
&>/dev/null || :
|
||||
|
||||
%postun selinux
|
||||
if [ $1 -eq 0 ] ; then
|
||||
%{_sbindir}/semodule -s targeted -r pymilter &> /dev/null || :
|
||||
fi
|
||||
%clean
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
%changelog
|
||||
* Wed Apr 17 2019 Stuart Gathman <stuart@gathman.org> - 1.0.4-1
|
||||
- New upstream release: cleanup unused files, additional platform support
|
||||
- Minor doc updates
|
||||
|
||||
* Sun Dec 23 2018 Stuart Gathman <stuart@gathman.org> - 1.0.3-1
|
||||
- New upstream release
|
||||
- patch step for python3 no longer required in build
|
||||
|
||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-4
|
||||
- Add unit tests to %%check
|
||||
|
||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-3
|
||||
- use libexec instead of libdir
|
||||
|
||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-2
|
||||
- add python34 subpackage on el7
|
||||
|
||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-1
|
||||
- build for both python2 and python3
|
||||
- add selinux policy allowing sendmail_t access to milters
|
||||
|
||||
* Tue Jul 17 2018 Miro Hrončok <mhroncok@redhat.com> - 1.0-13
|
||||
- Update Python macros to new packaging standards
|
||||
(See https://fedoraproject.org/wiki/Changes/Move_usr_bin_python_into_separate_package)
|
||||
|
||||
* Sat Jul 14 2018 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-12
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
|
||||
|
||||
* Fri Feb 09 2018 Iryna Shcherbina <ishcherb@redhat.com> - 1.0-11
|
||||
- Update Python 2 dependency declarations to new packaging standards
|
||||
(See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3)
|
||||
|
||||
* Fri Feb 09 2018 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-10
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
|
||||
|
||||
* Fri Feb 09 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 1.0-9
|
||||
- Escape macros in %%changelog
|
||||
|
||||
* Sat Aug 19 2017 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> - 1.0-8
|
||||
- Python 2 binary package renamed to python2-pymilter
|
||||
See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3
|
||||
|
||||
* Thu Aug 03 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-7
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Binutils_Mass_Rebuild
|
||||
|
||||
* Thu Jul 27 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-6
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
|
||||
>>>>>>> 021796e51e5919812f1c300d1830ef9ed378db2d
|
||||
|
||||
* Sat Feb 11 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-5
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild
|
||||
|
||||
* Tue Jul 19 2016 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.0-4
|
||||
- https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages
|
||||
|
||||
* Thu Feb 04 2016 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-3
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild
|
||||
|
||||
* Thu Jun 18 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.0-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild
|
||||
|
||||
* Sat Sep 27 2014 Paul Wouters <pwouters@redhat.com> - 1.0-1
|
||||
- Updated to 1.0
|
||||
- Use tmpfiles and /run
|
||||
|
||||
* Sun Aug 17 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.8-6
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_22_Mass_Rebuild
|
||||
|
||||
* Sat Jun 07 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.8-5
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild
|
||||
|
||||
* Fri Jan 10 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-4
|
||||
- Add COPYING
|
||||
- Fix buildroot macros and dist macro
|
||||
|
||||
* Fri Jan 10 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-3
|
||||
- rebuilt with proper file permission
|
||||
|
||||
* Tue Jan 07 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-2
|
||||
- Fixup for fedora release
|
||||
|
||||
* 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"
|
||||
|
||||
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
|
||||
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
|
||||
- Remove redundant table in miltermodule
|
||||
- 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
|
||||
- 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
|
||||
@@ -239,7 +90,7 @@ fi
|
||||
- Fix milter-template.py
|
||||
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
|
||||
|
||||
* Tue Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
|
||||
* Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
|
||||
- Handle IP6 in Milter.utils.iniplist()
|
||||
- python-2.6
|
||||
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
module pymilter 1.0;
|
||||
|
||||
require {
|
||||
type sendmail_t;
|
||||
type var_run_t;
|
||||
type initrc_t;
|
||||
class sock_file { write getattr };
|
||||
class unix_stream_socket connectto;
|
||||
}
|
||||
|
||||
#============= sendmail_t ==============
|
||||
allow sendmail_t initrc_t:unix_stream_socket connectto;
|
||||
allow sendmail_t var_run_t:sock_file { write getattr };
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import print_function
|
||||
|
||||
# A simple milter.
|
||||
|
||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||
@@ -7,10 +7,8 @@ from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import os
|
||||
try:
|
||||
from io import BytesIO
|
||||
except:
|
||||
from StringIO import StringIO as BytesIO
|
||||
import StringIO
|
||||
import rfc822
|
||||
import mime
|
||||
import Milter
|
||||
import tempfile
|
||||
@@ -23,14 +21,9 @@ class sampleMilter(Milter.Milter):
|
||||
"Milter to replace attachments poisonous to Windows with a WARNING message."
|
||||
|
||||
def log(self,*msg):
|
||||
print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),end=None)
|
||||
for i in msg:
|
||||
try:
|
||||
print(i,end=None)
|
||||
except UnicodeEncodeError:
|
||||
s = i.encode(encoding='utf-8',errors='surrogateescape')
|
||||
print(s,end=None)
|
||||
print()
|
||||
print "%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),
|
||||
for i in msg: print i,
|
||||
print
|
||||
|
||||
def __init__(self):
|
||||
self.tempname = None
|
||||
@@ -38,25 +31,18 @@ class sampleMilter(Milter.Milter):
|
||||
self.fp = None
|
||||
self.bodysize = 0
|
||||
self.id = Milter.uniqueID()
|
||||
self.user = None
|
||||
|
||||
# multiple messages can be received on a single connection
|
||||
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
|
||||
# of each message.
|
||||
@Milter.symlist('{auth_authen}')
|
||||
@Milter.noreply
|
||||
def envfrom(self,f,*str):
|
||||
"start of MAIL transaction"
|
||||
self.fp = BytesIO()
|
||||
self.log("mail from",f,str)
|
||||
self.fp = StringIO.StringIO()
|
||||
self.tempname = None
|
||||
self.mailfrom = f
|
||||
self.bodysize = 0
|
||||
self.user = self.getsymval('{auth_authen}')
|
||||
self.auth_type = self.getsymval('{auth_type}')
|
||||
if self.user:
|
||||
self.log("user",self.user,"sent mail from",f,str)
|
||||
else:
|
||||
self.log("mail from",f,str)
|
||||
return Milter.CONTINUE
|
||||
|
||||
def envrcpt(self,to,*str):
|
||||
@@ -67,31 +53,28 @@ class sampleMilter(Milter.Milter):
|
||||
self.log("rcpt to",to,str)
|
||||
return Milter.CONTINUE
|
||||
|
||||
@Milter.decode('bytes')
|
||||
def header(self,name,val):
|
||||
lname = name.lower()
|
||||
if lname == 'subject':
|
||||
|
||||
# even if we wanted the Taiwanese spam, we can't read Chinese
|
||||
# (delete if you read chinese mail)
|
||||
#print('val=',val.encode(errors='surrogateescape'))
|
||||
print('val=',val)
|
||||
if val.startswith(b'=?big5') or val.startswith(b'=?ISO-2022-JP'):
|
||||
if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'):
|
||||
self.log('REJECT: %s: %s' % (name,val))
|
||||
#self.setreply('550','','Go away spammer')
|
||||
return Milter.REJECT
|
||||
|
||||
# check for common spam keywords
|
||||
if val.find(b"$$$") >= 0 or val.find(b"XXX") >= 0 \
|
||||
or val.find(b"!!!") >= 0 or val.find(b"FREE") >= 0:
|
||||
if val.find("$$$") >= 0 or val.find("XXX") >= 0 \
|
||||
or val.find("!!!") >= 0 or val.find("FREE") >= 0:
|
||||
self.log('REJECT: %s: %s' % (name,val))
|
||||
#self.setreply('550','','Go away spammer')
|
||||
return Milter.REJECT
|
||||
|
||||
# check for spam that pretends to be legal
|
||||
lval = val.lower()
|
||||
if lval.startswith(b"adv:") or lval.startswith(b"adv.") \
|
||||
or lval.find(b'viagra') >= 0:
|
||||
if lval.startswith("adv:") or lval.startswith("adv.") \
|
||||
or lval.find('viagra') >= 0:
|
||||
self.log('REJECT: %s: %s' % (name,val))
|
||||
return Milter.REJECT
|
||||
|
||||
@@ -103,7 +86,7 @@ class sampleMilter(Milter.Milter):
|
||||
|
||||
# check for common bulk mailers
|
||||
if lname == 'x-mailer' and \
|
||||
val.lower() in (b'direct email',b'calypso',b'mail bomber'):
|
||||
val.lower() in ('direct email','calypso','mail bomber'):
|
||||
self.log('REJECT: %s: %s' % (name,val))
|
||||
#self.setreply('550','','Go away spammer')
|
||||
return Milter.REJECT
|
||||
@@ -112,12 +95,12 @@ class sampleMilter(Milter.Milter):
|
||||
if lname in ('subject','x-mailer'):
|
||||
self.log('%s: %s' % (name,val))
|
||||
if self.fp:
|
||||
self.fp.write(b"%s: %s\n" % (name.encode(),val)) # add header to buffer
|
||||
self.fp.write("%s: %s\n" % (name,val)) # add header to buffer
|
||||
return Milter.CONTINUE
|
||||
|
||||
def eoh(self):
|
||||
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
|
||||
self.fp.write(b'\n')
|
||||
self.fp.write("\n")
|
||||
self.fp.seek(0)
|
||||
# copy headers to a temp file for scanning the body
|
||||
headers = self.fp.getvalue()
|
||||
@@ -155,16 +138,19 @@ class sampleMilter(Milter.Milter):
|
||||
self.log("Temp file:",self.tempname)
|
||||
self.tempname = None # prevent removal of original message copy
|
||||
# copy defanged message to a temp file
|
||||
with tempfile.TemporaryFile() as out:
|
||||
out = tempfile.TemporaryFile()
|
||||
try:
|
||||
msg.dump(out)
|
||||
out.seek(0)
|
||||
msg = mime.message_from_file(out)
|
||||
fp = BytesIO(msg.as_bytes().split(b'\n\n',1)[1])
|
||||
msg = rfc822.Message(out)
|
||||
msg.rewindbody()
|
||||
while 1:
|
||||
buf = fp.read(8192)
|
||||
buf = out.read(8192)
|
||||
if len(buf) == 0: break
|
||||
self.replacebody(buf) # feed modified message to sendmail
|
||||
return Milter.ACCEPT # ACCEPT modified message
|
||||
finally:
|
||||
out.close()
|
||||
return Milter.TEMPFAIL
|
||||
|
||||
def close(self):
|
||||
@@ -185,13 +171,13 @@ if __name__ == "__main__":
|
||||
socketname = os.getenv("HOME") + "/pythonsock"
|
||||
Milter.factory = sampleMilter
|
||||
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS)
|
||||
print("""To use this with sendmail, add the following to sendmail.cf:
|
||||
print """To use this with sendmail, add the following to sendmail.cf:
|
||||
|
||||
O InputMailFilters=pythonfilter
|
||||
Xpythonfilter, S=local:%s
|
||||
|
||||
See the sendmail README for libmilter.
|
||||
sample milter startup""" % socketname)
|
||||
sample milter startup""" % socketname
|
||||
sys.stdout.flush()
|
||||
Milter.runmilter("pythonfilter",socketname,240)
|
||||
print("sample milter shutdown")
|
||||
print "sample milter shutdown"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[bdist_rpm]
|
||||
python=python3
|
||||
doc_files=README NEWS TODO COPYING CREDITS
|
||||
packager=Stuart D. Gathman <stuart@gathman.org>
|
||||
python=python2.6
|
||||
doc_files=README NEWS TODO
|
||||
packager=Stuart D. Gathman <stuart@bmsi.com>
|
||||
release=1
|
||||
|
||||
@@ -1,45 +1,40 @@
|
||||
import os
|
||||
import sys
|
||||
from setuptools import setup, Extension
|
||||
from distutils.core import setup, Extension
|
||||
|
||||
if sys.version < '2.6.5':
|
||||
sys.exit('ERROR: Sorry, python 2.6.5 is required for this module.')
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
# FIXME: on some versions of sendmail, smutil is renamed to sm.
|
||||
# On slackware and debian, leave it out entirely. It depends
|
||||
# on how libmilter was built by the sendmail package.
|
||||
#libs = ["milter", "smutil"]
|
||||
libs = ["milter"]
|
||||
libdirs = ["/usr/lib/libmilter"] # needed for Debian
|
||||
modules = ["mime"]
|
||||
|
||||
# NOTE: importing Milter to obtain version fails when milter.so not built
|
||||
setup(name = "pymilter", version = '1.0.5',
|
||||
setup(name = "pymilter", version = '0.9.7',
|
||||
description="Python interface to sendmail milter API",
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
long_description="""\
|
||||
This is a python extension module to enable python scripts to
|
||||
attach to sendmail's libmilter functionality. Additional python
|
||||
modules provide for navigating and modifying MIME parts, and
|
||||
sending DSNs or doing CBVs.
|
||||
""",
|
||||
author="Jim Niemira",
|
||||
author_email="urmane@urmane.org",
|
||||
maintainer="Stuart D. Gathman",
|
||||
maintainer_email="stuart@gathman.org",
|
||||
maintainer_email="stuart@bmsi.com",
|
||||
license="GPL",
|
||||
url="https://www.pymilter.org/",
|
||||
py_modules=modules,
|
||||
url="http://www.bmsi.com/python/milter.html",
|
||||
py_modules=["mime"],
|
||||
packages = ['Milter'],
|
||||
ext_modules=[
|
||||
Extension("milter", ["miltermodule.c"],
|
||||
library_dirs=libdirs,
|
||||
libraries=libs,
|
||||
# set MAX_ML_REPLY to 1 for sendmail < 8.13
|
||||
define_macros = [ ('MAX_ML_REPLY',32) ],
|
||||
# save lots of debugging time testing rfc2553 compliance
|
||||
extra_compile_args = [
|
||||
"-Werror=implicit-function-declaration",
|
||||
"-std=gnu17",
|
||||
]
|
||||
define_macros = [ ('MAX_ML_REPLY',32) ]
|
||||
),
|
||||
],
|
||||
keywords = ['sendmail','milter'],
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
appname="$1"
|
||||
script="${2:-${appname}}"
|
||||
datadir="/var/lib/milter"
|
||||
logdir="/var/log/milter"
|
||||
piddir="/var/run/milter"
|
||||
libdir="/usr/lib/pymilter"
|
||||
python="python2.4"
|
||||
exec >>${logdir}/${appname}.log 2>&1
|
||||
if test -s ${datadir}/${script}.py; then
|
||||
cd ${datadir} # use version in data dir if it exists for debugging
|
||||
elif test -s ${logdir}/${script}.py; then
|
||||
cd ${logdir} # use version in log dir if it exists for debugging
|
||||
else
|
||||
cd ${libdir}
|
||||
fi
|
||||
|
||||
${python} ${script}.py &
|
||||
echo $! >${piddir}/${appname}.pid
|
||||
@@ -2,9 +2,6 @@ import unittest
|
||||
import testmime
|
||||
import testsample
|
||||
import testutils
|
||||
import testgrey
|
||||
import testcfg
|
||||
import testpolicy
|
||||
import os
|
||||
|
||||
def suite():
|
||||
@@ -12,9 +9,6 @@ def suite():
|
||||
s.addTest(testmime.suite())
|
||||
s.addTest(testsample.suite())
|
||||
s.addTest(testutils.suite())
|
||||
s.addTest(testgrey.suite())
|
||||
s.addTest(testcfg.suite())
|
||||
s.addTest(testpolicy.suite())
|
||||
return s
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
SPF-Pass:example.com OK
|
||||
SPF-Neutral:example.com REJECT
|
||||
HELO-Neutral:example.com OK
|
||||
SPF-Permerror:foo@bad.example.com OK
|
||||
SPF-Permerror: REJECT
|
||||
SMTP-Auth:good@example.com OK
|
||||
SMTP-Auth:example.com REJECT
|
||||
SMTP-Auth:bad@localhost.localdomain REJECT
|
||||
SMTP-Test: REJECT
|
||||
SMTP-Test:.baz.com WILDCARD
|
||||
@@ -1,32 +0,0 @@
|
||||
# sample SRS configuration
|
||||
[srs]
|
||||
;secret="shhhh!"
|
||||
;maxage=21
|
||||
;hashlength=5
|
||||
# if defined, SRS uses a database for opaque rewriting
|
||||
;database=/var/log/milter/srsdata
|
||||
# sign these domains using SES to prevent forged bounces instead of SRS
|
||||
;ses = localdomain1.com, localdomain2.org
|
||||
# sign these domains using SRS in signing mode to prevent forged bounces
|
||||
;sign = localdomain1.com, localdomain2.org
|
||||
# rewrite all other domains to this domain using SRS
|
||||
;fwdomain = mydomain.com
|
||||
# additional domains to decode (reverse) srs
|
||||
# NOTE: bms.py in milter package can also do this, as can pysrs.m4 HACK.
|
||||
;srs = otherdomain.com
|
||||
# do not rewrite mail to these domains
|
||||
;nosrs = braindeadmail.com
|
||||
# Treat these localparts as a DSN. Lot's of braindead systems
|
||||
# send non-DSN mail to MAIL FROM.
|
||||
;banned_users = mailer-daemon, clamav, postmaster
|
||||
|
||||
[srsmilter]
|
||||
;datadir=/var/lib/milter
|
||||
socketname = /var/run/milter/srsmilter
|
||||
miltername = pysrsfilter
|
||||
# reject DSNs to unsigned recipients (bounce spam)
|
||||
reject_spoofed = true
|
||||
;trusted_relay = 1.2.3.4
|
||||
internal_connect = 192.168.*.*,127.0.0.1,::1
|
||||
# Enable outgoing SRS via CHGFROM (see code for limitations)
|
||||
miltersrs = false
|
||||
-18587
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
From the-concourse-on-high Sat Feb 2 13:01:43 2019
|
||||
Date: Sat, 02 Feb 2019 19:48:56 +0100
|
||||
To: stuart@[IPv6:fcd9:7f8a:e050:4b48:7fd6:7fa:5509:6e26]
|
||||
Subject: 来自qq.com的退信
|
||||
|
||||
Does you receive this email?
|
||||
Binary file not shown.
+72
@@ -0,0 +1,72 @@
|
||||
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
||||
by bmsaix.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA42304
|
||||
for <ed@bmsi.com>; Thu, 4 May 2000 05:22:03 -0400
|
||||
Received: from camco.celestial.com (root@dagney.celestial.com [192.136.111.7])
|
||||
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA21364
|
||||
for <ed@bmsi.com>; Thu, 4 May 2000 05:22:01 -0400
|
||||
Received: (12482 bytes) by camco.celestial.com
|
||||
via sendmail with P:stdio/D:lists/R:inet_hosts/T:smtp
|
||||
(sender: <owner-flexfax@celestial.com> owner: <owner-flexfax-outbound>)
|
||||
id <m12nHjG-000eNHa@camco.celestial.com>
|
||||
for flexfax-outbound; Thu, 4 May 2000 02:15:30 -0700 (PDT)
|
||||
(Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13)
|
||||
Received: from sgi.com(sgi.SGI.COM[192.48.153.1]) (12116 bytes) by camco.celestial.com
|
||||
via sendmail with P:esmtp/D:aliases/T:pipe
|
||||
(sender: <owner-flexfax@sgi.com> owner: <owner-flexfax>)
|
||||
id <m12nHh6-000eN7C@camco.celestial.com>
|
||||
for <flexfax@celestial.com>; Thu, 4 May 2000 02:13:16 -0700 (PDT)
|
||||
(Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13)
|
||||
Received: from proxy.internet ([195.184.42.82])
|
||||
by sgi.com (980327.SGI.8.8.8-aspam/980304.SGI-aspam:
|
||||
SGI does not authorize the use of its proprietary
|
||||
systems or networks for unsolicited or bulk email
|
||||
from the Internet.)
|
||||
via ESMTP id CAA02330
|
||||
for <flexfax@sgi.com>; Thu, 4 May 2000 02:13:10 -0700 (PDT)
|
||||
mail_from (orum@ditas.dk)
|
||||
Received: from [172.16.96.14] by proxy.daab.dkproxy.internet (NTMail 4.30.0013/NU4152.00.32401f35) with ESMTP id zmlyaaaa for <flexfax@sgi.com>; Thu, 4 May 2000 11:13:09 +0200
|
||||
Received: by mars with Internet Mail Service (5.5.2650.21)
|
||||
id <KGM63KG3>; Thu, 4 May 2000 11:11:13 +0100
|
||||
Message-ID: <9704D2AA604ED311BF6D0008C79F0A990B57BE@mars>
|
||||
From: =?iso-8859-1?Q?Peter_=D8rum?= <orum@ditas.dk>
|
||||
To: "'flexfax@sgi.com'" <flexfax@sgi.com>
|
||||
Subject: flexfax: ILOVEYOU
|
||||
Date: Thu, 4 May 2000 11:11:11 +0100
|
||||
MIME-Version: 1.0
|
||||
X-Mailer: Internet Mail Service (5.5.2650.21)
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----_=_NextPart_000_01BFB5B1.13228432"
|
||||
Sender: owner-flexfax@celestial.com
|
||||
Precedence: bulk
|
||||
|
||||
This message is in MIME format. Since your mail reader does not understand
|
||||
this format, some or all of this message may not be legible.
|
||||
|
||||
------_=_NextPart_000_01BFB5B1.13228432
|
||||
Content-Type: text/plain
|
||||
|
||||
|
||||
kindly check the attached LOVELETTER coming from me.
|
||||
|
||||
|
||||
------_=_NextPart_000_01BFB5B1.13228432
|
||||
Content-Type: application/octet-stream;
|
||||
name="LOVE-LETTER-FOR-YOU.TXT.vbs"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: attachment;
|
||||
filename="LOVE-LETTER-FOR-YOU.TXT.vbs"
|
||||
|
||||
rem barok -loveletter(vbe) <i hate go to school>
|
||||
rem by: spyder / ispyder@mail.com / @GRAMMERSoft Group / =
|
||||
Manila,Philippines
|
||||
On Error Resume Next
|
||||
set b=3Dfso.CreateTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM")
|
||||
b.close
|
||||
set d=3Dfso.OpenTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM",2)
|
||||
d.write dt5
|
||||
d.write join(lines,vbcrlf)
|
||||
d.write vbcrlf
|
||||
d.write dt6
|
||||
d.close
|
||||
end sub
|
||||
------_=_NextPart_000_01BFB5B1.13228432--
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
||||
by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41MmROS014480
|
||||
for <stuart@bmsi.com>; Wed, 1 May 2002 18:48:27 -0400
|
||||
Received: from bmsred.bmsi.com (bmsred [219.109.11.50])
|
||||
by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41MmFGR017812
|
||||
for <stuart@bmsi.com>; Wed, 1 May 2002 18:48:15 -0400
|
||||
X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
||||
by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41M3hOS038584
|
||||
for <ed@bmsi.com>; Wed, 1 May 2002 18:03:43 -0400
|
||||
X-Received: from exp.dflinc.com (exppub [12.148.147.210])
|
||||
by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41M3LGQ017812
|
||||
for <ed@bmsi.com>; Wed, 1 May 2002 18:03:22 -0400
|
||||
X-Received: from exp.dflinc.com (exp.dflinc.com [219.109.14.1])
|
||||
by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g41M3JGT012258
|
||||
for <ed@bmsi.com>; Wed, 1 May 2002 17:03:19 -0500
|
||||
X-Received: from dns.intervip.psi.br (dns.intervip.psi.br [200.215.126.2])
|
||||
by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g3NHlhGS032960
|
||||
for <lorraine@dflinc.com>; Tue, 23 Apr 2002 12:47:44 -0500
|
||||
X-Received: from Sncpyf (adsl-fnsbnu-055-k.brt.telesc.net.br [200.180.75.55])
|
||||
by dns.intervip.psi.br (Postfix) with SMTP id 1FAEE24D18
|
||||
for <lorraine@dflinc.com>; Tue, 23 Apr 2002 14:50:41 -0300 (BRT)
|
||||
From: enardelli <enardelli@karsten.com.br>
|
||||
To: lorraine@dflinc.com
|
||||
Subject: A special powful tool
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=XQ4T5Cj14m5h2vQ69IpO4mCG
|
||||
Message-Id: <20020423175041.1FAEE24D18@dns.intervip.psi.br>
|
||||
Date: Tue, 23 Apr 2002 14:50:41 -0300 (BRT)
|
||||
X-ReSent-Date: Wed, 1 May 2002 17:03:03 -0500 (CDT)
|
||||
X-ReSent-From: Gwen Bartelle <gwenb@dflinc.com>
|
||||
X-ReSent-To: ed@bmsi.com
|
||||
X-ReSent-Subject: A special powful tool
|
||||
X-ReSent-Message-ID: <Pine.A41.4.10.10205011703030.30638@exp.dflinc.com>
|
||||
ReSent-Date: Wed, 1 May 2002 18:47:52 -0400 (EDT)
|
||||
ReSent-From: Ed Bond <ed@bmsi.com>
|
||||
ReSent-To: Stuart Gathman <stuart@bmsi.com>
|
||||
ReSent-Subject: A special powful tool
|
||||
ReSent-Message-ID: <Pine.LNX.4.44.0205011847520.17454@bmsred.bmsi.com>
|
||||
|
||||
--XQ4T5Cj14m5h2vQ69IpO4mCG
|
||||
Content-Type: text/html;
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<HTML><HEAD></HEAD><BODY>
|
||||
<iframe src=3Dcid:Ux7VyFy7bTS9q height=3D0 width=3D0>
|
||||
</iframe>
|
||||
<FONT>Hi,This is a special powful tool<br>
|
||||
I wish you would enjoy it.</FONT></BODY></HTML>
|
||||
|
||||
--XQ4T5Cj14m5h2vQ69IpO4mCG
|
||||
Content-Type: audio/x-midi;
|
||||
name=hom1;tile=1;ord=3354010700499224[1].scr
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <Ux7VyFy7bTS9q>
|
||||
|
||||
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
CMDDePe/RHj3v5IT+r+Pe/e/kHr3v9Fv97/1Gfq/93H3v1Yc+r/3dve/oGj3v8sK+r+sx/e/
|
||||
Nyz5v7Hu+b98HD==
|
||||
--XQ4T5Cj14m5h2vQ69IpO4mCG
|
||||
--XQ4T5Cj14m5h2vQ69IpO4mCG
|
||||
Content-Type: application/octet-stream;
|
||||
name=hom1;tile=1;ord=3354010700499224[1].htm
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <Ux7VyFy7bTS9q>
|
||||
|
||||
PGh0bWw+PGhlYWQ+PHRpdGxlPkNsaWNrIGhlcmUgdG8gZmluZCBvdXQgbW9yZSE8L3RpdGxl
|
||||
PjwvaGVhZD4NCjxib2R5PjxTQ1JJUFQgTEFOR1VBR0U9SmF2YVNjcmlwdD4KPCEtLQp2YXIg
|
||||
U2hvY2tNb2RlID0gMDsKaWYgKG5hdmlnYXRvci5taW1lVHlwZXMgJiYgbmF2aWdhdG9yLm1p
|
||||
bWVUeXBlc1siYXBwbGljYXRpb24veC1zaG9ja3dhdmUtZmxhc2giXSAmJiBuYXZpZ2F0b3Iu
|
||||
bWltZVR5cGVzWyJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFzaCJdLmVuYWJsZWRQbHVn
|
||||
aW4pIHsKaWYgKG5hdmlnYXRvci5wbHVnaW5zICYmIG5hdmlnYXRvci5wbHVnaW5zWyJTaG9j
|
||||
a3dhdmUgRmxhc2giXSkKU2hvY2tNb2RlID0gMTsKfQplbHNlIGlmIChuYXZpZ2F0b3IudXNl
|
||||
ckFnZW50ICYmIG5hdmlnYXRvci51c2VyQWdlbnQuaW5kZXhPZigiTVNJRSIpPj0wIAomJiAo
|
||||
bmF2aWdhdG9yLnVzZXJBZ2VudC5pbmRleE9mKCJXaW5kb3dzIDkiKT49MCB8fCBuYXZpZ2F0
|
||||
b3IudXNlckFnZW50LmluZGV4T2YoIldpbmRvd3MgTlQiKT49MCkpIHsKZG9jdW1lbnQud3Jp
|
||||
dGUoJzxTQ1JJUFQgTEFOR1VBR0U9VkJTY3JpcHRcPiBcbicpOwpkb2N1bWVudC53cml0ZSgn
|
||||
b24gZXJyb3IgcmVzdW1lIG5leHQgXG4nKTsKZG9jdW1lbnQud3JpdGUoJ1Nob2NrTW9kZSA9
|
||||
IChJc09iamVjdChDcmVhdGVPYmplY3QoIlNob2Nrd2F2ZUZsYXNoLlNob2Nrd2F2ZUZsYXNo
|
||||
LjMiKSkpICcpOwpkb2N1bWVudC53cml0ZSgnPFwvU0NSSVBUXD4gJyk7Cn0KaWYgKCBTaG9j
|
||||
a01vZGUgKSB7CmRvY3VtZW50LndyaXRlKCc8T0JKRUNUIGNsYXNzaWQ9ImNsc2lkOkQyN0NE
|
||||
QjZFLUFFNkQtMTFjZi05NkI4LTQ0NDU1MzU0MDAwMCInKTsKZG9jdW1lbnQud3JpdGUoJyBj
|
||||
b2RlYmFzZT0iaHR0cDovL2FjdGl2ZS5tYWNyb21lZGlhLmNvbS9mbGFzaDIvY2Ficy9zd2Zs
|
||||
YXNoLmNhYiN2ZXJzaW9uPTMsMCwwLDAiJyk7CmRvY3VtZW50LndyaXRlKCcgSUQ9YmFubmVy
|
||||
IFdJRFRIPTIzMCBIRUlHSFQ9MjIwPicpOwpkb2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1F
|
||||
PW1vdmllIFZBTFVFPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5ici9hZHMvcG9wXzIzMHgyMjBf
|
||||
Z3Z0X3RlbGVmb25lLnN3Zj9jbGlja3RhZz1odHRwOi8vYWQuYnIuZG91YmxlY2xpY2submV0
|
||||
L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0wJTNCMCUzQjY2NjEw
|
||||
MDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0dHAlM2ElMmYlMmZ3
|
||||
d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFsLmpzcCI+ICcpOwpk
|
||||
b2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1FPXF1YWxpdHkgVkFMVUU9YXV0b2hpZ2g+ICcp
|
||||
Owpkb2N1bWVudC53cml0ZSgnPEVNQkVEIFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIv
|
||||
YWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5zd2Y/Y2xpY2t0YWc9aHR0cDovL2FkLmJy
|
||||
LmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUz
|
||||
QjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8NTA5MjQ0fDElM0IlM0Il
|
||||
M2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3Bv
|
||||
cnRhbC5qc3AiJyk7CmRvY3VtZW50LndyaXRlKCcgc3dMaXZlQ29ubmVjdD1GQUxTRSBXSURU
|
||||
SD0yMzAgSEVJR0hUPTIyMCcpOwpkb2N1bWVudC53cml0ZSgnIFFVQUxJVFk9YXV0b2hpZ2gn
|
||||
KTsKZG9jdW1lbnQud3JpdGUoJyBUWVBFPSJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFz
|
||||
aCIgUExVR0lOU1BBR0U9Imh0dHA6Ly93d3cubWFjcm9tZWRpYS5jb20vc2hvY2t3YXZlL2Rv
|
||||
d25sb2FkL2luZGV4LmNnaT9QMV9Qcm9kX1ZlcnNpb249U2hvY2t3YXZlRmxhc2giPicpOwpk
|
||||
b2N1bWVudC53cml0ZSgnPC9FTUJFRD4nKTsKZG9jdW1lbnQud3JpdGUoJzwvT0JKRUNUPicp
|
||||
Owp9IGVsc2UgaWYgKCEobmF2aWdhdG9yLmFwcE5hbWUgJiYgbmF2aWdhdG9yLmFwcE5hbWUu
|
||||
aW5kZXhPZigiTmV0c2NhcGUiKT49MCAmJiBuYXZpZ2F0b3IuYXBwVmVyc2lvbi5pbmRleE9m
|
||||
KCIyLiIpPj0wKSl7CmRvY3VtZW50LndyaXRlKCc8QSBIUkVGPSJodHRwOi8vYWQuYnIuZG91
|
||||
YmxlY2xpY2submV0L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0w
|
||||
JTNCMCUzQjY2NjEwMDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0
|
||||
dHAlM2ElMmYlMmZ3d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFs
|
||||
LmpzcCIgVEFSR0VUPSJfYmxhbmsiPjxJTUcgU1JDPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5i
|
||||
ci9hZHMvcG9wXzIzMHgyMjBfZ3Z0X3RlbGVmb25lLmdpZiIgV0lEVEg9MjMwIEhFSUdIVD0y
|
||||
MjAgQk9SREVSPTA+PC9BPicpOwp9Ci8vLS0+CjwvU0NSSVBUPgo8Tk9FTUJFRD48QSBIUkVG
|
||||
PT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8
|
||||
JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8
|
||||
NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9w
|
||||
dXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1HIFNSQz0iaHR0
|
||||
cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5naWYi
|
||||
IFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PRU1CRUQ+CjxOT1NDUklQ
|
||||
VD48QSBIUkVGPT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8
|
||||
MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAl
|
||||
M0I1MDk5MTd8NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIv
|
||||
bWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1H
|
||||
IFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxl
|
||||
Zm9uZS5naWYiIFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PU0NSSVBU
|
||||
PjwvYm9keT4NCjwvaHRtbD
|
||||
--XQ4T5Cj14m5h2vQ69IpO4mCG--
|
||||
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
||||
by bmsaix.bmsi.com (8.9.1/8.9.1) with ESMTP id QAA24094
|
||||
for <ed@bmsi.com>; Fri, 12 Jan 2001 16:30:00 -0500
|
||||
Received: from jscaix.jsconnor.com (jscaix [209.193.177.106])
|
||||
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id QAA30044
|
||||
for <ed@bmsi.com>; Fri, 12 Jan 2001 16:29:54 -0500
|
||||
Received: from connor.jsconnor.com (connor.jsconnor.com [192.168.100.15])
|
||||
by jscaix.jsconnor.com (8.9.1/8.9.1) with ESMTP id QAA12022
|
||||
for <ed@bmsi.com>; Fri, 12 Jan 2001 16:31:51 -0500
|
||||
X-Received: from goodspeed2.apical.com (ns1.apical.com [209.150.15.130])
|
||||
by jscaix.jsconnor.com (8.9.1/8.9.1) with ESMTP id HAA36550
|
||||
for <carrollf@jsconnor.com>; Fri, 12 Jan 2001 07:19:10 -0500
|
||||
X-Received: from SalCanino (cz-cblk-150-16-32.cyberzone.net [209.150.16.32])
|
||||
by goodspeed2.apical.com (8.9.3/8.9.3) with SMTP id HAA14946
|
||||
for <carrollf@jsconnor.com>; Fri, 12 Jan 2001 07:16:37 -0500
|
||||
Reply-To: <sal.canino@innovativeconcepts.com>
|
||||
From: "Sal Canino" <sal.canino@innovativeconcepts.com>
|
||||
To: "Carroll Forehand" <carrollf@jsconnor.com>
|
||||
Subject: AUTEAE
|
||||
Date: Fri, 12 Jan 2001 04:16:36 -0800
|
||||
Message-ID: <NEBBKLEPKLBIEKBANDGCIEMOCGAA.sal.canino@innovativeconcepts.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----=_NextPart_000_0003_01C07C4E.74368FC0"
|
||||
X-Priority: 3 (Normal)
|
||||
X-MSMail-Priority: Normal
|
||||
X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
|
||||
Importance: Normal
|
||||
X-MimeOLE: Produced By Microsoft MimeOLE V5.50.4133.2400
|
||||
Disposition-Notification-To: "Sal Canino" <sal.canino@innovativeconcepts.com>
|
||||
ReSent-Date: Fri, 12 Jan 2001 16:29:03 -0500 (EST)
|
||||
ReSent-From: Carroll Forehand <carrollf@jsconnor.com>
|
||||
ReSent-To: ed@bmsi.com
|
||||
ReSent-Subject: AUTEAE
|
||||
ReSent-Message-ID: <Pine.A41.4.10.10101121629001.171826@connor.jsconnor.com>
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
------=_NextPart_000_0003_01C07C4E.74368FC0
|
||||
Content-Type: text/plain;
|
||||
charset="iso-8859-1"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
|
||||
------=_NextPart_000_0003_01C07C4E.74368FC0
|
||||
Content-Type: application/octet-stream;
|
||||
name="PEDI.JPG.vbs"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: attachment;
|
||||
filename="PEDI.JPG.vbs"
|
||||
|
||||
rem =
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A=
|
||||
rem "Plan Colombia" virus v1.0=0A=
|
||||
rem by Sand Ja9e Gr0w (www.colombia.com)=0A=
|
||||
=0A=
|
||||
rem Dedicated to all the people that want to be hackers or crackers, in =
|
||||
Colombia =0A=
|
||||
rem This program is also a protest act against the violence and =
|
||||
corruption that Colombia lives...=0A=
|
||||
rem I always wanting that all this finishes, I have said...=0A=
|
||||
=0A=
|
||||
=0A=
|
||||
rem Santa fe de Bogot=E1 2000/09=0A=
|
||||
rem I dedicate to all you the song "GoodBye" of Andreas Bochelli=0A=
|
||||
rem =
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
|
||||
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A=
|
||||
=0A=
|
||||
=0A=
|
||||
rem Thanks God..!=0A=
|
||||
rem A greeting for "Lina Mar=EDa" from "Santa fe de Bogot=E1"=0A=
|
||||
rem A greeting for "Tizo" from "Spain"=0A=
|
||||
rem And One kicked of tail to my friends, "eL ChE" and "ThE SpY"=0A=
|
||||
=0A=
|
||||
rem okay, ok... =0A=
|
||||
rem my baby start here...=0A=
|
||||
=0A=
|
||||
=0A=
|
||||
On Error Resume Next=0A=
|
||||
|
||||
------=_NextPart_000_0003_01C07C4E.74368FC0--
|
||||
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
||||
by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EMUxS24174
|
||||
for <stuart@bmsi.com>; Fri, 14 Sep 2001 18:30:59 -0400
|
||||
Received: from bmsred.bmsi.com (bmsred [219.109.11.50])
|
||||
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id SAA12740
|
||||
for <stuart@bmsi.com>; Fri, 14 Sep 2001 18:30:58 -0400
|
||||
X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
||||
by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EESNW28934
|
||||
for <ed@bmsi.com>; Fri, 14 Sep 2001 10:28:23 -0400
|
||||
X-Received: from bwi.bwicorp.com (bwi.bwicorp.com [209.116.254.106])
|
||||
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id KAA34262
|
||||
for <ed@bmsi.com>; Fri, 14 Sep 2001 10:28:20 -0400
|
||||
X-Received: from bwicorp.com (bwi3 [192.168.3.22])
|
||||
by bwi.bwicorp.com (8.9.1/8.9.1) with ESMTP id KAA42970
|
||||
for <ed@bmsi.com>; Fri, 14 Sep 2001 10:33:54 -0400
|
||||
Date: Fri, 14 Sep 2001 10:33:54 -0400
|
||||
From: Mary Smith <mary@bwicorp.com>
|
||||
Message-Id: <200109141433.KAA42970@bwi.bwicorp.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="==i3.9.0oisdboibsd((kncd"
|
||||
ReSent-Date: Fri, 14 Sep 2001 18:30:47 -0400 (EDT)
|
||||
ReSent-From: Ed Bond <ed@bmsi.com>
|
||||
ReSent-To: Stuart Gathman <stuart@bmsi.com>
|
||||
ReSent-Subject: Resent mail....
|
||||
ReSent-Message-ID: <Pine.LNX.4.33.0109141830470.13214@bmsred.bmsi.com>
|
||||
|
||||
--==i3.9.0oisdboibsd((kncd
|
||||
Content-Type: application/octet-stream; name="READER_DIGEST_LETTER.TXT.pif"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename="READER_DIGEST_LETTER.TXT.pif"
|
||||
|
||||
TVpQAAIAAAAEAA8A//8AALgAAAAAAAAAQAAaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAEAALoQAA4ftAnNIbgBTM0hkJBUaGlzIHByb2dyYW0gbXVzdCBiZSBydW4gdW5kZXIgV2lu
|
||||
MzINCiQ3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBFAABMAQQA5ijojgAAAAAAAAAA4ACOgQsBAhkA
|
||||
FAAAAAYAAAAAAAAAEAAAABAAAAAwAAAAAEAAABAAAAACAAABAAAAAAAAAAMACgAAAAAAAMAAAAAE
|
||||
AAAAAAAAAgAAAAAAEAAAIAAAAAAQAAAQAAAAAAAAEAAAAAAAAAAAAAAAAEAAAIoAAAAAUAAAAAYA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ09ERQAAAAAA
|
||||
IAAAABAAAAAUAAAABgAAAAAAAAAAAAAAAAAAIAAA4ERBVEEAAAAAABAAAAAwAAAAAgAAABoAAAAA
|
||||
AAAAAAAAAAAAAEAAAMAuaWRhdGEAAAAQAAAAQAAAAAIAAAAcAAAAAAAAAAAAAAAAAABAAADALnJz
|
||||
cmMAAAAAgAAAAFAAAAAwAAAAHgAAAAAAAAAAAAAAAAAAQAAA0AAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
RDY5alLDAJCK/jLsU0G8R03PAwt5DjEcFVK3ICRNw5dh2gxwqg7aZ3VtO1ynbZr2zAD/////////
|
||||
/////6IDEwBbAAggAAAA
|
||||
|
||||
|
||||
--==i3.9.0oisdboibsd((kncd--
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
From mdb@go2net.com Tue Sep 18 10:31:34 2001
|
||||
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
||||
by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8IEVXM42662
|
||||
for <stuart@bmsi.com>; Tue, 18 Sep 2001 10:31:34 -0400
|
||||
Received: from STOREULV2 (mail.indexas.no [195.70.182.114])
|
||||
by www.bmsi.com (8.9.1/8.9.1) with SMTP id KAA27604
|
||||
for <stuart@bmsi.com>; Tue, 18 Sep 2001 10:31:31 -0400
|
||||
Date: Tue, 18 Sep 2001 10:31:31 -0400
|
||||
From: mdb@go2net.com
|
||||
Message-Id: <200109181431.KAA27604@www.bmsi.com>
|
||||
Subject: udesktopdesktopeksempeleksempeldesktopeksempeldesktopeksempeldesktopdesktopdesktopeksempeleksempeleksempeleksempeldesktopeksempeleksempeleksempeleksempeleksempeleksempeldesktopeksempeleksempeleksempeldesktopeksempeleksempeldesktopdesktopdesktopeksempeldeskmail.bmsi.com.desktop
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related;
|
||||
type="multipart/alternative";
|
||||
boundary="====_ABC1234567890DEF_===="
|
||||
X-Priority: 3
|
||||
X-MSMail-Priority: Normal
|
||||
X-Unsent: 1
|
||||
Status: RO
|
||||
X-Status:
|
||||
X-Keywords:
|
||||
|
||||
--====_ABC1234567890DEF_====
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="====_ABC0987654321DEF_===="
|
||||
|
||||
--====_ABC0987654321DEF_====
|
||||
Content-Type: text/html;
|
||||
charset="iso-8859-1"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
|
||||
<HTML><HEAD></HEAD><BODY bgColor=3D#ffffff>
|
||||
<iframe src=3Dcid:EA4DMGBP9p height=3D0 width=3D0>
|
||||
</iframe></BODY></HTML>
|
||||
--====_ABC0987654321DEF_====--
|
||||
|
||||
--====_ABC1234567890DEF_====
|
||||
Content-Type: audio/x-wav;
|
||||
name="readme.exe"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <EA4DMGBP9p>
|
||||
|
||||
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAA2AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1v
|
||||
ZGUuDQ0KJAAAAAAAAAA11CFvcbVPPHG1TzxxtU88E6pcPHW1TzyZqkU8dbVPPJmqSzxytU88cbVO
|
||||
PBG1TzyZqkQ8fbVPPMmzSTxwtU88UmljaHG1TzwAAAAAAAAAAMBEAWMAAAB/UEUAAEwBBQB1Oqc7
|
||||
AAAAAAAAAADgAA4BCwEGAABwAAAAYAAAAAAAALN0AAAAEAAAAIAAAAAAFzYAEAAAABAAAAQAAAAA
|
||||
AAAABAAAAAAAAAAAEAEAABAAAAAAAAACAAAAAAAQAAAQAAAAABAAABAAAAAAAAAQAAAAAAAAAAAA
|
||||
AACEgQAAUAAAAADgAACIHgAAAAAAAAAAAAAAAAAAAAAAAAAAAQA4CgAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIQBAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAudGV4dAAAAFZlAAAAEAAAAHAAAAAQAAAAAAAAAAAAAAAAAAAgAABgLnJkYXRhAAAq
|
||||
CQAAAIAAAAAQAAAAgAAAAAAAAAAAAAAAAAAAQAAAQC5kYXRhAAAAKEcAAACQAAAAIAAAAJAAAAAA
|
||||
AAAAAAAAAAAAAEAAAMAucnNyYwAAAAAgAAAA4AAAACAAAACwAAAAAAAAAAAAAAAAAABAAABALnJl
|
||||
bG9jAABGCwAAAAABAAAQAAAA0AAAAAAAAAAAAAAAAAAAQAAAQgAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAA=
|
||||
|
||||
--====_ABC1234567890DEF_====
|
||||
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
From mdb@go2net.com Tue Sep 18 10:31:34 2001
|
||||
Received: from localhost (varna148.pip.digsys.bg [193.68.1.148])
|
||||
by danbo.digsys.bg (8.10.1/8.10.1) with SMTP id fAM7FHk06734
|
||||
for butchc@trwonnor.com; Thu, 22 Nov 2001 09:15:18 +0200 (EET)
|
||||
From: POP - interlogvar <interlogvar@mbox.digsys.bg>
|
||||
Message-Id: <200111220715.fAM7FHk06734@danbo.digsys.bg>
|
||||
To: butchc@trwonnor.com
|
||||
Subject: Funny shit to see ?!
|
||||
Date: Thu,22 Nov 2001 09:16:34 -0000
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="bound"
|
||||
X-Priority: 3
|
||||
X-MSMail-Priority: Normal
|
||||
X-Mailer: Microsoft Outlook Express 5.50.4522.1300
|
||||
X-MimeOLE: Produced By Microsoft MimeOLE V5.50.4522.1300
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--bound
|
||||
Content-Type: text/html;
|
||||
charset="iso-8859-1"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<HTML><HEAD></HEAD><BODY><iframe src=3Dcid:SOMECID height=3D0 width=3D0></iframe>
|
||||
<font>peace</font></BODY></HTML>
|
||||
|
||||
--bound
|
||||
Content-Type: audio/x-wav;
|
||||
name="whatever.exe"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <SOMECID>
|
||||
|
||||
TVoAAAIAAAACAB4AHgAAAAACAAAAAAAAAAAAAMWnLuEOH7oOALQJ
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAA=
|
||||
|
||||
--bound--
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
From mdb@go2net.com Tue Sep 18 10:31:34 2001
|
||||
Received: from aglnss01.grupoagrisal.net ([172.16.0.1])
|
||||
by agntss05 (Lotus Domino Release 5.07a)
|
||||
with ESMTP id 2001120416164050:5294 ;
|
||||
Tue, 4 Dec 2001 16:16:40 -0600
|
||||
Subject: MAEU XSS025786 - ORDER 1251 - CONTAINER MAEU 6053725
|
||||
To: kathyp@jsconnor.com
|
||||
Cc: Blanca@ace-of-hearts.net
|
||||
X-Mailer: Lotus Notes Release 5.07a May 14, 2001
|
||||
Message-ID: <OF28551015.C47BCC85-ON06256B18.0079DD92@grupoagrisal.net>
|
||||
From: sherrera.dco.lc@agrisal.com
|
||||
Date: Tue, 4 Dec 2001 16:11:48 -0600
|
||||
MIME-Version: 1.0
|
||||
X-MIMETrack: Serialize by Router on AGLNSS01/AGRISAL(Release 5.07a |May 14, 2001) at 04/12/2001
|
||||
04:11:57 p.m.,
|
||||
Itemize by SMTP Server on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at
|
||||
12/04/2001 04:16:41 PM,
|
||||
Serialize by Router on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at
|
||||
12/04/2001 04:16:51 PM
|
||||
Content-type: application/octet-stream;
|
||||
name="FAX20.exe"
|
||||
Content-Disposition: attachment; filename="FAX20.exe"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAKJsVAAAACIAACIAACIBr6AQA
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
From pandora.owner@pandora.cz Wed Mar 24 21:02:22 2004
|
||||
Received: from pandora.cz (localhost [127.0.0.1])
|
||||
by pandora3.mobil.cz (8.12.8/8.12.8) with ESMTP id i2O88iWu021270
|
||||
for <stuart@bmsi.com>; Wed, 24 Mar 2004 09:08:44 +0100
|
||||
Message-Id: <200403240808.i2O88iWu021270@pandora3.mobil.cz>
|
||||
X-Sender: Pandora
|
||||
MIME-Version: 1.0
|
||||
Date: Wed, 24 Mar 2004 09:08:44 +0100
|
||||
From: "administrator@pandora.cz" <administrator@pandora.cz>
|
||||
To: "stuart@bmsi.com" <stuart@bmsi.com>
|
||||
Subject: Konferenceneexistuje
|
||||
Content-Type: multipart/mixed; boundary="Pandora3Bndry_1080115724426044878"
|
||||
|
||||
|
||||
--Pandora3Bndry_1080115724426044878
|
||||
Content-Type: multipart/alternative; boundary="Pandora3Bndry_1080115724783315537"
|
||||
|
||||
|
||||
--Pandora3Bndry_1080115724783315537
|
||||
Content-Type: text/plain; charset="ISO-8859-2"
|
||||
|
||||
Konference '2003-07-46063' neexistuje.
|
||||
|
||||
--Pandora3Bndry_1080115724783315537
|
||||
Content-Type: text/html; charset="ISO-8859-2"
|
||||
|
||||
Konference '2003-07-46063' neexistuje.
|
||||
|
||||
--Pandora3Bndry_1080115724783315537--
|
||||
|
||||
--Pandora3Bndry_1080115724426044878
|
||||
Content-Type: message/rfc822; boundary="----=_NextPart_000_0010_00000FFF.00007545"
|
||||
|
||||
MIME-Version: 1.0
|
||||
Date: Wed, 24 Mar 2004 09:03:28 +0100
|
||||
From: "" <stuart@bmsi.com>
|
||||
To: "" <2003-07-46063@pandora.cz>
|
||||
Subject: =?ISO-8859-2?q?Re=3A_Your_software?=
|
||||
Content-Type: multipart/mixed; boundary="Pandora3Bndry_10801157231587976770"
|
||||
|
||||
|
||||
--Pandora3Bndry_10801157231587976770
|
||||
Content-Type: text/plain; charset="Windows-1252"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
See the attached file for details.
|
||||
|
||||
|
||||
--Pandora3Bndry_10801157231587976770
|
||||
Content-Type: application/octet-stream; name="application.pif"
|
||||
Content-Disposition: attachment; filename="application.pif"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAuAAAAKvnXsbvhjCV74Ywle+GMJVsmj6V44YwlQeZOpX2hjCV74YxlbiGMJVsjm2V
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
--Pandora3Bndry_10801157231587976770--
|
||||
|
||||
--Pandora3Bndry_1080115724426044878--
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import unittest
|
||||
from Milter.config import MilterConfigParser
|
||||
|
||||
class ConfigTestCase(unittest.TestCase):
|
||||
def testConfig(self):
|
||||
cp = MilterConfigParser()
|
||||
cp.read(['test/pysrs.cfg'])
|
||||
socketname = cp.getdefault('srsmilter','socketname',
|
||||
'/var/run/milter/srsmilter')
|
||||
self.assertEqual(socketname,'/var/run/milter/srsmilter')
|
||||
miltersrs = cp.getboolean('srsmilter','miltersrs')
|
||||
self.assertFalse(miltersrs)
|
||||
|
||||
def suite(): return unittest.TestLoader().loadTestsFromTestCase(ConfigTestCase)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
import unittest
|
||||
import doctest
|
||||
import os
|
||||
#from Milter.greylist import Greylist
|
||||
from Milter.greysql import Greylist
|
||||
|
||||
class GreylistTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.fname = 'test.db'
|
||||
if os.path.isfile(self.fname):
|
||||
os.remove(self.fname)
|
||||
|
||||
def tearDown(self):
|
||||
#os.remove(self.fname)
|
||||
pass
|
||||
|
||||
def testGrey(self):
|
||||
grey = Greylist(self.fname)
|
||||
# first time
|
||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com')
|
||||
self.assertEqual(rc,0)
|
||||
# not in window yet
|
||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*60)
|
||||
self.assertEqual(rc,0)
|
||||
# within window
|
||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=15*60)
|
||||
self.assertEqual(rc,1)
|
||||
# new triple
|
||||
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=15*60)
|
||||
self.assertEqual(rc,0)
|
||||
# seen again
|
||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*3600)
|
||||
self.assertEqual(rc,2)
|
||||
# new one past expire
|
||||
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=6*3600)
|
||||
self.assertEqual(rc,0)
|
||||
# original past retain
|
||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=37*24*3600)
|
||||
self.assertEqual(rc,0)
|
||||
# new one for testing expire
|
||||
rc = grey.check('1.2.3.5','flub@bar.com','baz@spat.com',timeinc=20*24*3600)
|
||||
self.assertEqual(rc,0)
|
||||
grey.close()
|
||||
# test cleanup
|
||||
grey = Greylist(self.fname)
|
||||
rc = grey.clean(timeinc=37*24*3600)
|
||||
self.assertEqual(rc,1)
|
||||
grey.close()
|
||||
|
||||
def suite():
|
||||
s = unittest.TestLoader().loadTestsFromTestCase(GreylistTestCase)
|
||||
return s
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.TextTestRunner().run(suite())
|
||||
+76
-122
@@ -1,22 +1,36 @@
|
||||
# @author Stuart D. Gathman <stuart@bmsi.com>
|
||||
# Copyright 2005,2009,2020 Business Management Systems, Inc.
|
||||
# This code is under the GNU General Public License. See COPYING for details.
|
||||
from __future__ import print_function
|
||||
# $Log$
|
||||
# Revision 1.4 2005/07/20 14:49:44 customdesigned
|
||||
# Handle corrupt and empty ZIP files.
|
||||
#
|
||||
# Revision 1.3 2005/06/17 01:49:39 customdesigned
|
||||
# Handle zip within zip.
|
||||
#
|
||||
# Revision 1.2 2005/06/02 15:00:17 customdesigned
|
||||
# Configure banned extensions. Scan zipfile option with test case.
|
||||
#
|
||||
# Revision 1.1.1.2 2005/05/31 18:23:49 customdesigned
|
||||
# Development changes since 0.7.2
|
||||
#
|
||||
# Revision 1.23 2005/02/11 18:34:14 stuart
|
||||
# Handle garbage after quote in boundary.
|
||||
#
|
||||
# Revision 1.22 2005/02/10 01:10:59 stuart
|
||||
# Fixed MimeMessage.ismodified()
|
||||
#
|
||||
# Revision 1.21 2005/02/10 00:56:49 stuart
|
||||
# Runs with python2.4. Defang not working correctly - more work needed.
|
||||
#
|
||||
# Revision 1.20 2004/11/20 16:38:17 stuart
|
||||
# Add rcs log
|
||||
#
|
||||
import unittest
|
||||
import mime
|
||||
import zipfile
|
||||
import socket
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except:
|
||||
from io import StringIO
|
||||
import StringIO
|
||||
import email
|
||||
import sys
|
||||
import Milter
|
||||
try:
|
||||
from email import Errors as errors
|
||||
except:
|
||||
from email import errors
|
||||
from email import Errors
|
||||
|
||||
samp1_txt1 = """Dear Agent 1
|
||||
I hope you can read this. Whenever you write label it P.B.S kids.
|
||||
@@ -27,70 +41,50 @@ hostname = socket.gethostname()
|
||||
|
||||
class MimeTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.zf = zipfile.ZipFile('test/virus.zip','r')
|
||||
self.zf.setpassword(b'denatured')
|
||||
|
||||
def tearDown(self):
|
||||
self.zf.close()
|
||||
self.zf = None
|
||||
|
||||
# test mime parameter parsing
|
||||
def testParam(self):
|
||||
plist = mime._parseparam('; boundary="----=_NextPart_000_4e56_490d_48e3"')
|
||||
plist = [ x for x in plist if x ] # py2 doesn't include empty params
|
||||
self.assertEqual(1,len(plist))
|
||||
self.assertTrue(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"')
|
||||
plist = mime._parseparam(
|
||||
'; boundary="----=_NextPart_000_4e56_490d_48e3"')
|
||||
self.failUnless(len(plist)==1)
|
||||
self.failUnless(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"')
|
||||
plist = mime._parseparam('; name="Jim&amp;Girlz.jpg"')
|
||||
plist = [ x for x in plist if x ] # py2 doesn't include empty params
|
||||
self.assertEqual(1,len(plist))
|
||||
self.assertTrue(plist[0] == 'name="Jim&amp;Girlz.jpg"')
|
||||
self.failUnless(len(plist)==1)
|
||||
self.failUnless(plist[0] == 'name="Jim&amp;Girlz.jpg"')
|
||||
|
||||
def testParse(self,fname='samp1'):
|
||||
with open('test/'+fname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
self.assertTrue(msg.ismultipart())
|
||||
msg = mime.message_from_file(open('test/'+fname,"r"))
|
||||
self.failUnless(msg.ismultipart())
|
||||
parts = msg.get_payload()
|
||||
self.assertTrue(len(parts) == 2)
|
||||
self.failUnless(len(parts) == 2)
|
||||
txt1 = parts[0].get_payload()
|
||||
self.assertTrue(txt1.rstrip() == samp1_txt1,txt1)
|
||||
with open('test/missingboundary',"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
self.failUnless(txt1.rstrip() == samp1_txt1,txt1)
|
||||
msg = mime.message_from_file(open('test/missingboundary',"r"))
|
||||
# should get no exception as long as we don't try to parse
|
||||
# message attachments
|
||||
mime.defang(msg,scan_rfc822=False)
|
||||
with open('test/missingboundary.out','wb') as fp:
|
||||
msg.dump(fp)
|
||||
with open('test/missingboundary',"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg.dump(open('test/missingboundary.out','w'))
|
||||
msg = mime.message_from_file(open('test/missingboundary',"r"))
|
||||
try:
|
||||
mime.defang(msg)
|
||||
# python 2.4 doesn't get exceptions on missing boundaries, and
|
||||
# if message is modified, output is readable by mail clients
|
||||
if sys.hexversion < 0x02040000:
|
||||
self.fail('should get boundary error parsing bad rfc822 attachment')
|
||||
except errors.BoundaryError:
|
||||
except Errors.BoundaryError:
|
||||
pass
|
||||
|
||||
def testDefang(self,vname='virus1',part=1,
|
||||
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
|
||||
try:
|
||||
with self.zf.open(vname,"r") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
except KeyError:
|
||||
with open('test/'+vname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg = mime.message_from_file(open('test/'+vname,"r"))
|
||||
mime.defang(msg,scan_zip=True)
|
||||
self.assertTrue(msg.ismodified(),"virus not removed")
|
||||
self.failUnless(msg.ismodified(),"virus not removed")
|
||||
oname = vname + '.out'
|
||||
with open('test/'+oname,"wb") as fp:
|
||||
msg.dump(fp)
|
||||
with open('test/'+oname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg.dump(open('test/'+oname,"w"))
|
||||
msg = mime.message_from_file(open('test/'+oname,"r"))
|
||||
txt2 = msg.get_payload()
|
||||
if type(txt2) == list:
|
||||
txt2 = txt2[part].get_payload()
|
||||
self.assertTrue(
|
||||
self.failUnless(
|
||||
txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
|
||||
|
||||
def testDefang3(self):
|
||||
@@ -106,60 +100,51 @@ class MimeTestCase(unittest.TestCase):
|
||||
|
||||
# virus6 has no parts - the virus is directly inline
|
||||
def testDefang6(self,vname="virus6",fname='FAX20.exe'):
|
||||
with self.zf.open(vname,"r") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg = mime.message_from_file(open('test/'+vname,"r"))
|
||||
mime.defang(msg)
|
||||
oname = vname + '.out'
|
||||
with open('test/'+oname,"wb") as fp:
|
||||
msg.dump(fp)
|
||||
with open('test/'+oname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
self.assertFalse(msg.ismultipart())
|
||||
msg.dump(open('test/'+oname,"w"))
|
||||
msg = mime.message_from_file(open('test/'+oname,"r"))
|
||||
self.failIf(msg.ismultipart())
|
||||
txt2 = msg.get_payload()
|
||||
self.assertTrue(txt2 == mime.virus_msg % \
|
||||
self.failUnless(txt2 == mime.virus_msg % \
|
||||
(fname,hostname,None),txt2)
|
||||
|
||||
# honey virus has a sneaky ASP payload which is parsed correctly
|
||||
# by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1
|
||||
def testDefang7(self,vname="honey",fname='story[1].scr'):
|
||||
with open('test/'+vname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg = mime.message_from_file(open('test/'+vname,"r"))
|
||||
mime.defang(msg)
|
||||
oname = vname + '.out'
|
||||
with open('test/'+oname,"wb") as fp:
|
||||
msg.dump(fp)
|
||||
with open('test/'+oname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg.dump(open('test/'+oname,"w"))
|
||||
msg = mime.message_from_file(open('test/'+oname,"r"))
|
||||
parts = msg.get_payload()
|
||||
txt2 = parts[1].get_payload()
|
||||
txt3 = parts[2].get_payload()
|
||||
self.assertTrue(txt2.rstrip()+'\n' == mime.virus_msg % \
|
||||
self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \
|
||||
(fname,hostname,None),txt2)
|
||||
if txt3 != '':
|
||||
self.assertTrue(txt3.rstrip()+'\n' == mime.virus_msg % \
|
||||
self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \
|
||||
('story[1].asp',hostname,None),txt3)
|
||||
|
||||
def testParse2(self,fname="spam7"):
|
||||
with open('test/'+fname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
self.assertTrue(msg.ismultipart())
|
||||
msg = mime.message_from_file(open('test/'+fname,"r"))
|
||||
self.failUnless(msg.ismultipart())
|
||||
parts = msg.get_payload()
|
||||
self.assertTrue(len(parts) == 2)
|
||||
self.failUnless(len(parts) == 2)
|
||||
name = parts[1].getname()
|
||||
self.assertTrue(name == "Jim&amp;Girlz.jpg","name=%s"%name)
|
||||
self.failUnless(name == "Jim&amp;Girlz.jpg","name=%s"%name)
|
||||
|
||||
def testZip(self,vname="zip1",fname='zip.zip'):
|
||||
self.testDefang(vname,1,'zip.zip')
|
||||
# test scan_zip flag
|
||||
with open('test/'+vname,"rb") as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg = mime.message_from_file(open('test/'+vname,"r"))
|
||||
mime.defang(msg,scan_zip=False)
|
||||
self.assertFalse(msg.ismodified())
|
||||
self.failIf(msg.ismodified())
|
||||
# test ignoring empty zip (often found in DSNs)
|
||||
with open('test/zip2','rb') as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg = mime.message_from_file(open('test/zip2','r'))
|
||||
mime.defang(msg,scan_zip=True)
|
||||
self.assertFalse(msg.ismodified())
|
||||
self.failIf(msg.ismodified())
|
||||
# test corrupt zip (often an EXE named as a ZIP)
|
||||
self.testDefang('zip3',1,'zip.zip')
|
||||
# test zip within zip
|
||||
@@ -176,51 +161,22 @@ class MimeTestCase(unittest.TestCase):
|
||||
mime.check_html(msg)
|
||||
# don't let a tricky virus slip one past us
|
||||
msg = msg.get_submsg()
|
||||
if isinstance(msg,email.message.Message):
|
||||
if isinstance(msg,email.Message.Message):
|
||||
return mime.check_attachments(msg,self._chk_attach)
|
||||
return Milter.CONTINUE
|
||||
|
||||
def testCheckAttach(self,fname="test1"):
|
||||
# test1 contains a very long filename
|
||||
with open('test/'+fname,'rb') as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
msg = mime.message_from_file(open('test/'+fname,'r'))
|
||||
mime.defang(msg,scan_zip=True)
|
||||
self.assertFalse(msg.ismodified())
|
||||
with open('test/test2','rb') as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
self.failIf(msg.ismodified())
|
||||
msg = mime.message_from_file(open('test/tmpytgcE5.fail','r'))
|
||||
rc = mime.check_attachments(msg,self._chk_attach)
|
||||
self.assertEqual(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
|
||||
self.assertEqual(rc,Milter.CONTINUE)
|
||||
|
||||
def test_getnames(self):
|
||||
names = []
|
||||
self.sawpif = False
|
||||
def do_part(m):
|
||||
n = m.getnames()
|
||||
a = names
|
||||
a += n
|
||||
return Milter.CONTINUE
|
||||
def chk_part(m):
|
||||
for k,n in m.getnames():
|
||||
if n and n.lower().endswith('.pif'):
|
||||
self.sawpif = True
|
||||
s = m.get_submsg()
|
||||
print(m.get_content_type(),type(s),'modified:',m.ismodified())
|
||||
if isinstance(s,email.message.Message):
|
||||
return mime.check_attachments(s,chk_part)
|
||||
return Milter.CONTINUE
|
||||
|
||||
with self.zf.open('virus7','r') as fp:
|
||||
msg = mime.message_from_file(fp)
|
||||
self.assertTrue(msg.ismultipart())
|
||||
mime.check_attachments(msg,do_part)
|
||||
self.assertTrue(('filename','application.pif') in names)
|
||||
self.assertFalse(self.sawpif)
|
||||
mime.check_attachments(msg,chk_part)
|
||||
self.assertTrue(self.sawpif)
|
||||
self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
|
||||
self.assertEquals(rc,Milter.CONTINUE)
|
||||
|
||||
def testHTML(self,fname=""):
|
||||
result = StringIO()
|
||||
result = StringIO.StringIO()
|
||||
filter = mime.HTMLScriptFilter(result)
|
||||
msg = """<! Illegal declaration used as comment>
|
||||
<![if conditional]> Optional SGML <![endif]>
|
||||
@@ -229,19 +185,17 @@ class MimeTestCase(unittest.TestCase):
|
||||
script = "<script lang=javascript> Dangerous script </script>"
|
||||
filter.feed(msg + script)
|
||||
filter.close()
|
||||
#print(result.getvalue())
|
||||
#print('---')
|
||||
#print(msg + filter.msg)
|
||||
self.assertTrue(result.getvalue() == msg + filter.msg)
|
||||
#print result.getvalue()
|
||||
self.failUnless(result.getvalue() == msg + filter.msg)
|
||||
|
||||
def suite(): return unittest.TestLoader().loadTestsFromTestCase(MimeTestCase)
|
||||
def suite(): return unittest.makeSuite(MimeTestCase,'test')
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
unittest.main()
|
||||
else:
|
||||
for fname in sys.argv[1:]:
|
||||
with open(fname,'rb') as fp:
|
||||
fp = open(fname,'r')
|
||||
msg = mime.message_from_file(fp)
|
||||
mime.defang(msg,scan_zip=True)
|
||||
print(msg.as_string())
|
||||
print msg.as_string()
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
from __future__ import print_function
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from Milter.policy import MTAPolicy
|
||||
|
||||
class Config(object):
|
||||
def __init__(self):
|
||||
self.access_file='test/access.db'
|
||||
self.access_file_nulls=True
|
||||
self.access_file_colon = False
|
||||
|
||||
class PolicyTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config = Config()
|
||||
if os.access('test/access',os.R_OK):
|
||||
if not os.path.exists('test/access.db') or \
|
||||
os.path.getmtime('test/access') > os.path.getmtime('test/access.db'):
|
||||
cmd = 'tr : ! <test/access | makemap hash test/access.db'
|
||||
if os.system(cmd):
|
||||
print('failed!')
|
||||
else:
|
||||
print("Missing test/access")
|
||||
|
||||
def testPolicy(self):
|
||||
self.config.access_file_colon = False
|
||||
self.config.access_file_nulls = False # FIXME: test old and new bsddb
|
||||
with MTAPolicy('good@example.com',conf=self.config) as p:
|
||||
pol = p.getPolicy('smtp-auth')
|
||||
self.assertEqual(pol,'OK')
|
||||
with MTAPolicy('bad@example.com',conf=self.config) as p:
|
||||
pol = p.getPolicy('smtp-auth')
|
||||
self.assertEqual(pol,'REJECT')
|
||||
with MTAPolicy('bad@bad.example.com',conf=self.config) as p:
|
||||
pol = p.getPolicy('smtp-auth')
|
||||
self.assertEqual(pol,None)
|
||||
with MTAPolicy('any@random.com',conf=self.config) as p:
|
||||
pol = p.getPolicy('smtp-test')
|
||||
self.assertEqual(pol,'REJECT')
|
||||
with MTAPolicy('foo@bar.baz.com',conf=self.config) as p:
|
||||
pol = p.getPolicy('smtp-test')
|
||||
self.assertEqual(pol,'WILDCARD')
|
||||
|
||||
def suite(): return unittest.TestLoader().loadTestsFromTestCase(PolicyTestCase)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
unittest.main()
|
||||
else:
|
||||
a = sys.argv[1:]
|
||||
while len(a) >= 2:
|
||||
e,k = a[:2]
|
||||
with MTAPolicy(e,conf=Config()) as p:
|
||||
pol = p.getPolicy(k)
|
||||
print(pol)
|
||||
a = a[2:]
|
||||
+113
-111
@@ -1,106 +1,113 @@
|
||||
import unittest
|
||||
import Milter
|
||||
import sample
|
||||
import template
|
||||
import mime
|
||||
import zipfile
|
||||
from Milter.test import TestBase
|
||||
from Milter.testctx import TestCtx
|
||||
import rfc822
|
||||
import StringIO
|
||||
|
||||
class TestMilter(TestBase,sample.sampleMilter):
|
||||
class TestMilter(sample.sampleMilter):
|
||||
|
||||
_protocol = 0
|
||||
def __init__(self):
|
||||
TestBase.__init__(self)
|
||||
sample.sampleMilter.__init__(self)
|
||||
self.logfp = open("test/milter.log","a")
|
||||
|
||||
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):
|
||||
|
||||
def setUp(self):
|
||||
self.zf = zipfile.ZipFile('test/virus.zip','r')
|
||||
self.zf.setpassword(b'denatured')
|
||||
|
||||
def tearDown(self):
|
||||
self.zf.close()
|
||||
self.zf = None
|
||||
|
||||
def testTemplate(self,fname='test2'):
|
||||
ctx = TestCtx()
|
||||
Milter.factory = template.myMilter
|
||||
ctx._setsymval('{auth_authen}','batman')
|
||||
ctx._setsymval('{auth_type}','batcomputer')
|
||||
ctx._setsymval('j','mailhost')
|
||||
count = 10
|
||||
while count > 0:
|
||||
rc = ctx._connect(helo='milter-template.example.org')
|
||||
self.assertEqual(rc,Milter.CONTINUE)
|
||||
with open('test/'+fname,'rb') as fp:
|
||||
rc = ctx._feedFile(fp)
|
||||
milter = ctx.getpriv()
|
||||
self.assertFalse(ctx._bodyreplaced,"Message body replaced")
|
||||
ctx._close()
|
||||
count -= 1
|
||||
|
||||
def testHeader(self,fname='utf8'):
|
||||
ctx = TestCtx()
|
||||
Milter.factory = sample.sampleMilter
|
||||
ctx._setsymval('{auth_authen}','batman')
|
||||
ctx._setsymval('{auth_type}','batcomputer')
|
||||
ctx._setsymval('j','mailhost')
|
||||
rc = ctx._connect()
|
||||
self.assertEqual(rc,Milter.CONTINUE)
|
||||
with open('test/'+fname,'rb') as fp:
|
||||
rc = ctx._feedFile(fp)
|
||||
milter = ctx.getpriv()
|
||||
self.assertFalse(ctx._bodyreplaced,"Message body replaced")
|
||||
fp = ctx._body
|
||||
with open('test/'+fname+".tstout","wb") as ofp:
|
||||
ofp.write(fp.getvalue())
|
||||
ctx._close()
|
||||
|
||||
def testCtx(self,fname='virus1'):
|
||||
ctx = TestCtx()
|
||||
Milter.factory = sample.sampleMilter
|
||||
ctx._setsymval('{auth_authen}','batman')
|
||||
ctx._setsymval('{auth_type}','batcomputer')
|
||||
ctx._setsymval('j','mailhost')
|
||||
rc = ctx._connect()
|
||||
self.assertTrue(rc == Milter.CONTINUE)
|
||||
with self.zf.open(fname) as fp:
|
||||
rc = ctx._feedFile(fp)
|
||||
milter = ctx.getpriv()
|
||||
# self.assertTrue(milter.user == 'batman',"getsymval failed: "+
|
||||
# "%s != %s"%(milter.user,'batman'))
|
||||
self.assertEqual(milter.user,'batman')
|
||||
self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed")
|
||||
self.assertTrue(rc == Milter.ACCEPT)
|
||||
self.assertTrue(ctx._bodyreplaced,"Message body not replaced")
|
||||
fp = ctx._body
|
||||
with open('test/'+fname+".tstout","wb") as f:
|
||||
f.write(fp.getvalue())
|
||||
#self.assertTrue(fp.getvalue() == open("test/virus1.out","r").read())
|
||||
fp.seek(0)
|
||||
msg = mime.message_from_file(fp)
|
||||
s = msg.get_payload(1).get_payload()
|
||||
milter.log(s)
|
||||
ctx._close()
|
||||
|
||||
def testDefang(self,fname='virus1'):
|
||||
milter = TestMilter()
|
||||
milter.setsymval('{auth_authen}','batman')
|
||||
milter.setsymval('{auth_type}','batcomputer')
|
||||
milter.setsymval('j','mailhost')
|
||||
rc = milter.connect()
|
||||
self.assertTrue(rc == Milter.CONTINUE)
|
||||
with self.zf.open(fname) as fp:
|
||||
rc = milter.feedFile(fp)
|
||||
self.assertTrue(milter.user == 'batman',"getsymval failed")
|
||||
# setsymlist not working in TestBase
|
||||
#self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed")
|
||||
self.assertTrue(rc == Milter.ACCEPT)
|
||||
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
|
||||
self.failUnless(rc == Milter.CONTINUE)
|
||||
rc = milter.feedMsg(fname)
|
||||
self.failUnless(rc == Milter.ACCEPT)
|
||||
self.failUnless(milter.bodyreplaced,"Message body not replaced")
|
||||
fp = milter._body
|
||||
with open('test/'+fname+".tstout","wb") as f:
|
||||
f.write(fp.getvalue())
|
||||
#self.assertTrue(fp.getvalue() == open("test/virus1.out","r").read())
|
||||
open('test/'+fname+".tstout","w").write(fp.getvalue())
|
||||
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
|
||||
fp.seek(0)
|
||||
msg = mime.message_from_file(fp)
|
||||
s = msg.get_payload(1).get_payload()
|
||||
@@ -111,38 +118,33 @@ class BMSMilterTestCase(unittest.TestCase):
|
||||
milter = TestMilter()
|
||||
milter.connect('somehost')
|
||||
rc = milter.feedMsg(fname)
|
||||
self.assertTrue(rc == Milter.ACCEPT)
|
||||
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.")
|
||||
self.failUnless(rc == Milter.ACCEPT)
|
||||
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
|
||||
fp = milter._body
|
||||
with open('test/'+fname+".tstout","wb") as f:
|
||||
f.write(fp.getvalue())
|
||||
open('test/'+fname+".tstout","w").write(fp.getvalue())
|
||||
milter.close()
|
||||
|
||||
def testDefang2(self):
|
||||
milter = TestMilter()
|
||||
milter.connect('somehost')
|
||||
rc = milter.feedMsg('samp1')
|
||||
self.assertTrue(rc == Milter.ACCEPT)
|
||||
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.")
|
||||
with self.zf.open("virus3") as fp:
|
||||
rc = milter.feedFile(fp)
|
||||
self.assertTrue(rc == Milter.ACCEPT)
|
||||
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
|
||||
self.failUnless(rc == Milter.ACCEPT)
|
||||
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
|
||||
rc = milter.feedMsg("virus3")
|
||||
self.failUnless(rc == Milter.ACCEPT)
|
||||
self.failUnless(milter.bodyreplaced,"Message body not replaced")
|
||||
fp = milter._body
|
||||
with open("test/virus3.tstout","wb") as f:
|
||||
f.write(fp.getvalue())
|
||||
#self.assertTrue(fp.getvalue() == open("test/virus3.out","r").read())
|
||||
with self.zf.open("virus6") as fp:
|
||||
rc = milter.feedFile(fp)
|
||||
self.assertTrue(rc == Milter.ACCEPT)
|
||||
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
|
||||
self.assertTrue(milter._headerschanged,"Message headers not adjusted")
|
||||
open("test/virus3.tstout","w").write(fp.getvalue())
|
||||
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
|
||||
rc = milter.feedMsg("virus6")
|
||||
self.failUnless(rc == Milter.ACCEPT)
|
||||
self.failUnless(milter.bodyreplaced,"Message body not replaced")
|
||||
self.failUnless(milter.headerschanged,"Message headers not adjusted")
|
||||
fp = milter._body
|
||||
with open("test/virus6.tstout","wb") as f:
|
||||
f.write(fp.getvalue())
|
||||
open("test/virus6.tstout","w").write(fp.getvalue())
|
||||
milter.close()
|
||||
|
||||
def suite(): return unittest.TestLoader().loadTestsFromTestCase(BMSMilterTestCase)
|
||||
def suite(): return unittest.makeSuite(BMSMilterTestCase,'test')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+13
-29
@@ -1,11 +1,9 @@
|
||||
from __future__ import print_function
|
||||
import unittest
|
||||
import doctest
|
||||
import os
|
||||
import Milter.utils
|
||||
from Milter.cache import AddrCache
|
||||
from Milter.dynip import is_dynip
|
||||
from Milter.pyip6 import inet_ntop
|
||||
|
||||
class AddrCacheTestCase(unittest.TestCase):
|
||||
|
||||
@@ -13,7 +11,6 @@ class AddrCacheTestCase(unittest.TestCase):
|
||||
self.fname = 'test.dat'
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(self.fname):
|
||||
os.remove(self.fname)
|
||||
|
||||
def testAdd(self):
|
||||
@@ -21,43 +18,30 @@ class AddrCacheTestCase(unittest.TestCase):
|
||||
cache['foo@bar.com'] = None
|
||||
cache.addperm('baz@bar.com')
|
||||
cache['temp@bar.com'] = 'testing'
|
||||
self.assertTrue(cache.has_key('foo@bar.com'))
|
||||
self.assertTrue(not cache.has_key('hello@bar.com'))
|
||||
self.assertTrue('baz@bar.com' in cache)
|
||||
self.assertEqual(cache['temp@bar.com'],'testing')
|
||||
self.failUnless(cache.has_key('foo@bar.com'))
|
||||
self.failUnless(not cache.has_key('hello@bar.com'))
|
||||
self.failUnless('baz@bar.com' in cache)
|
||||
self.assertEquals(cache['temp@bar.com'],'testing')
|
||||
s = open(self.fname).readlines()
|
||||
self.assertTrue(len(s) == 2)
|
||||
self.assertTrue(s[0].startswith('foo@bar.com '))
|
||||
self.assertEqual(s[1].strip(),'baz@bar.com')
|
||||
self.failUnless(len(s) == 2)
|
||||
self.failUnless(s[0].startswith('foo@bar.com '))
|
||||
self.assertEquals(s[1].strip(),'baz@bar.com')
|
||||
# check that new result overrides old
|
||||
cache['temp@bar.com'] = None
|
||||
self.assertTrue(not cache['temp@bar.com'])
|
||||
self.failUnless(not cache['temp@bar.com'])
|
||||
|
||||
def testDomain(self):
|
||||
with open(self.fname,'w') as fp:
|
||||
print('spammer.com',file=fp)
|
||||
fp = open(self.fname,'w')
|
||||
print >>fp,'spammer.com'
|
||||
fp.close()
|
||||
cache = AddrCache(fname=self.fname)
|
||||
cache.load(self.fname,30)
|
||||
self.assertTrue('spammer.com' in cache)
|
||||
|
||||
def testParseHeader(self):
|
||||
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
|
||||
h = Milter.utils.parse_header(s)
|
||||
self.assertEqual(h,'Last Few Coldplay Album Artworks Available\x00')
|
||||
s='=?iso-8859-1?Q?Peter_=D8rum?= <orum@ditas.dk>'
|
||||
h = Milter.utils.parse_header(s)
|
||||
self.assertEqual(h,'Peter \xd8rum <orum@ditas.dk>')
|
||||
|
||||
@unittest.expectedFailure
|
||||
def testParseAddress(self):
|
||||
s = Milter.utils.parseaddr('a(WRONG)@b')
|
||||
self.assertEqual(s,('WRONG', 'a@b'))
|
||||
self.failUnless('spammer.com' in cache)
|
||||
|
||||
def suite():
|
||||
s = unittest.TestLoader().loadTestsFromTestCase(AddrCacheTestCase)
|
||||
s = unittest.makeSuite(AddrCacheTestCase,'test')
|
||||
s.addTest(doctest.DocTestSuite(Milter.utils))
|
||||
s.addTest(doctest.DocTestSuite(Milter.dynip))
|
||||
s.addTest(doctest.DocTestSuite(Milter.pyip6))
|
||||
return s
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user