Compare commits

...

104 Commits

Author SHA1 Message Date
Stuart D. Gathman 10fdccf366 Release 1.0.4 2019-04-17 19:07:50 -04:00
Stuart D. Gathman 7d097fa4a0 start.sh superceded by daemonize on EL6 and systemd elsewhere. 2019-04-17 17:25:06 -04:00
Stuart D. Gathman ca81502c85 Remove milter.path no longer needed. Unified rpm spec. 2019-04-17 17:16:39 -04:00
Ralph Seichter 50356d4710 Fix for compilation error on macOS 10.14 (Mojave) (#31)
This change ensures that arpa/inet.h is included when building
miltermodule.c on macOS 10.14. See
https://github.com/sdgathman/pymilter/issues/30

Signed-off-by: Ralph Seichter <github@seichter.de>
2019-04-17 11:53:56 -04:00
dkg ec3fa46799 Correctly document that body callback chunks are in bytes (#28)
https://github.com/sdgathman/pymilter/issues/12 says "Obviously, body
and replacebody are bytes" and milter_wrap_body in miltermodule.c
says:

   arglist = Py_BuildValue("(Oy#)", c, bodyp, bodylen);
…

So pymilter should sport the correct documentation.
2019-02-22 15:54:01 -05:00
Stuart D. Gathman 04e0b15640 Import full path on py3 2019-02-22 15:54:01 -05:00
Stuart D. Gathman ff6a07ef10 Misspelling - Scott Kitterman 2018-12-26 10:31:20 -05:00
Stuart D. Gathman 7dfda0a3bd More #ifdef consolidation. 2018-12-24 18:20:26 -05:00
Pino Toscano 183ce91a61 Include arpa/inet.h on any GNU libc platform (#24)
This header is provided by GNU libc on any platform, so include it
unconditionally if __GLIBC__ is defined.

Fixes #23.
2018-12-24 17:42:13 -05:00
Stuart D. Gathman d30918aca7 Fix python3 unit tests 2018-12-23 23:38:15 -05:00
Stuart D. Gathman 4e8fda517c Fix getsymval for python3 2018-12-23 23:16:42 -05:00
Stuart D. Gathman 2194d8fd96 setsymlist not called yet in TestMilter 2018-12-23 23:03:46 -05:00
Stuart D. Gathman 52e1b4ae32 Move sys to top of mime.py 2018-12-23 22:31:20 -05:00
Stuart D. Gathman 55e5378659 Move py3 sgmllib to an internal module - Scott Kitterman 2018-12-23 20:29:01 -05:00
Stuart D. Gathman f9b2241ec6 Improve readability of module init, setitem. Move old changes to ChangeLog 2018-12-23 20:00:24 -05:00
Stuart D. Gathman 62783fbbfd Convert python3 patch for miltermodule.c to #ifdef - Scott Kitterman 2018-12-23 19:46:59 -05:00
Stuart D. Gathman d521665f75 Working testctx, still need to redirect logging in testsample.py 2018-08-18 17:49:37 -04:00
Stuart D. Gathman 55eb05e526 Change deprecated failUnless to assertTrue 2018-08-05 00:34:06 -04:00
Stuart D. Gathman fc008f6db0 Update documentation 2018-08-04 14:09:23 -04:00
Stuart D. Gathman 96cd9ac263 Request compile error on implicit function declarations to save lots
of debugging time.
2018-01-09 21:52:39 -05:00
jcea 10471faa7d Correctly support IPv6 in Solaris (#17)
I can't see how adding one more OS to the manual list can hurt for now, so in goes the PR.  Thanks.
2018-01-09 14:46:11 -05:00
Stuart D. Gathman 36750bac78 Fix some python3 syntax errors 2017-04-20 17:00:14 -04:00
Stuart D. Gathman 42e7a02638 Missed version 2016-12-13 14:17:34 -05:00
Stuart D. Gathman bc9d8c622b Release 1.0.2 2016-12-13 14:15:56 -05:00
Stuart D. Gathman 2fa952e108 Make test cases work. (Previous commit incomplete) 2016-12-02 00:19:27 -05:00
Stuart D. Gathman 381e906b6a Implement setsymlist decorator and test framework 2016-12-01 23:59:31 -05:00
Stuart D. Gathman 207278479f Another setsymlist misspelling 2016-11-17 09:12:16 -05:00
Stuart D. Gathman a0bd76cded Gerhard Schmidt corrected the FreeBSD test via pymilter list. 2016-11-08 08:12:52 -05:00
Stuart D. Gathman 8e96c23ddc Update email and url in setup.py 2016-11-07 20:10:54 -05:00
Stuart D. Gathman 5ec4e2b34d Update email 2016-10-29 09:51:13 -04:00
Stuart D. Gathman 28c3a6afd6 update .gitignore 2016-10-17 23:48:09 -04:00
Stuart D. Gathman 36df47f019 Move expected failure to testutils.py where it can be tagged.
Change to example.com/40 for iniplist with leftover bits to ignore.
2016-10-17 23:38:49 -04:00
Tom Hendrikx e5c03665e9 Fixes (#8)
* Fix tests that fail

Solves #5

* FIx syntax errors
2016-10-17 23:23:55 -04:00
Stuart D. Gathman ea9ca0c12a Pass bytes to body callback. 2016-10-17 20:42:46 -04:00
Stuart D. Gathman fb1da3b12b Call PyType_Ready 2016-10-17 19:15:07 -04:00
Stuart D. Gathman 74d33126b5 Create src.tar from git-archive 2016-09-30 20:12:41 -04:00
Stuart D. Gathman 834ef18c09 Really support python3 2016-09-29 00:41:42 -04:00
Stuart D. Gathman a36dcbfcdd All unittests pass in py2 and py3! 2016-09-29 00:27:42 -04:00
Stuart D. Gathman 626d5ae20e Add ported sgmllib module to keep SGMLFilter working for now. 2016-09-29 00:19:26 -04:00
Stuart D. Gathman 9d7645c1a5 Binary file handling and extension scanning work in py3. 2016-09-26 18:57:56 -04:00
Stuart D. Gathman eaa6a43f0d Missed some tabs. 2016-09-26 18:56:57 -04:00
Stuart D. Gathman 032efebaed Use with statement to close test files. 2016-09-26 18:56:04 -04:00
Stuart D. Gathman edef64a422 Binary file output for emails 2016-09-26 18:55:25 -04:00
Stuart D. Gathman 5361315634 Minor fixes to make test suite pass for python2 after binary/text file changes. 2016-09-26 13:36:22 -04:00
Stuart D. Gathman 755f3edb2b Use binary files for email. Still some issues to work out with payloads. 2016-09-22 21:57:14 -04:00
Stuart D. Gathman bae79a4f1c Fix lots of py3isms. Email package is borked in py3, however. 2016-09-21 17:24:37 -04:00
Stuart D. Gathman 70fa47dac6 thread renamed to _thread in python3 2016-09-21 11:35:01 -04:00
Stuart D. Gathman b4931bebbd Update source URL to github 2016-09-21 00:50:20 -04:00
Stuart D. Gathman 604255a29c Release 1.0.1-1 2016-09-21 00:39:52 -04:00
Stuart D. Gathman 7e12680867 Builds on el6,el7,f24 2016-09-20 22:00:01 -04:00
Stuart D. Gathman d6337e565d Builds for f24 2016-09-20 18:36:59 -04:00
Stuart D. Gathman cddef88ed9 Python3 patch for miltermodule.c 2016-09-20 16:25:52 -04:00
Stuart D. Gathman 1337bf612b Make progress do nothing 2016-08-22 13:50:43 -04:00
Stuart D. Gathman bfd6f270da Merge branch 'master' of https://github.com/sdgathman/pymilter 2016-08-22 13:46:44 -04:00
Yudai Kato 6394b8714b add quarantine() and progress() as not implemented functions for now. (#6) 2016-08-22 13:46:08 -04:00
Stuart D. Gathman 547fb39f2a More python3 fixes. Run pyip6 doctests in test suite. 2016-08-11 15:38:07 -04:00
Stuart D. Gathman 6e2153454a Forgot to initialize TestBase._sender 2016-08-11 09:48:09 -04:00
Stuart D. Gathman ded1412294 Record new envfrom for TestMilter.chgfrom 2016-08-10 17:57:51 -04:00
Stuart D. Gathman 627a2be49f Merge pull request #4 from yudai09/fix/support_test_chgfrom
suport chgfrom() in TestMilter
2016-08-10 09:07:33 -04:00
Yudai Kato 4c9c168096 suport chgfrom() in TestMilter
see #3
2016-08-10 15:00:20 +09:00
Stuart D. Gathman ea84943f29 Fix StringIO 2016-07-26 10:06:56 -04:00
Stuart D. Gathman 999a446484 flush= not supported until python-3.3 2016-07-26 09:58:30 -04:00
Stuart D. Gathman 76eb93223c Use print function everywhere 2016-07-26 09:52:40 -04:00
Stuart D. Gathman 99552b40e9 Target python2.7 for master 2016-07-25 22:36:33 -04:00
Stuart D. Gathman bf17ff6a5c Use unicode literal to join unicode strings. 2016-07-25 22:35:20 -04:00
Stuart D. Gathman 32f3034b94 Add section to link projects using pymilter. 2016-07-25 22:33:29 -04:00
Stuart D. Gathman 3cdf7aa6a5 Fix test case 2016-07-25 22:28:35 -04:00
Stuart D. Gathman 728ac069cf Fix spurious cleanup error. 2016-07-25 22:12:27 -04:00
Stuart D. Gathman e28947c084 Update README 2016-07-24 21:41:43 -04:00
Stuart Gathman 5f76be956e Handle missing padding in encoded header 2015-10-02 18:53:07 +00:00
Stuart Gathman 3665be544f Test case for missing padding. 2015-10-02 18:25:27 +00:00
Stuart Gathman 1e8c90997b Link to related packages. 2015-06-24 04:31:14 +00:00
Stuart Gathman 2660540641 Copy sendmail-devel libmilter api into documention, since milter.org is gone. 2015-06-24 03:41:38 +00:00
Stuart Gathman 161b4c31e1 Fix header_leading_space, update doc version. 2015-02-27 01:04:27 +00:00
Stuart Gathman 9575547dad Fix bug from pyspf - caching server altering case of cached names. 2015-02-17 22:46:36 +00:00
Stuart Gathman 8dfda22cbd Add dns name support for iniplist() 2014-03-28 03:09:10 +00:00
Stuart Gathman 8f7c090879 Release 1.0-2 2014-03-01 23:38:51 +00:00
Stuart Gathman d69c002020 Release 1.0 2014-03-01 23:30:12 +00:00
Stuart Gathman 980dc5f599 pymilter SELinux policy as addon package 2013-06-26 22:24:02 +00:00
Stuart Gathman 8770262622 Initial selinux policy support 2013-06-26 18:28:49 +00:00
Stuart Gathman af49a7a45e Clean while exporting, and handle exporting IP6 2013-06-16 03:39:47 +00:00
Stuart Gathman fca8d83370 Import and export csv for converting existing greylist database. 2013-05-22 18:25:13 +00:00
Stuart Gathman f28cab2d1c Doc updates 2013-04-18 04:06:02 +00:00
Stuart Gathman 76424c7c3f Selinux policy additions. 2013-04-18 04:04:42 +00:00
Stuart Gathman 3e1754acff Call opensocket to check and remove unix domain sockets before starting. 2013-04-18 04:03:36 +00:00
Stuart Gathman 40de08925d Recognize IPv6 localhost. 2013-03-27 02:21:30 +00:00
Stuart Gathman 522a631192 Update Doxyfile 2013-03-22 18:12:50 +00:00
Stuart Gathman 5c8c189330 Remove bad setreply example, doc updates. 2013-03-19 21:25:10 +00:00
Stuart Gathman 5330047902 Move many configs to datadir 2013-03-15 23:04:38 +00:00
Stuart Gathman a8f373ea65 Allow ACCEPT as an exception policy. 2013-03-15 20:50:01 +00:00
Stuart Gathman f357be1e99 Release 0.9.8 2013-03-14 22:11:26 +00:00
Stuart Gathman 84eeecf9a6 tabnanny, restore missing test email 2013-03-12 01:46:08 +00:00
Stuart Gathman a180b212c6 Call negotiate from test mixin so that the noreply exception works. 2013-03-11 23:52:21 +00:00
Stuart Gathman bd0df5d77a Accept any combination of lists and space separated strings. 2013-03-11 22:21:14 +00:00
Stuart Gathman 34746823f7 Use python locking to avoid busy wait. 2013-03-09 22:23:27 +00:00
Stuart Gathman baeddd9fa5 Make TestBase members private, fix getsymlist misspelling. 2013-03-09 05:42:14 +00:00
Stuart Gathman 4854f95b59 Handle varargs in setreply 2013-03-09 00:26:03 +00:00
Stuart Gathman 242f2fa78f Better untrapped exception message. const char for doc comments. 2013-03-09 00:25:23 +00:00
Stuart Gathman 1e0324399b Add mixin class for unit testing milters. 2013-03-08 17:37:20 +00:00
Stuart Gathman 078d9f2078 Read then write sqlite transactions must use BEGIN IMMEDIATE 2013-02-25 19:10:57 +00:00
Stuart Gathman ff06b5f1b4 Close Cursor objects explicitly 2013-02-17 05:13:38 +00:00
Stuart Gathman dd581f5d9a Optional sqlite3 greylist implementation. 2013-02-16 19:27:39 +00:00
Stuart Gathman 3fb9beb5c0 Testcase for greylist 2013-02-16 05:40:46 +00:00
Stuart Gathman b12c4c9746 Doc updates. 2013-01-13 01:46:17 +00:00
41 changed files with 21979 additions and 1573 deletions
+8
View File
@@ -0,0 +1,8 @@
*.pyc
build/
test/*.out
test/*.tstout
test/*.log
test.db
dist
MANIFEST
+2 -3
View File
@@ -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@bmsi.com) took that
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
kludge and added threading and context objects to it, wrote a proper OO
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,7 +7,6 @@ real, usable Python extension.
Other contributors (in random order):
Daniel Troeder
for pointing out a typo in @noreply
arkanes@irc.freenode.net
@@ -44,4 +43,4 @@ Business Management Systems - http://www.bmsi.com
for hosting the website, and providing paying clients who need milter service
so I can work on it as part of my day job.
If I have left anybody out, send me a reminder: stuart@bmsi.com
If I have left anybody out, send me a reminder: stuart@gathman.org
+210
View File
@@ -1,3 +1,213 @@
# 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.
#
+687 -638
View File
File diff suppressed because it is too large Load Diff
+102 -64
View File
@@ -8,12 +8,17 @@
# Copyright 2001,2009 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
__version__ = '0.9.7'
from __future__ import print_function
__version__ = '1.0.4'
import os
import re
import milter
import thread
try:
import thread
except:
# libmilter uses posix threads
import _thread as thread
from milter import *
from functools import wraps
@@ -21,12 +26,6 @@ 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,6 +48,12 @@ 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'%+')
@@ -107,7 +112,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_HEAD_LEADSPC flag. By default,
# enable_protocols() with the P_HDR_LEADSPC flag. By default,
# header continuation lines are collected and joined before getting
# sent to a milter. Headers modified or added by the milter are
# folded by the MTA as necessary according to its own standards.
@@ -125,7 +130,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_HEAD_LEADSPC)
return enable_protocols(klass,P_HDR_LEADSPC)
## Function decorator to disable callback methods.
# If the MTA supports it, tells the MTA not to invoke this callback,
@@ -142,6 +147,7 @@ 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'
@@ -174,6 +180,21 @@ def noreply(func):
wrapper.milter_protocol = nr_mask
return wrapper
## Function decorator to set macros used in a callback.
# By default, the MTA sends all macros defined for a callback.
# If some or all of these are unused, the bandwidth can be saved
# by listing the ones that are used.
# @since 1.0.2
def symlist(*syms):
if len(syms) > 5:
raise ValueError('@symlist limited to 5 macros by MTA: '+func.__name__)
def setsyms(func):
if func.__name__ not in MACRO_CALLBACKS:
raise ValueError('@symlist applied to non-symlist method: '+func.__name__)
func._symlist = syms
return func
return setsyms
## Disabled action exception.
# set_flags() can tell the MTA that this application will not use certain
# features (such as CHGFROM). This can also be negotiated for each
@@ -227,7 +248,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,SETSMLIST</code>.
# CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</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
@@ -263,7 +284,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="https://www.milter.org/developers/api/xxfi_connect">
# <a href="milter_api/xxfi_connect.html">
# 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.
@@ -300,7 +321,7 @@ class Base(object):
@nocallback
def hello(self,hostname): return CONTINUE
## Called when the SMTP client says MAIL FROM. Called by the
# <a href="https://www.milter.org/developers/api/xxfi_envfrom">
# <a href="milter_api/xxfi_envfrom.html">
# xxfi_envfrom</a> callback.
# Returning REJECT rejects the message, but not the connection.
# The sender is the "envelope" from as defined by
@@ -311,7 +332,7 @@ class Base(object):
@nocallback
def envfrom(self,f,*str): return CONTINUE
## Called when the SMTP client says RCPT TO. Called by the
# <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
# <a href="milter_api/xxfi_envrcpt.html">
# xxfi_envrcpt</a> callback.
# Returning REJECT rejects the current recipient, not the entire message.
# The recipient is the "envelope" recipient as defined by
@@ -371,13 +392,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="https://www.milter.org/developers/api/xxfi_negotiate">
# <a href="milter_api/xxfi_negotiate.html">
# xffi_negotiate</a> callback. This is an advanced callback,
# do not override unless you know what you are doing. Most
# negotiation can be done simply by using the supplied
@@ -394,11 +415,16 @@ 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:
except Exception as x:
# don't change anything if something went wrong
return ALL_OPTS
return CONTINUE
@@ -408,7 +434,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="https://www.milter.org/developers/api/smfi_getsymval">
# See <a href="milter_api/smfi_getsymval.html">
# smfi_getsymval</a> for default sendmail macros.
# @param sym the macro name
def getsymval(self,sym):
@@ -416,12 +442,21 @@ 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!
# is that if you carefully double any '%%', your message will be
# sent - but with the '%%' still doubled!
# See <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> for more information.
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
# @param msg The text part of the SMTP reply. If msg is None,
# an empty message is used.
# @param ml Optional additional message lines.
def setreply(self,rcode,xcode=None,msg=None,*ml):
for m in (msg,)+ml:
if 1 in [len(s)&1 for s in R.findall(m)]:
@@ -429,22 +464,38 @@ class Base(object):
return self._ctx.setreply(rcode,xcode,msg,*ml)
## Tell the MTA which macro names will be used.
# The <code>Milter.SETSMLIST</code> action flag must be set.
# This information can reduce the size of messages received from sendmail,
# and hence could reduce bandwidth between sendmail and your milter where
# that is a factor. The <code>Milter.SETSYMLIST</code> action flag must be
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT,
# M_DATA, M_EOM, M_EOH.
#
# May only be called from negotiate callback.
# @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)
# 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))
# Milter methods which can only be called from eom callback.
## Add a mail header field.
# Calls <a href="https://www.milter.org/developers/api/smfi_addheader">
# Calls <a href="milter_api/smfi_addheader.html">
# smfi_addheader</a>.
# The <code>Milter.ADDHDRS</code> action flag must be set.
#
@@ -458,7 +509,7 @@ class Base(object):
return self._ctx.addheader(field,value,idx)
## Change the value of a mail header field.
# Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">
# Calls <a href="milter_api/smfi_chgheader.html">
# smfi_chgheader</a>.
# The <code>Milter.CHGHDRS</code> action flag must be set.
#
@@ -472,7 +523,7 @@ class Base(object):
return self._ctx.chgheader(field,idx,value)
## Add a recipient to the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">
# Calls <a href="milter_api/smfi_addrcpt.html">
# 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
@@ -492,7 +543,7 @@ class Base(object):
raise DisabledAction("ADDRCPT_PAR")
return self._ctx.addrcpt(rcpt,params)
## Delete a recipient from the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">
# Calls <a href="milter_api/smfi_delrcpt.html">
# smfi_delrcpt</a>.
# The recipient should match one passed to the envrcpt callback.
# The <code>Milter.DELRCPT</code> action flag must be set.
@@ -505,7 +556,7 @@ class Base(object):
return self._ctx.delrcpt(rcpt)
## Replace the message body.
# Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">
# Calls <a href="milter_api/smfi_replacebody.html">
# smfi_replacebody</a>.
# The entire message body must be replaced.
# Call repeatedly with blocks of data until the entire body is transferred.
@@ -519,7 +570,7 @@ class Base(object):
return self._ctx.replacebody(body)
## Change the SMTP envelope sender address.
# Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">
# Calls <a href="milter_api/smfi_chgfrom.html">
# 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),
@@ -536,7 +587,7 @@ class Base(object):
return self._ctx.chgfrom(sender,params)
## Quarantine the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">
# Calls <a href="milter_api/smfi_quarantine.html">
# 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.
@@ -550,7 +601,7 @@ class Base(object):
return self._ctx.quarantine(reason)
## Tell the MTA to wait a bit longer.
# Calls <a href="https://www.milter.org/developers/api/smfi_progress">
# Calls <a href="milter_api/smfi_progress.html">
# smfi_progress</a>.
# Resets timeouts in the MTA that detect a "hung" milter.
def progress(self):
@@ -564,9 +615,9 @@ class Milter(Base):
## Provide simple logging to sys.stdout
def log(self,*msg):
print 'Milter:',
for i in msg: print i,
print
print('Milter:',end=None)
for i in msg: print(i,end=None)
print()
@noreply
def connect(self,hostname,family,hostaddr):
@@ -703,28 +754,7 @@ 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):
# 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)
def runmilter(name,socketname,timeout = 0,rmsock=True):
# The default flags set include everything
# milter.set_flags(milter.ADDHDRS)
@@ -755,6 +785,14 @@ def runmilter(name,socketname,timeout = 0):
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()
+20 -14
View File
@@ -10,6 +10,9 @@
# 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
@@ -43,8 +46,9 @@
# 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 plock import PLock
from Milter.plock import PLock
class AddrCache(object):
time_format = '%Y%b%d %H:%M:%S %Z'
@@ -72,8 +76,8 @@ class AddrCache(object):
except OSError:
fp = ()
for ln in fp:
try:
rcpt,ts = ln.strip().split(None,1)
try:
rcpt,ts = ln.strip().split(None,1)
try:
l = time.strptime(ts,AddrCache.time_format)
t = time.mktime(l)
@@ -84,11 +88,11 @@ class AddrCache(object):
except: # unparsable timestamp - likely garbage
changed = True
continue
except: # manual entry (no timestamp)
cache[ln.strip().lower()] = (now,None)
wfp.write(ln)
except: # manual entry (no timestamp)
cache[ln.strip().lower()] = (now,None)
wfp.write(ln)
if changed:
lock.commit(self.fname+'.old')
lock.commit(self.fname+'.old')
else:
lock.unlock()
except IOError:
@@ -126,13 +130,13 @@ class AddrCache(object):
ts,res = self.cache[lsender]
too_old = time.time() - self.age*24*60*60 # max age in days
if not ts or ts > too_old:
return res
return res
del self.cache[lsender]
raise KeyError, sender
except KeyError,x:
raise KeyError(sender)
except KeyError as x:
try:
user,host = sender.split('@',1)
return self.__getitem__(host)
user,host = sender.split('@',1)
return self.__getitem__(host)
except ValueError:
raise x
@@ -144,7 +148,8 @@ class AddrCache(object):
if not ts: return # already permanent
self.cache[lsender] = (None,res)
if not res:
print >>open(self.fname,'a'),sender
with open(self.fname,'a') as fp:
print(sender,file=fp)
def __setitem__(self,sender,res):
lsender = sender.lower()
@@ -152,7 +157,8 @@ class AddrCache(object):
self.cache[lsender] = (now,res)
if not res and self.fname:
s = time.strftime(AddrCache.time_format,time.localtime(now))
print >>open(self.fname,'a'),sender,s # log refreshed senders
with open(self.fname,'a') as fp:
print(sender,s,file=fp) # log refreshed senders
def __len__(self):
return len(self.cache)
+15 -13
View File
@@ -1,4 +1,5 @@
from ConfigParser import ConfigParser
import os.path
class MilterConfigParser(ConfigParser):
@@ -20,7 +21,7 @@ class MilterConfigParser(ConfigParser):
return [q.strip() for q in self.get(sect,opt).split(',')]
return []
def getaddrset(self,sect,opt):
def getaddrset(self,sect,opt,dir=''):
if not self.has_option(sect,opt):
return {}
s = self.get(sect,opt)
@@ -29,13 +30,14 @@ class MilterConfigParser(ConfigParser):
q = q.strip()
if q.startswith('file:'):
domain = q[5:].lower()
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
fname = os.path.join(dir,domain)
d[domain] = d.setdefault(domain,[]) + open(fname,'r').read().split()
else:
user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user)
user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user)
return d
def getaddrdict(self,sect,opt):
def getaddrdict(self,sect,opt,dir=''):
if not self.has_option(sect,opt):
return {}
d = {}
@@ -43,14 +45,14 @@ class MilterConfigParser(ConfigParser):
q = q.strip()
if self.has_option(sect,q):
l = self.get(sect,q)
for addr in l.split(','):
addr = addr.strip()
if addr.startswith('file:'):
fname = addr[5:]
for a in open(fname,'r').read().split():
d[a] = q
else:
d[addr] = q
for addr in l.split(','):
addr = addr.strip()
if addr.startswith('file:'):
fname = os.path.join(dir,addr[5:])
for a in open(fname,'r').read().split():
d[a] = q
else:
d[addr] = q
return d
def getdefault(self,sect,opt,default=None):
+10 -8
View File
@@ -1,6 +1,7 @@
## @package Milter.dns
# Provide a higher level interface to pydns.
from __future__ import print_function
import DNS
from DNS import DNSError
@@ -14,9 +15,9 @@ MAX_CNAME = 10
# @return a list of ((name,type),data) tuples
def DNSLookup(name, qtype):
try:
# To be thread safe, we create a fresh DnsRequest with
# each call. It would be more efficient to reuse
# a req object stored in a Session.
# To be thread safe, we create a fresh DnsRequest with
# each call. It would be more efficient to reuse
# a req object stored in a Session.
req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req()
#resp.show()
@@ -25,8 +26,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, x:
raise DNSError, str(x)
except IOError as x:
raise DNSError(str(x))
class Session(object):
"""A Session object has a simple cache with no TTL that is valid
@@ -73,6 +74,7 @@ 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
@@ -96,7 +98,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 in cnames:
if cname.lower().rstrip('.') in cnames:
raise DNSError('CNAME loop')
result = self.dns(cname, qtype, cnames=cnames)
if result:
@@ -119,5 +121,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))
+18 -14
View File
@@ -5,6 +5,9 @@
# 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
#
@@ -66,6 +69,7 @@
# 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
from email.Message import Message
@@ -114,17 +118,17 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
if a[0] == receiver:
return (553,'Fraudulent MX for %s: %s' % (domain,host))
if not (200 <= code <= 299):
raise smtplib.SMTPHeloError(code, resp)
raise smtplib.SMTPHeloError(code, resp)
if msg:
try:
smtp.sendmail('<%s>'%ourfrom,mailfrom,msg)
except smtplib.SMTPSenderRefused:
smtp.sendmail('<%s>'%ourfrom,mailfrom,msg)
except smtplib.SMTPSenderRefused:
# does not accept DSN, try postmaster (at the risk of mail loops)
smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg)
smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg)
else: # CBV
code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom)
if code != 250:
raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom)
code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom)
if code != 250:
raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom)
if isinstance(mailfrom,basestring):
mailfrom = [mailfrom]
badrcpts = {}
@@ -132,19 +136,19 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
code,resp = smtp.rcpt(rcpt)
if code not in (250,251):
badrcpts[rcpt] = (code,resp)# permanent error
smtp.quit()
smtp.quit()
if len(badrcpts) == 1:
return badrcpts.values()[0] # permanent error
if badrcpts:
return badrcpts
return None # success
except smtplib.SMTPRecipientsRefused,x:
except smtplib.SMTPRecipientsRefused as x:
if len(x.recipients) == 1:
return x.recipients.values()[0] # permanent error
return x.recipients
except smtplib.SMTPSenderRefused,x:
except smtplib.SMTPSenderRefused as x:
return x.args[:2] # does not accept DSN
except smtplib.SMTPDataError,x:
except smtplib.SMTPDataError as x:
return x.args # permanent error
except smtplib.SMTPException:
pass # any other error, try next MX
@@ -227,6 +231,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()))
+8 -7
View File
@@ -9,6 +9,7 @@
# 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}')
@@ -53,11 +54,11 @@ def is_dynip(host,addr):
if host.find(addr) >= 0: return True
if addr.find(':') >= 0: return False # IP6
a = addr.split('.')
ia = map(int,a)
ia = list(map(int,a))
h = host
m = ip3.findall(host)
if m:
g = map(int,m)[:4]
g = list(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
@@ -68,8 +69,8 @@ def is_dynip(host,addr):
if ia[2:] in (g[:2],g[-2:]): return True
for m in ip3.finditer(host):
if int(m.group()) == ia[3]:
h = host[:m.start()] + '<3>' + host[m.end():]
break
h = host[:m.start()] + '<3>' + host[m.end():]
break
if rehmac.search(h): return True
if host.find(''.join(a[:3])) >= 0: return True
if host.find(''.join(a[1:])) >= 0: return True
@@ -86,11 +87,11 @@ if __name__ == '__main__':
if a[3:5] == ['connect','from']:
host = a[5]
if host.startswith('[') and host.endswith(']'):
continue # no PTR
continue # no PTR
ip = a[7][2:-2]
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))
+55 -8
View File
@@ -1,3 +1,4 @@
from __future__ import print_function
import time
import shelve
import thread
@@ -18,13 +19,19 @@ def quoteAddress(s):
class Record(object):
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
def __init__(self):
now = time.time()
def __init__(self,timeinc=0):
now = time.time() + timeinc
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):
@@ -34,8 +41,38 @@ class Greylist(object):
self.greylist_retain = grey_retain * 24 * 3600 # days
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):
def check(self,ip,sender,recipient,timeinc=0):
"Return number of allowed messages for greylist triple."
sender = quoteAddress(sender)
recipient = quoteAddress(recipient)
@@ -45,15 +82,15 @@ class Greylist(object):
dbp = self.dbp
try:
r = dbp[key]
now = time.time()
now = time.time() + timeinc
if now > r.lastseen + self.greylist_retain:
# expired
log.debug('Expired greylist: %s',key)
r = Record()
r = Record(timeinc)
elif now < r.firstseen + self.greylist_time + 5:
# still greylisted
log.debug('Early greylist: %s',key)
#r = Record()
#r = Record(timeinc)
r.lastseen = now
elif r.cnt or now < r.firstseen + self.greylist_expire:
# in greylist window or active
@@ -63,12 +100,22 @@ class Greylist(object):
else:
# passed greylist window
log.debug('Late greylist: %s',key)
r = Record()
r = Record(timeinc)
dbp[key] = r
except:
r = Record()
r = Record(timeinc)
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()
+109
View File
@@ -0,0 +1,109 @@
import time
import logging
import urllib
import sqlite3
try:
import thread
except:
import _thread as thread
from datetime import datetime
log = logging.getLogger('milter.greylist')
_db_lock = thread.allocate_lock()
class Greylist(object):
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
self.ignoreLastByte = False
self.greylist_time = grey_time * 60 # minutes
self.greylist_expire = grey_expire * 3600 # hours
self.greylist_retain = grey_retain * 24 * 3600 # days
self.conn = sqlite3.connect(dbname)
self.conn.row_factory = sqlite3.Row
try:
self.conn.execute('''create table greylist(
ip text , sender text, recipient text,
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
primary key (ip,sender,recipient))''')
except: pass
def import_csv(self,fp):
import csv
rdr = csv.reader(fp)
cur = self.conn.execute('begin immediate')
try:
for r in rdr:
cur.execute('''insert into
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
values(?,?,?,?,?,?,?)''', r)
self.conn.commit()
finally:
cur.close();
def clean(self,timeinc=0):
"Delete records past the retention limit."
now = time.time() + timeinc - self.greylist_retain
cur = self.conn.cursor()
try:
cur.execute('delete from greylist where lastseen < ?',(now,))
cnt = cur.rowcount
self.conn.commit()
finally: cur.close()
return cnt
def check(self,ip,sender,recipient,timeinc=0):
"Return number of allowed messages for greylist triple."
_db_lock.acquire()
cur = self.conn.execute('begin immediate')
try:
cur.execute('''select firstseen,lastseen,cnt,umis from greylist where
ip=? and sender=? and recipient=?''',(ip,sender,recipient))
r = cur.fetchone()
now = time.time() + timeinc
cnt = 0
if not r:
cur.execute('''insert into
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
values(?,?,?,?,?,?,?)''', (ip,sender,recipient,now,now,0,None))
elif now > r['lastseen'] + self.greylist_retain:
# expired
log.debug('Expired greylist: %s:%s:%s',ip,sender,recipient)
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
where ip=? and sender=? and recipient=?''',
(now,now,0,None,ip,sender,recipient))
elif now < r['firstseen'] + self.greylist_time + 5:
# still greylisted
log.debug('Early greylist: %s:%s:%s',ip,sender,recipient)
#r = Record()
cur.execute('''update greylist set lastseen=?
where ip=? and sender=? and recipient=?''',
(now,ip,sender,recipient))
elif r['cnt'] or now < r['firstseen'] + self.greylist_expire:
# in greylist window or active
cnt = r['cnt'] + 1
cur.execute('''update greylist set lastseen=?,cnt=?
where ip=? and sender=? and recipient=?''',
(now,cnt,ip,sender,recipient))
log.debug('Active greylist(%d): %s:%s:%s',cnt,ip,sender,recipient)
else:
# passed greylist window
log.debug('Late greylist: %s:%s:%s',ip,sender,recipient)
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
where ip=? and sender=? and recipient=?''',
(now,now,0,None,ip,sender,recipient))
self.conn.commit()
finally:
cur.close()
_db_lock.release()
return cnt
def close(self):
self.conn.close()
if __name__ == '__main__':
import sys
g = Greylist(sys.argv[1])
try:
g.import_csv(sys.stdin)
finally: g.close()
+6 -6
View File
@@ -11,7 +11,7 @@ class PLock(object):
self.basename = basename
self.fp = None
def lock(self,lockname=None,mode=0660,strict_perms=False):
def lock(self,lockname=None,mode=0o660,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(0002)
u = os.umask(0o2)
try:
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
finally:
@@ -31,8 +31,8 @@ class PLock(object):
os.chown(self.lockname,-1,st.st_gid)
except:
if strict_perms:
self.unlock()
raise
self.unlock()
raise
return self.fp
def wlock(self,lockname=None):
@@ -46,12 +46,12 @@ 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:
try:
os.remove(backname)
os.remove(backname)
except OSError: pass
os.link(self.basename,backname)
os.rename(self.lockname,self.basename)
+25 -24
View File
@@ -6,6 +6,7 @@ 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
@@ -48,11 +49,11 @@ def inet_ntop(s):
e = n[:l]
for i in range(9-l):
if a[i:i+l] == e:
if i == 0:
return ':'+':%x'*(8-l) % a[l:]
if i == 8 - l:
return '%x:'*(8-l) % a[:-l] + ':'
return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:]
if i == 0:
return ':'+':%x'*(8-l) % a[l:]
if i == 8 - l:
return '%x:'*(8-l) % a[:-l] + ':'
return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:]
return "%x:%x:%x:%x:%x:%x:%x:%x" % a
def inet_pton(p):
@@ -80,38 +81,38 @@ def inet_pton(p):
(0, 0, 0, 0, 0, 65535, 258, 772)
>>> try: inet_pton('::1.2.3.4.5')
... except ValueError,x: print x
... except ValueError as x: print(x)
::1.2.3.4.5
"""
if p == '::':
return '\0'*16
return b'\0'*16
s = p
m = RE_IP4.search(s)
try:
if m:
pos = m.start()
ip4 = [int(i) for i in s[pos:].split('.')]
if not pos:
return struct.pack('!QLBBBB',0,65535,*ip4)
s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4)
pos = m.start()
ip4 = [int(i) for i in s[pos:].split('.')]
if not pos:
return struct.pack('!QLBBBB',0,65535,*ip4)
s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4)
a = s.split('::')
if len(a) == 2:
l,r = a
if not l:
r = r.split(':')
return struct.pack('!HHHHHHHH',
l,r = a
if not l:
r = r.split(':')
return struct.pack('!HHHHHHHH',
*[0]*(8-len(r)) + [int(s,16) for s in r])
if not r:
l = l.split(':')
return struct.pack('!HHHHHHHH',
if not r:
l = l.split(':')
return struct.pack('!HHHHHHHH',
*[int(s,16) for s in l] + [0]*(8-len(l)))
l = l.split(':')
r = r.split(':')
return struct.pack('!HHHHHHHH',
l = l.split(':')
r = r.split(':')
return struct.pack('!HHHHHHHH',
*[int(s,16) for s in l] + [0]*(8-len(l)-len(r))
+ [int(s,16) for s in r])
if len(a) == 1:
return struct.pack('!HHHHHHHH',
return struct.pack('!HHHHHHHH',
*[int(s,16) for s in a[0].split(':')])
except ValueError: pass
raise ValueError,p
raise ValueError(p)
+553
View File
@@ -0,0 +1,553 @@
"""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()
+240
View File
@@ -0,0 +1,240 @@
## @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 log(self,*msg):
for i in msg: print(i,file=self.logfp,end=None)
print(file=self.logfp)
## Set a macro value.
# These are retrieved by the %milter with getsymval.
# @param name the macro name, as passed to getsymval
# @param val the macro value
def setsymval(self,name,val):
self._macros[name] = val
def getsymval(self,name):
stage = self._stage
if stage >= 0:
syms = self._symlist[stage]
if syms is not None and name not in syms:
return None
return self._macros.get(name,None)
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self._bodyreplaced = True
else:
raise IOError("replacebody not called from eom()")
def chgfrom(self,sender,params=None):
if not self._body:
raise IOError("chgfrom not called from eom()")
self.log('chgfrom: sender=%s' % (sender))
self._envfromchanged = True
self._sender = sender
# TODO: write implement quarantine()
def quarantine(self,reason):
raise NotImplemented
# TODO: measure time between milter calls
def progress(self):
pass
# FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a %milter
def chgheader(self,field,idx,value):
if not self._body:
raise IOError("chgheader not called from eom()")
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '':
del self._msg[field]
else:
self._msg[field] = value
self._headerschanged = True
def addheader(self,field,value,idx=-1):
if not self._body:
raise IOError("addheader not called from eom()")
self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value
self._headerschanged = True
def delrcpt(self,rcpt):
if not self._body:
raise IOError("delrcpt not called from eom()")
self._delrcpt.append(rcpt)
def addrcpt(self,rcpt):
if not self._body:
raise IOError("addrcpt not called from eom()")
self._addrcpt.append(rcpt)
## Save the reply codes and messages in self._reply.
def setreply(self,rcode,xcode,*msg):
self._reply = (rcode,xcode) + msg
def setsymlist(self,stage,macros):
if not self._actions & Milter.SETSYMLIST:
raise DisabledAction("SETSYMLIST")
if self._stage != -1:
raise RuntimeError("setsymlist may only be called from negotiate")
# not used yet, but just for grins we save the data
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(b' ')
except: pass
a += m
if len(a) > 5:
raise ValueError('setsymlist limited to 5 macros by MTA')
if self._symlist[stage] is not None:
raise ValueError('setsymlist already called for stage:'+stage)
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(h,val)
if rc != Milter.CONTINUE: return rc
# eoh
self._stage = Milter.M_EOH
rc = self.eoh()
self._stage = None
if rc != Milter.CONTINUE: return rc
# body
header,body = msg.as_bytes().split(b'\n\n',1)
bfp = BytesIO(body)
while 1:
buf = bfp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = BytesIO()
self._stage = Milter.M_EOM
rc = self.eom()
self._stage = None
if self._bodyreplaced:
body = self._body.getvalue()
self._body = BytesIO()
self._body.write(header)
self._body.write(b'\n\n')
self._body.write(body)
return rc
## Feed an email contained in a file to the %milter.
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
# @param sender MAIL FROM
# @param rcpts RCPT TO, multiple recipients may be supplied
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
with open('test/'+fname,'rb') as fp:
return self.feedFile(fp,sender,*rcpts)
## Call the connect and helo callbacks.
# The helo callback is not called if connect does not return CONTINUE.
# @param host the hostname passed to the connect callback
# @param helo the hostname passed to the helo callback
# @param ip the IP address passed to the connect callback
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
self._body = None
self._bodyreplaced = False
self._setctx(None)
opts = [ Milter.CURR_ACTS,~0,0,0 ]
self._stage = -1
rc = self.negotiate(opts)
self._stage = Milter.M_CONNECT
rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE:
self._stage = None
self.close()
return rc
self._stage = Milter.M_HELO
rc = self.hello(helo)
self._stage = None
if rc != Milter.CONTINUE:
self.close()
return rc
+297
View File
@@ -0,0 +1,297 @@
## @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
import time
import mime
try:
from io import BytesIO
except:
from StringIO import StringIO as BytesIO
import Milter
from Milter import utils
import mime
## 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):
return self._priv.header(fld,val)
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)
+50 -24
View File
@@ -5,12 +5,14 @@
import re
import struct
import socket
import email.Errors
import email.errors
from email.header import decode_header
import email.base64mime
import email.utils
from fnmatch import fnmatchcase
from email.Header import decode_header
#import email.Utils
import rfc822
from binascii import a2b_base64
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$'
@@ -53,8 +55,8 @@ if hasattr(socket,'has_ipv6') and socket.has_ipv6:
else:
from pyip6 import inet_ntop, inet_pton
MASK = 0xFFFFFFFFL
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
MASK = 0xFFFFFFFF
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
def cidr(i,n,mask=MASK):
return ~(mask >> n) & mask & i
@@ -67,6 +69,12 @@ 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'])
@@ -75,8 +83,10 @@ 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)
@@ -84,25 +94,33 @@ def iniplist(ipaddr,iplist):
p = pat.split('/',1)
if ip4re.match(p[0]):
if len(p) > 1:
n = int(p[1])
n = int(p[1])
else:
n = 32
if cidr(addr2bin(p[0]),n) == cidr(ipnum,n):
return True
elif ip6re.match(p[0]):
if len(p) > 1:
n = int(p[1])
n = int(p[1])
else:
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.
@@ -116,12 +134,10 @@ 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', 'addr...@example.com')
>>> parseaddr('a(WRONG)@b')
('WRONG', 'a@b')
('Real Name (comment)', 'addr...@example.com')
"""
#return email.Utils.parseaddr(t)
res = rfc822.parseaddr(t)
#return email.utils.parseaddr(t)
res = email.utils.parseaddr(t)
# dirty fix for some broken cases
if not res[0]:
pos = t.find('<')
@@ -130,7 +146,7 @@ def parseaddr(t):
pos1 = addrspec.rfind(':')
if pos1 > 0:
addrspec = addrspec[pos1+1:]
return rfc822.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
return email.utils.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
if not res[1]:
pos = t.find('<')
if pos > 0 and t[-1] == '>':
@@ -138,9 +154,19 @@ def parseaddr(t):
pos1 = addrspec.rfind(':')
if pos1 > 0:
addrspec = addrspec[pos1+1:]
return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
return email.utils.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.
@@ -185,18 +211,18 @@ def parse_header(val):
for s,enc in h:
if enc:
try:
u.append(unicode(s,enc,'replace'))
except LookupError:
u.append(unicode(s))
u.append(s.decode(enc,'replace'))
except LookupError:
u.append(s.decode())
else:
u.append(unicode(s))
u = ''.join(u)
for enc in ('us-ascii','iso-8859-1','utf8'):
u.append(s.decode())
u = u''.join(u)
for enc in ('us-ascii','iso-8859-1','utf-8'):
try:
return u.encode(enc)
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
+5 -30
View File
@@ -11,25 +11,24 @@ any point, tell Sendmail to reject, discard, or accept the message.
Requirements
------------
This python milter extension: http://www.bmsi.com/python/milter.html
Python milter extension: http://https://pypi.python.org/pypi/pymilter/
Python: http://www.python.org
Sendmail: http://www.sendmail.org
NB: From Sendmail's libmilter/README:
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):
not just -pthread). libmilter is currently known to work on (modulo problems
in the pthread support of some specific versions):
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:
@@ -110,33 +109,9 @@ _FFR_MILTER for the cf macros. For example,
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
RedHat 6.2 Notes
----------------
The Redhat 6.2 sendmail RPM does not enable milter. You can obtain a
modified spec file at
http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec
use it to rebuild the Redhat 7.2 SRPM. The RH6.2 SRPM does not have
recent sendmail security patches.
RedHat 7.2 Notes
----------------
The Redhat 7.2 sendmail RPM enables milter in sendmail - but does not include
the headers needed for compiling a milter. You can obtain a modified spec
file with a sendmail-devel package that includes the needed static libraries
and headers at
http://www.bmsi.com/linux/sendmail-rh72.spec
IPv6 Notes
----------
IPv6 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
@@ -194,7 +169,7 @@ Authors
-------
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
kludge and added threading and context objects to it, wrote a proper OO
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a
-4
View File
@@ -1,6 +1,2 @@
Support smfi_negotiate and auto negotiate only those callbacks for which
Milter.Milter methods have been overridden. (Python should be able to
do that.)
Lookup exact RFC syntax of real name / email and make
Milter.utils.parse_addr() pass all unit tests.
+59 -14
View File
@@ -1,46 +1,48 @@
## @mainpage Writing Milters in Python
#
# At the lowest level, the <code>milter</code> module provides a thin wrapper
# around the <a href="https://www.milter.org/developers/api/index"> sendmail
# around the <a href="milter_api/index.html"> 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 callback events.
# must provide methods corresponding to the libmilter event callbacks.
#
# Each event method returns a code to tell sendmail whether to proceed with
# Each callback method returns a code to tell sendmail whether to proceed with
# processing the message. This is a big advantage of milters over other mail
# filtering systems. Unwanted mail can be stopped in its tracks at the
# earliest possible point.
# earliest possible point. The callback return codes are
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
#
# The <code>Milter.Base</code> class provides default implementations for
# The Milter.Base class provides default implementations for
# event methods that do nothing, and also provides wrappers for the libmilter
# 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 <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 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>mime</code> module provides a wrapper for the Python email package
# The mime module provides a wrapper for the Python email package
# that fixes some bugs, and simplifies modifying selected parts of a MIME
# message.
#
# @section threading
#
# The libmilter library which pymilter wraps
# <a href="https://www.milter.org/developers/overview#SignalHandling">handles
# <a href="milter_overview#SignalHandling">handles
# all signals</a> itself, and expects to be called from a single main thread.
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
# <a href="https://www.milter.org/developers/api/smfi_stop">smfi_stop</a>
# <a href="milter_api/smfi_stop.html">smfi_stop</a>
# and the last to an internal ABORT.
#
# If you use python threads or threading modules, then signal handling gets
@@ -50,4 +52,47 @@
# 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 @ref milter-template.py.
# 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.
#
+121 -37
View File
@@ -3,10 +3,65 @@
## @package milter
#
# A thin wrapper around libmilter.
# A thin wrapper around libmilter. Most users will not import
# milter directly, but will instead import Milter and subclass
# Milter.Base. This module gives you ultimate low level control
# from python.
#
## Hold context for a milter connection.
## Continue processing the current connection, message, or recipient.
CONTINUE = 0
## For a connection-oriented routine, reject this connection;
# call Milter.Base.close(). For a message-oriented routine, except
# Milter.Base.eom() or Milter.Base.abort(), reject this message. For a
# recipient-oriented routine, reject the current recipient (but continue
# processing the current message).
REJECT = 1
## For a message- or recipient-oriented routine, accept this message, but
# silently discard it. SMFIS_DISCARD should not be returned by a
# connection-oriented routine.
DISCARD = 2
## For a connection-oriented routine, accept this connection without further
# filter processing; call Milter.Base.close(). For a message- or
# recipient-oriented routine, accept this message without further filtering.
ACCEPT = 3
## Return a temporary failure, i.e., the corresponding SMTP command will return
# an appropriate 4xx status code. For a message-oriented routine, except
# Milter.Base.envfrom(), fail for this message. For a connection-oriented
# routine, fail for this connection; call Milter.Base.close(). For a recipient-oriented
# routine, only
# fail for the current recipient; continue message processing.
TEMPFAIL = 4
## Skip further callbacks of the same type in this transaction.
# Currently this return value is only allowed in Milter.Base.body(). It can be
# used if a %milter has received sufficiently many body chunks to make a
# decision, but still wants to invoke message modification functions that are
# only allowed to be called from Milter.Base.eom(). Note: the %milter must
# negotiate this behavior with the MTA, i.e., it must check whether the
# protocol action SMFIP_SKIP is available and if so, the %milter must request
# it.
SKIP = 5
## Do not send a reply back to the MTA.
# The %milter must negotiate this behavior with the MTA, i.e., it must check
# whether the appropriate protocol action P_NR_* is available and if so,
# the %milter must request it. If you set the P_NR_* protocol action for a
# callback, that callback must always reply with NOREPLY. Using any other
# reply code is a violation of the API. If in some cases your callback may
# return another value (e.g., due to some resource shortages), then you must
# not set P_NR_* and you must use CONTINUE as the default return
# code. (Alternatively you can try to delay reporting the problem to a later
# callback for which P_NR_* is not set.)
#
# This is negotiated and returned automatically by the Milter.noreply
# function decorator.
NOREPLY = 6
## Hold context for a %milter connection.
# Each connection to sendmail creates a new <code>SMFICTX</code> struct within
# libmilter. The milter module in turn creates a milterContext
# tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code>
@@ -20,46 +75,52 @@
# and converts function callbacks to instance method invocations.
#
class milterContext(object):
## Calls <a href="https://www.milter.org/developers/api/smfi_getsymval">smfi_getsymval</a>.
## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>.
def getsymval(self,sym): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_setreply">
## Calls <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> or
# <a href="https://www.milter.org/developers/api/smfi_setmlreply">
# <a href="milter_api/smfi_setmlreply.html">
# smfi_setmlreply</a>.
# @param rcode SMTP response code
# @param xcode extended SMTP response code
# @param msg one or more message lines. If the MTA does not support
# multiline messages, only the first is used.
def setreply(self,rcode,xcode,*msg): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_addheader">smfi_addheader</a>.
## Calls <a href="milter_api/smfi_addheader.html">smfi_addheader</a>.
def addheader(self,name,value,idx=-1): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">smfi_chgheader</a>.
## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>.
def chgheader(self,name,idx,value): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">smfi_addrcpt</a>.
## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>.
def addrcpt(self,rcpt,params=None): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">smfi_delrcpt</a>.
## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>.
def delrcpt(self,rcpt): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">smfi_replacebody</a>.
## Calls <a href="milter_api/smfi_replacebody.html">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="https://www.milter.org/developers/api/smfi_quarantine">smfi_quarantine</a>.
## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>.
def quarantine(self,reason): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_progress">smfi_progress</a>.
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>.
def progress(self): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">smfi_chgfrom</a>.
## Calls <a href="milter_api/smfi_chgfrom.html">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.
def setsmlist(self,stage,macrolist): pass
# It may only be called from the negotiate callback.
# The protocol stages are
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>.
# @param stage protocol stage in which the macro list should be used
# @param macrolist a space separated list of macro names
def setsymlist(self,stage,macrolist): pass
class error(Exception): pass
## 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.
@@ -77,54 +138,72 @@ def set_abort_callback(cb): pass
def set_close_callback(cb): pass
## Sets the return code for untrapped Python exceptions during a callback.
# Must be one of TEMPFAIL,REJECT,CONTINUE
# The default is TEMPFAIL. You should not depend on this handler. Your
# application should have its own top level exception handler for each
# callback. You can then choose your own reply message, log the stack track
# were you please, and so on. However, if you miss one, this last ditch
# handler will print a standard stack trace to sys.stderr, and return to
# sendmail.
# @param code one of #TEMPFAIL,#REJECT,#CONTINUE, or since 1.0, #ACCEPT
def set_exception_policy(code): pass
## 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).
# @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). 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
# @param negotiate the
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
# <a href="milter_api/xxfi_negotiate.html">
# xxfi_negotiate</a> callback, called to negotiate supported
# actions, callbacks, and protocol steps.
# @param unknown the
# <a href="https://www.milter.org/developers/api/xxfi_unknown">
# <a href="milter_api/xxfi_unknown.html">
# xxfi_unknown</a> callback, called when for SMTP commands
# not recognized by the MTA. (Extend SMTP in your milter!)
# @param data the
# <a href="https://www.milter.org/developers/api/xxfi_data">
# <a href="milter_api/xxfi_data.html">
# xxfi_data</a> callback, called when the DATA
# SMTP command is received.
def register(name,negotiate=None,unknown=None,data=None): pass
## 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="https://www.milter.org/developers/api/smfi_main">
# Calls <a href="milter_api/smfi_main.html">
# smfi_main</a>.
def main(): pass
## Set the libmilter debugging 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
# <a href="milter_api/smfi_setdbg.html">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="https://www.milter.org/developers/api/smfi_settimeout">
# Calls <a href="milter_api/smfi_settimeout.html">
# smfi_settimeout</a>. Must be called before calling main().
def settimeout(secs): pass
## Set socket backlog.
# Calls <a href="https://www.milter.org/developers/api/smfi_setbacklog">
# Calls <a href="milter_api/smfi_setbacklog.html">
# smfi_setbacklog</a>. Must be called before calling main().
def setbacklog(n): pass
@@ -133,16 +212,21 @@ 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.
# group or world writable directory. milter.setconn() will not fail with
# an invalid socket - this will be detected only when calling milter.main()
# or milter.opensocket().
# @param s the socket address in proto:address format
# <pre>
# setconn('unix:/var/run/pythonfilter')
# setconn('inet:8800') # listen on ANY interface
# setconn('inet:7871@@publichost') # listen on a specific interface
# setconn('inet6:8020')
# 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
# </pre>
def setconn(s): pass
## Stop the milter gracefully.
## Stop the %milter gracefully.
def stop(): pass
## Retrieve diagnostic info.
+6 -7
View File
@@ -1,15 +1,14 @@
web:
doxygen
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-milter-devel doc/html/milter_api
rsync -ravKk doc/html/ bmsi.com:/var/www/html/pymilter
cd doc/html; zip -r ../../doc .
VERSION=0.9.6
CVSTAG=pymilter-0_9_6
VERSION=1.0.4
PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz
$(SRCTAR):
cvs export -r$(CVSTAG) -d $(PKG) pymilter
tar cvfz $(PKG).tar.gz $(PKG)
rm -r $(PKG)
git archive --format=tar.gz --prefix=$(PKG)/ -o $(SRCTAR) $(PKG)
cvstar: $(SRCTAR)
gittar: $(SRCTAR)
+5 -4
View File
@@ -1,7 +1,8 @@
## A very simple milter to prevent mixing of internal and external mail.
## A very simple sample 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
@@ -13,7 +14,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
@@ -68,12 +69,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()
+17 -9
View File
@@ -1,14 +1,18 @@
## To roll your own milter, create a class that extends Milter.
# 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
# based on Sendmail's milter API
# 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
import StringIO
try:
from StringIO import StringIO
except:
from io import StringIO
import time
import email
import sys
@@ -71,7 +75,10 @@ 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)
self.fp = StringIO.StringIO()
# NOTE: self.fp is only an *internal* copy of message data. You
# must use addheader, chgheader, replacebody to change the message
# on the MTA.
self.fp = StringIO()
self.canon_from = '@'.join(parse_addr(mailfrom))
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
return Milter.CONTINUE
@@ -104,7 +111,6 @@ class myMilter(Milter.Base):
def eom(self):
self.fp.seek(0)
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')
@@ -129,10 +135,12 @@ def background():
t = logq.get()
if not t: break
msg,id,ts = t
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
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,
print
for i in msg: print(i,end=None)
print()
sys.stdout.flush()
## ===
@@ -147,12 +155,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__":
main()
+161 -265
View File
@@ -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@bmsi.com)
* Stuart Gathman (stuart@gathman.org)
*
* 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,201 +34,6 @@ $ 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
@@ -266,7 +71,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_kernel__) || defined(__linux__)
#if defined(__FreeBSD__) || defined(__linux__) || defined(__sun__) || defined(__GLIBC__) || (defined(__APPLE__) && defined(__MACH__))
#define HAVE_IPV6_RFC2553
#include <arpa/inet.h>
#endif
@@ -327,7 +132,13 @@ static struct MilterCallback {
{ NULL , NULL }
};
staticforward struct smfiDesc description; /* forward declaration */
#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
static PyObject *MilterError;
/* The interpreter instance that called milter.main */
@@ -339,8 +150,6 @@ typedef struct {
static milter_Diag diag;
staticforward PyTypeObject milter_ContextType;
typedef struct {
PyObject_HEAD
SMFICTX *ctx; /* libmilter thread state */
@@ -436,7 +245,7 @@ _thread_return(PyThreadState *t,int val,char *errstr) {
return _generic_return(val,errstr);
}
static char milter_set_flags__doc__[] =
static const char milter_set_flags__doc__[] =
"set_flags(int) -> None\n\
Set flags for filter capabilities; OR of one or more of:\n\
ADDHDRS - filter may add headers\n\
@@ -477,7 +286,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
return Py_None;
}
static char milter_set_connect_callback__doc__[] =
static const 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\
@@ -504,7 +313,7 @@ milter_set_connect_callback(PyObject *self, PyObject *args) {
"O:set_connect_callback", &connect_callback);
}
static char milter_set_helo_callback__doc__[] =
static const char milter_set_helo_callback__doc__[] =
"set_helo_callback(Function) -> None\n\
Sets the Python function invoked upon SMTP HELO.\n\
Function takes args (ctx, hostname) -> int\n\
@@ -515,7 +324,7 @@ milter_set_helo_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_helo_callback", &helo_callback);
}
static char milter_set_envfrom_callback__doc__[] =
static const char milter_set_envfrom_callback__doc__[] =
"set_envfrom_callback(Function) -> None\n\
Sets the Python function invoked on envelope from.\n\
Function takes args (ctx, from, *str) -> int\n\
@@ -528,7 +337,7 @@ milter_set_envfrom_callback(PyObject *self, PyObject *args) {
&envfrom_callback);
}
static char milter_set_envrcpt_callback__doc__[] =
static const 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\
@@ -541,7 +350,7 @@ milter_set_envrcpt_callback(PyObject *self, PyObject *args) {
&envrcpt_callback);
}
static char milter_set_header_callback__doc__[] =
static const 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\
@@ -554,7 +363,7 @@ milter_set_header_callback(PyObject *self, PyObject *args) {
&header_callback);
}
static char milter_set_eoh_callback__doc__[] =
static const 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";
@@ -564,20 +373,20 @@ milter_set_eoh_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback);
}
static char milter_set_body_callback__doc__[] =
static const char milter_set_body_callback__doc__[] =
"set_body_callback(Function) -> None\n\
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 -> String - body data";
chunk -> bytes - body data";
static PyObject *
milter_set_body_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_body_callback", &body_callback);
}
static char milter_set_eom_callback__doc__[] =
static const char milter_set_eom_callback__doc__[] =
"set_eom_callback(Function) -> None\n\
Sets the Python function invoked at end of message.\n\
This routine is the only place where special operations\n\
@@ -590,7 +399,7 @@ milter_set_eom_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_eom_callback", &eom_callback);
}
static char milter_set_abort_callback__doc__[] =
static const char milter_set_abort_callback__doc__[] =
"set_abort_callback(Function) -> None\n\
Sets the Python function invoked if message is aborted\n\
outside of the control of the filter, for example,\n\
@@ -604,7 +413,7 @@ milter_set_abort_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_abort_callback", &abort_callback);
}
static char milter_set_close_callback__doc__[] =
static const char milter_set_close_callback__doc__[] =
"set_close_callback(Function) -> None\n\
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\
@@ -617,7 +426,7 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
static int exception_policy = SMFIS_TEMPFAIL;
static char milter_set_exception_policy__doc__[] =
static const char milter_set_exception_policy__doc__[] =
"set_exception_policy(i) -> None\n\
Sets the policy for untrapped Python exceptions during a callback.\n\
Must be one of TEMPFAIL,REJECT,CONTINUE";
@@ -628,7 +437,8 @@ 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_REJECT: case SMFIS_TEMPFAIL:
case SMFIS_CONTINUE: case SMFIS_ACCEPT:
exception_policy = i;
Py_INCREF(Py_None);
return Py_None;
@@ -643,23 +453,30 @@ _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", "Filter failure");
smfi_setreply(self->ctx, "554", "5.3.0", untrapped_msg);
return SMFIS_REJECT;
case SMFIS_TEMPFAIL:
smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure");
smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg);
return SMFIS_TEMPFAIL;
}
return SMFIS_CONTINUE;
return exception_policy;
}
/* This should never happen, _report_exception is only called when
* the caller has already detected a python exception. If it
* does somehow happen, pretend nothing is wrong... */
_release_thread(self->t);
return SMFIS_CONTINUE;
}
@@ -676,7 +493,11 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
result = PyEval_CallObject(cb, arglist);
Py_DECREF(arglist);
if (result == NULL) return _report_exception(self);
#if 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];
@@ -691,7 +512,11 @@ _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;
@@ -708,7 +533,11 @@ 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
@@ -716,8 +545,13 @@ static PyObject *
makeip6addr(struct sockaddr_in6 *addr) {
char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */
const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf);
if (s) return PyString_FromString(s);
return PyString_FromString("inet6:unknown");
#if 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
@@ -808,7 +642,11 @@ 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 = PyUnicode_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);
@@ -865,7 +703,11 @@ 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, bodylen);
#else
arglist = Py_BuildValue("(Os#)", c, bodyp, bodylen);
#endif
return _generic_wrapper(c, body_callback, arglist);
}
@@ -926,20 +768,17 @@ 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)
? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
#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()) {
@@ -947,7 +786,6 @@ milter_wrap_negotiate(SMFICTX *ctx,
PyErr_Clear();
rc = SMFIS_REJECT;
}
#endif
}
else if (rc != SMFIS_ALL_OPTS)
rc = SMFIS_REJECT;
@@ -987,7 +825,7 @@ milter_wrap_close(SMFICTX *ctx) {
return r;
}
static char milter_register__doc__[] =
static const 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.";
@@ -1032,7 +870,7 @@ milter_register(PyObject *self, PyObject *args, PyObject *kwds) {
return _generic_return(smfi_register(description), "cannot register");
}
static char milter_opensocket__doc__[] =
static const 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.";
@@ -1045,7 +883,7 @@ milter_opensocket(PyObject *self, PyObject *args) {
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
}
static char milter_main__doc__[] =
static const 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().";
@@ -1069,7 +907,7 @@ milter_main(PyObject *self, PyObject *args) {
return o;
}
static char milter_setdbg__doc__[] =
static const char milter_setdbg__doc__[] =
"setdbg(int) -> None\n\
Sets debug level in sendmail/libmilter source. Dubious usefulness.";
@@ -1080,7 +918,7 @@ milter_setdbg(PyObject *self, PyObject *args) {
return _generic_return(smfi_setdbg(val), "cannot set debug value");
}
static char milter_setbacklog__doc__[] =
static const char milter_setbacklog__doc__[] =
"setbacklog(int) -> None\n\
Set the TCP connection queue size for the milter socket.";
@@ -1092,7 +930,7 @@ milter_setbacklog(PyObject *self, PyObject *args) {
return _generic_return(smfi_setbacklog(val), "cannot set backlog");
}
static char milter_settimeout__doc__[] =
static const char milter_settimeout__doc__[] =
"settimeout(int) -> None\n\
Set the time (in seconds) that sendmail will wait before\n\
considering this filter dead.";
@@ -1105,7 +943,7 @@ milter_settimeout(PyObject *self, PyObject *args) {
return _generic_return(smfi_settimeout(val), "cannot set timeout");
}
static char milter_setconn__doc__[] =
static const 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\
@@ -1125,7 +963,7 @@ milter_setconn(PyObject *self, PyObject *args) {
return _generic_return(smfi_setconn(str), "cannot set connection");
}
static char milter_stop__doc__[] =
static const 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.";
@@ -1138,7 +976,7 @@ milter_stop(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_stop(), "cannot stop");
}
static char milter_getdiag__doc__[] =
static const 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.";
@@ -1148,7 +986,7 @@ milter_getdiag(PyObject *self, PyObject *args) {
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
}
static char milter_getversion__doc__[] =
static const char milter_getversion__doc__[] =
"getversion() -> tuple\n\
Return runtime libmilter version as a tuple of major,minor,patchlevel.";
static PyObject *
@@ -1162,7 +1000,7 @@ milter_getversion(PyObject *self, PyObject *args) {
return Py_BuildValue("(kkk)", major,minor,patch);
}
static char milter_getsymval__doc__[] =
static const char milter_getsymval__doc__[] =
"getsymval(String) -> String\n\
Returns a symbol's value. Context-dependent, and unclear from the dox.";
@@ -1177,7 +1015,7 @@ milter_getsymval(PyObject *self, PyObject *args) {
return Py_BuildValue("s", smfi_getsymval(ctx, str));
}
static char milter_setreply__doc__[] =
static const char milter_setreply__doc__[] =
"setreply(rcode, xcode, message) -> None\n\
Sets the specific reply code to be used in response\n\
to the active command.\n\
@@ -1241,7 +1079,7 @@ milter_setreply(PyObject *self, PyObject *args) {
"cannot set reply");
}
static char milter_addheader__doc__[] =
static const 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\
@@ -1278,7 +1116,7 @@ milter_addheader(PyObject *self, PyObject *args) {
}
#ifdef SMFIF_CHGFROM
static char milter_chgfrom__doc__[] =
static const 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\
@@ -1301,13 +1139,13 @@ milter_chgfrom(PyObject *self, PyObject *args) {
}
#endif
static char milter_chgheader__doc__[] =
static const 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 occurence of this header\n\
int - the Nth occurrence 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.";
@@ -1329,7 +1167,7 @@ milter_chgheader(PyObject *self, PyObject *args) {
"cannot change header");
}
static char milter_addrcpt__doc__[] =
static const 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\
@@ -1359,7 +1197,7 @@ milter_addrcpt(PyObject *self, PyObject *args) {
return _thread_return(t,rc, "cannot add recipient");
}
static char milter_delrcpt__doc__[] =
static const 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.";
@@ -1377,7 +1215,7 @@ milter_delrcpt(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
}
static char milter_replacebody__doc__[] =
static const char milter_replacebody__doc__[] =
"replacebody(string) -> None\n\
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\
@@ -1399,7 +1237,7 @@ milter_replacebody(PyObject *self, PyObject *args) {
(unsigned char *)bodyp, bodylen), "cannot replace message body");
}
static char milter_setpriv__doc__[] =
static const 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\
@@ -1425,7 +1263,7 @@ milter_setpriv(PyObject *self, PyObject *args) {
return old;
}
static char milter_getpriv__doc__[] =
static const 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\
@@ -1443,7 +1281,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
}
#ifdef SMFIF_QUARANTINE
static char milter_quarantine__doc__[] =
static const 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.";
@@ -1464,7 +1302,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
#endif
#ifdef SMFIR_PROGRESS
static char milter_progress__doc__[] =
static const char milter_progress__doc__[] =
"progress() -> None\n\
Notify the MTA that we are working on a message so it will reset timeouts.";
@@ -1481,23 +1319,23 @@ milter_progress(PyObject *self, PyObject *args) {
}
#endif
#ifdef SMFIF_SETSMLIST
static char milter_setsmlist__doc__[] =
"setsmlist(stage,macrolist) -> None\n\
#ifdef SMFIF_SETSYMLIST
static const char milter_setsymlist__doc__[] =
"setsymlist(stage,macrolist) -> None\n\
Tell the MTA which macro values we are interested in for a given stage";
static PyObject *
milter_setsmlist(PyObject *self, PyObject *args) {
milter_setsymlist(PyObject *self, PyObject *args) {
SMFICTX *ctx;
PyThreadState *t;
int stage = 0;
char *smlist = 0;
if (!PyArg_ParseTuple(args, "is:setsmlist",&stage, &smlist)) return NULL;
if (!PyArg_ParseTuple(args, "is:setsymlist",&stage, &smlist)) return NULL;
ctx = _find_context(self);
if (ctx == NULL) return NULL;
t = PyEval_SaveThread();
return _thread_return(t,smfi_setsmlist(ctx,stage,smlist),
return _thread_return(t,smfi_setsymlist(ctx,stage,smlist),
"cannot set macro list");
}
#endif
@@ -1521,16 +1359,18 @@ static PyMethodDef context_methods[] = {
#ifdef SMFIF_CHGFROM
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
#endif
#ifdef SMFIF_SETSMLIST
{ "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__doc__},
#ifdef SMFIF_SETSYMLIST
{ "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__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",
@@ -1580,14 +1420,23 @@ 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 */
@@ -1601,26 +1450,63 @@ 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 char milter_documentation[] =
static const 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
void
initmilter(void) {
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"milter", /* m_name */
milter_documentation,/* m_doc */
-1, /* m_size */
milter_methods, /* m_methods */
NULL, /* m_reload */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL, /* m_free */
};
PyMODINIT_FUNC PyInit_milter(void) {
PyObject *m, *d;
if (PyType_Ready(&milter_ContextType) < 0)
return NULL;
m = PyModule_Create(&moduledef);
if (m == NULL) return NULL;
#else
void initmilter(void) {
PyObject *m, *d;
m = Py_InitModule4("milter", milter_methods, milter_documentation,
(PyObject*)NULL, PYTHON_API_VERSION);
(PyObject*)NULL, PYTHON_API_VERSION);
#endif
d = PyModule_GetDict(m);
MilterError = PyErr_NewException("milter.error", NULL, NULL);
PyDict_SetItemString(d,"error", MilterError);
@@ -1645,8 +1531,15 @@ initmilter(void) {
#ifdef SMFIF_CHGFROM
setitem(d,"CHGFROM",SMFIF_CHGFROM);
#endif
#ifdef SMFIF_SETSMLIST
setitem(d,"SETSMLIST",SMFIF_SETSMLIST);
#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 */
#endif
#ifdef SMFIS_ALL_OPTS
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
@@ -1679,4 +1572,7 @@ 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
}
+97 -85
View File
@@ -1,4 +1,7 @@
# $Log$
# Revision 1.8 2011/11/05 15:51:03 customdesigned
# New example
#
# Revision 1.7 2009/06/13 21:15:12 customdesigned
# Doxygen updates.
#
@@ -81,7 +84,7 @@
## @package mime
# This module provides a "defang" function to replace naughty attachments.
#
#
# We also provide workarounds for bugs in the email module that comes
# with python. The "bugs" fixed mostly come up only with malformed
# messages - but that is what you have when dealing with spam.
@@ -90,26 +93,34 @@
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
import StringIO
from __future__ import print_function
try:
from io import BytesIO, StringIO
except:
from StringIO import StringIO
BytesIO = StringIO
import socket
import Milter
import zipfile
import sys
import email
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
from email.message import Message
try:
from email.generator import BytesGenerator
from email import message_from_binary_file
except:
from email.generator import Generator as BytesGenerator
from email import message_from_file as message_from_binary_file
from email.utils import quote
from types import ListType,StringType
if not getattr(Message,'as_bytes',None):
Message.as_bytes = Message.as_string
## Return a list of filenames in a zip file.
# Embedded zip files are recursively expanded.
def zipnames(txt):
fp = StringIO.StringIO(txt)
fp = BytesIO(txt)
zipf = zipfile.ZipFile(fp,'r')
names = []
for nm in zipf.namelist():
@@ -120,46 +131,44 @@ def zipnames(txt):
## Fix multipart handling in email.Generator.
#
class MimeGenerator(Generator):
class MimeGenerator(BytesGenerator):
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
# full MIME type, then dispatch to self._handle_<maintype>(). If
# that's missing too, then dispatch to self._writeBody().
main = msg.get_content_maintype()
if msg.is_multipart() and main.lower() != 'multipart':
self._handle_multipart(msg)
else:
Generator._dispatch(self,msg)
if msg.is_multipart() and main.lower() != 'multipart':
self._handle_multipart(msg)
else:
BytesGenerator._dispatch(self,msg)
def unquote(s):
"""Remove quotes from a string."""
if len(s) > 1:
if s.startswith('"'):
if s.endswith('"'):
if s.endswith('"'):
s = s[1:-1]
else: # remove garbage after trailing quote
try: s = s[1:s[1:].index('"')+1]
except:
return s
return s.replace('\\\\', '\\').replace('\\"', '"')
else: # remove garbage after trailing quote
try: s = s[1:s[1:].index('"')+1]
except:
return s
return s.replace('\\\\', '\\').replace('\\"', '"')
if s.startswith('<') and s.endswith('>'):
return s[1:-1]
return s[1:-1]
return s
from types import TupleType
def _unquotevalue(value):
if isinstance(value, TupleType):
if isinstance(value, tuple):
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
## Enhance email.message.Message
#
# Tracks modifications to headers of body or any part independently.
@@ -200,23 +209,23 @@ class MimeMessage(Message):
interpret as a name - and hence decide to execute this message."""
names = []
for attr,val in self._get_params_preserve([],'content-type'):
if isinstance(val, TupleType):
if isinstance(val, tuple):
# It's an RFC 2231 encoded parameter
newvalue = _unquotevalue(val)
if val[0]:
val = unicode(newvalue[2], newvalue[0])
else:
val = unicode(newvalue[2])
newvalue = _unquotevalue(val)
if val[0]:
val = unicode(newvalue[2], newvalue[0])
else:
val = unicode(newvalue[2])
else:
val = _unquotevalue(val.strip())
val = _unquotevalue(val.strip())
names.append((attr,val))
names += [("filename",self.get_filename())]
if scan_zip:
for key,name in tuple(names): # copy by converting to tuple
if name and name.lower().endswith('.zip'):
txt = self.get_payload(decode=True)
if txt.strip():
names += zipnames(txt)
if name and name.lower().endswith('.zip'):
txt = self.get_payload(decode=True)
if txt.strip():
names += zipnames(txt)
return names
def ismodified(self):
@@ -235,9 +244,9 @@ class MimeMessage(Message):
g = MimeGenerator(file)
g.flatten(self,unixfrom=unixfrom)
def as_string(self, unixfrom=False):
def as_bytes(self, unixfrom=False):
"Return the entire formatted message as a string."
fp = StringIO.StringIO()
fp = BytesIO()
self.dump(fp,unixfrom=unixfrom)
return fp.getvalue()
@@ -287,18 +296,18 @@ class MimeMessage(Message):
if t == 'message/rfc822' or t.startswith('multipart/'):
if not self.submsg:
txt = self.get_payload()
if type(txt) == str:
txt = self.get_payload(decode=True)
self.submsg = email.message_from_string(txt,MimeMessage)
for part in self.submsg.walk():
part.modified = False
else:
self.submsg = txt[0]
if type(txt) == str:
txt = self.get_payload(decode=True)
self.submsg = email.message_from_string(txt,MimeMessage)
for part in self.submsg.walk():
part.modified = False
else:
self.submsg = txt[0]
return self.submsg
return None
def message_from_file(fp):
msg = email.message_from_file(fp,MimeMessage)
msg = message_from_binary_file(fp,MimeMessage)
for part in msg.walk():
part.modified = False
assert not msg.ismodified()
@@ -309,7 +318,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 = map(lambda x:'.' + x,extlist.split(','))
bad_extensions = ['.' + x for x in extlist.split(',')]
def check_ext(name):
"Check a name for dangerous Winblows extensions."
@@ -333,7 +342,7 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
if badname:
if key == 'zipname':
badname = msg.get_filename()
break
break
else:
return Milter.CONTINUE
except zipfile.BadZipfile:
@@ -348,8 +357,6 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
msg["Content-Type"] = "text/plain; name="+name
return Milter.CONTINUE
import email.Iterators
def check_attachments(msg,check):
"""Scan attachments.
msg MimeMessage
@@ -380,7 +387,7 @@ class _defang:
return rc
def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True,
scan_zip=False):
scan_zip=False):
"""Compatible entry point.
Replace all attachments with dangerous names."""
self._savname = savname
@@ -395,18 +402,21 @@ class _defang:
# emulate old defang function
defang = _defang()
import sgmllib
if sys.version < '3.0.0':
from sgmllib import SGMLParser as HTMLParser
else:
from Milter.sgmllib import SGMLParser as HTMLParser
import re
declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*')
declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*')
class SGMLFilter(sgmllib.SGMLParser):
class SGMLFilter(HTMLParser):
"""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):
sgmllib.SGMLParser.__init__(self)
HTMLParser.__init__(self)
self.out = out
def handle_comment(self,comment):
@@ -437,7 +447,7 @@ class SGMLFilter(sgmllib.SGMLParser):
self.out.write("<!%s>" % data)
def write(self,buf):
"Act like a writer. Why doesn't SGMLParser do this by default?"
"Act like a writer. Why doesn't HTMLParser do this by default?"
self.feed(buf)
# Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft
@@ -450,25 +460,25 @@ class SGMLFilter(sgmllib.SGMLParser):
n = len(rawdata)
j = i + 2
while j < n:
c = rawdata[j]
if c == ">":
# end of declaration syntax
self.handle_special(rawdata[i+2:j])
return j + 1
if c in "\"'":
m = declstringlit.match(rawdata, j)
if not m:
c = rawdata[j]
if c == ">":
# end of declaration syntax
self.handle_special(rawdata[i+2:j])
return j + 1
if c in "\"'":
m = declstringlit.match(rawdata, j)
if not m:
# incomplete or an error?
return -1
j = m.end()
elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
m = declname.match(rawdata, j)
if not m:
# incomplete or an error?
return -1
j = m.end()
else:
j += 1
return -1
j = m.end()
elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
m = declname.match(rawdata, j)
if not m:
# incomplete or an error?
return -1
j = m.end()
else:
j += 1
# end of buffer between tokens
return -1
@@ -480,11 +490,14 @@ 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
self.out.write(self.msg)
def end_script(self):
#print('end script')
self.ignoring -= 1
if not self.ignoring:
self.out.write(self.msg)
def handle_data(self,data):
if not self.ignoring: SGMLFilter.handle_data(self,data)
def handle_comment(self,comment):
@@ -497,16 +510,16 @@ def check_html(msg,savname=None):
if msgtype == 'application/octet-stream':
for (attr,name) in msg.getnames():
if name and name.lower().endswith(".htm"):
msgtype = 'text/html'
msgtype = 'text/html'
if msgtype == 'text/html':
out = StringIO.StringIO()
out = StringIO()
htmlfilter = HTMLScriptFilter(out)
try:
htmlfilter.write(msg.get_payload(decode=True))
htmlfilter.write(msg.get_payload(decode=True).decode())
htmlfilter.close()
#except sgmllib.SGMLParseError:
except:
#mimetools.copyliteral(msg.get_payload(),open('debug.out','w')
mimetools.copyliteral(msg.get_payload(),open('debug.out','wb'))
htmlfilter.close()
hostname = socket.gethostname()
msg.set_payload(
@@ -525,18 +538,17 @@ def check_html(msg,savname=None):
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:]:
fp = open(fname)
fp = open(fname,'rb')
msg = message_from_file(fp)
email.Iterators._structure(msg)
email.iterators._structure(msg)
check_attachments(msg,_list_attach)
+6
View File
@@ -0,0 +1,6 @@
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
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/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" $@
+211 -61
View File
@@ -1,87 +1,237 @@
%define __python python2.6
%define pythonbase python26
# 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 python34
%else
%global python3 python3
%endif
%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
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
License: GPLv2+
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html
BuildRequires: python2-devel, %{python3}-devel, sendmail-devel >= 8.13
# 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
Requires: %{pythonbase}-pydns
# Needed for callbacks, not a core function but highly useful for milters
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13
BuildRequires: gcc
%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
%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\
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-%{version}
%setup -q -n pymilter-pymilter-%{version}
%patch -p1 -b .check
%build
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
%py2_build
#patch -p1 -b -z .py3 <milter.patch # not needed since 1.0.3
%py3_build
checkmodule -m -M -o pymilter.mod pymilter.te
semodule_package -o pymilter.pp -m pymilter.mod
%install
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install --root=$RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
mkdir -p $RPM_BUILD_ROOT%{libdir}
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
%py2_install
%py3_install
# start.sh is used by spfmilter, srsmilter, and milter, and could be used by
# other milters using pymilter.
%files
%defattr(-,root,root,-)
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
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%{python_sitearch}/*
%{libdir}
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%{python2_sitearch}/*
%clean
rm -rf $RPM_BUILD_ROOT
%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
%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
%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
@@ -90,7 +240,7 @@ rm -rf $RPM_BUILD_ROOT
- Fix milter-template.py
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
* Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
* Tue Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
- Handle IP6 in Milter.utils.iniplist()
- python-2.6
+13
View File
@@ -0,0 +1,13 @@
module pymilter 1.0;
require {
type sendmail_t;
type var_run_t;
type initrc_t;
class sock_file { write getattr };
class unix_stream_socket connectto;
}
#============= sendmail_t ==============
allow sendmail_t initrc_t:unix_stream_socket connectto;
allow sendmail_t var_run_t:sock_file { write getattr };
+38 -32
View File
@@ -1,4 +1,4 @@
from __future__ import print_function
# A simple milter.
# Author: Stuart D. Gathman <stuart@bmsi.com>
@@ -7,8 +7,10 @@
import sys
import os
import StringIO
import rfc822
try:
from io import BytesIO
except:
from StringIO import StringIO as BytesIO
import mime
import Milter
import tempfile
@@ -21,9 +23,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),
for i in msg: print i,
print
print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),end=None)
for i in msg: print(i,end=None)
print()
def __init__(self):
self.tempname = None
@@ -31,18 +33,25 @@ 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.log("mail from",f,str)
self.fp = StringIO.StringIO()
self.fp = BytesIO()
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):
@@ -60,23 +69,23 @@ class sampleMilter(Milter.Milter):
# even if we wanted the Taiwanese spam, we can't read Chinese
# (delete if you read chinese mail)
if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'):
self.log('REJECT: %s: %s' % (name,val))
self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer')
return Milter.REJECT
return Milter.REJECT
# check for common spam keywords
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))
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
return Milter.REJECT
# check for spam that pretends to be legal
lval = val.lower()
if lval.startswith("adv:") or lval.startswith("adv.") \
or lval.find('viagra') >= 0:
self.log('REJECT: %s: %s' % (name,val))
return Milter.REJECT
or lval.find('viagra') >= 0:
self.log('REJECT: %s: %s' % (name,val))
return Milter.REJECT
# check for invalid message id
if lname == 'message-id' and len(val) < 4:
@@ -86,7 +95,7 @@ class sampleMilter(Milter.Milter):
# check for common bulk mailers
if lname == 'x-mailer' and \
val.lower() in ('direct email','calypso','mail bomber'):
val.lower() in ('direct email','calypso','mail bomber'):
self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer')
return Milter.REJECT
@@ -95,12 +104,12 @@ class sampleMilter(Milter.Milter):
if lname in ('subject','x-mailer'):
self.log('%s: %s' % (name,val))
if self.fp:
self.fp.write("%s: %s\n" % (name,val)) # add header to buffer
self.fp.write(("%s: %s\n" % (name,val)).encode()) # add header to buffer
return Milter.CONTINUE
def eoh(self):
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
self.fp.write("\n")
self.fp.write(b'\n')
self.fp.seek(0)
# copy headers to a temp file for scanning the body
headers = self.fp.getvalue()
@@ -123,7 +132,7 @@ class sampleMilter(Milter.Milter):
h = msg.getheaders(name)
cnt = len(h)
for i in range(cnt,0,-1):
self.chgheader(name,i-1,'')
self.chgheader(name,i-1,'')
def eom(self):
if not self.fp: return Milter.ACCEPT
@@ -138,19 +147,16 @@ 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
out = tempfile.TemporaryFile()
try:
with tempfile.TemporaryFile() as out:
msg.dump(out)
out.seek(0)
msg = rfc822.Message(out)
msg.rewindbody()
msg = mime.message_from_file(out)
fp = BytesIO(msg.as_bytes().split(b'\n\n',1)[1])
while 1:
buf = out.read(8192)
if len(buf) == 0: break
self.replacebody(buf) # feed modified message to sendmail
buf = fp.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):
@@ -171,13 +177,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")
+3 -3
View File
@@ -1,5 +1,5 @@
[bdist_rpm]
python=python2.6
doc_files=README NEWS TODO
packager=Stuart D. Gathman <stuart@bmsi.com>
python=python3
doc_files=README NEWS TODO COPYING CREDITS
packager=Stuart D. Gathman <stuart@gathman.org>
release=1
+8 -5
View File
@@ -11,9 +11,10 @@ if sys.version < '2.6.5':
#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 = '0.9.7',
setup(name = "pymilter", version = '1.0.4',
description="Python interface to sendmail milter API",
long_description="""\
This is a python extension module to enable python scripts to
@@ -24,17 +25,19 @@ sending DSNs or doing CBVs.
author="Jim Niemira",
author_email="urmane@urmane.org",
maintainer="Stuart D. Gathman",
maintainer_email="stuart@bmsi.com",
maintainer_email="stuart@gathman.org",
license="GPL",
url="http://www.bmsi.com/python/milter.html",
py_modules=["mime"],
url="https://pythonhosted.org/milter/",
py_modules=modules,
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) ]
define_macros = [ ('MAX_ML_REPLY',32) ],
# save lots of debugging time testing rfc2553 compliance
extra_compile_args = [ "-Werror=implicit-function-declaration" ]
),
],
keywords = ['sendmail','milter'],
-19
View File
@@ -1,19 +0,0 @@
#!/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
View File
@@ -2,6 +2,7 @@ import unittest
import testmime
import testsample
import testutils
import testgrey
import os
def suite():
@@ -9,6 +10,7 @@ def suite():
s.addTest(testmime.suite())
s.addTest(testsample.suite())
s.addTest(testutils.suite())
s.addTest(testgrey.suite())
return s
if __name__ == '__main__':
+18587
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
import unittest
import doctest
import os
#from Milter.greylist import Greylist
from Milter.greysql import Greylist
class GreylistTestCase(unittest.TestCase):
def setUp(self):
self.fname = 'test.db'
if os.path.isfile(self.fname):
os.remove(self.fname)
def tearDown(self):
#os.remove(self.fname)
pass
def testGrey(self):
grey = Greylist(self.fname)
# first time
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com')
self.assertEqual(rc,0)
# not in window yet
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*60)
self.assertEqual(rc,0)
# within window
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=15*60)
self.assertEqual(rc,1)
# new triple
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=15*60)
self.assertEqual(rc,0)
# seen again
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*3600)
self.assertEqual(rc,2)
# new one past expire
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=6*3600)
self.assertEqual(rc,0)
# original past retain
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=37*24*3600)
self.assertEqual(rc,0)
# new one for testing expire
rc = grey.check('1.2.3.5','flub@bar.com','baz@spat.com',timeinc=20*24*3600)
self.assertEqual(rc,0)
grey.close()
# test cleanup
grey = Greylist(self.fname)
rc = grey.clean(timeinc=37*24*3600)
self.assertEqual(rc,1)
grey.close()
def suite():
s = unittest.makeSuite(GreylistTestCase,'test')
return s
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())
+86 -55
View File
@@ -1,4 +1,7 @@
# $Log$
# Revision 1.5 2011/06/09 17:27:42 customdesigned
# Documentation updates.
#
# Revision 1.4 2005/07/20 14:49:44 customdesigned
# Handle corrupt and empty ZIP files.
#
@@ -23,14 +26,21 @@
# Revision 1.20 2004/11/20 16:38:17 stuart
# Add rcs log
#
from __future__ import print_function
import unittest
import mime
import socket
import StringIO
try:
from StringIO import StringIO
except:
from io import StringIO
import email
import sys
import Milter
from email import Errors
try:
from email import Errors as errors
except:
from email import errors
samp1_txt1 = """Dear Agent 1
I hope you can read this. Whenever you write label it P.B.S kids.
@@ -43,48 +53,56 @@ class MimeTestCase(unittest.TestCase):
# test mime parameter parsing
def testParam(self):
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('; 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('; name="Jim&amp;amp;Girlz.jpg"')
self.failUnless(len(plist)==1)
self.failUnless(plist[0] == 'name="Jim&amp;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;amp;Girlz.jpg"')
def testParse(self,fname='samp1'):
msg = mime.message_from_file(open('test/'+fname,"r"))
self.failUnless(msg.ismultipart())
with open('test/'+fname,"rb") as fp:
msg = mime.message_from_file(fp)
self.assertTrue(msg.ismultipart())
parts = msg.get_payload()
self.failUnless(len(parts) == 2)
self.assertTrue(len(parts) == 2)
txt1 = parts[0].get_payload()
self.failUnless(txt1.rstrip() == samp1_txt1,txt1)
msg = mime.message_from_file(open('test/missingboundary',"r"))
self.assertTrue(txt1.rstrip() == samp1_txt1,txt1)
with open('test/missingboundary',"rb") as fp:
msg = mime.message_from_file(fp)
# should get no exception as long as we don't try to parse
# message attachments
mime.defang(msg,scan_rfc822=False)
msg.dump(open('test/missingboundary.out','w'))
msg = mime.message_from_file(open('test/missingboundary',"r"))
with open('test/missingboundary.out','wb') as fp:
msg.dump(fp)
with open('test/missingboundary',"rb") as fp:
msg = mime.message_from_file(fp)
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:
self.fail('should get boundary error parsing bad rfc822 attachment')
except errors.BoundaryError:
pass
def testDefang(self,vname='virus1',part=1,
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
msg = mime.message_from_file(open('test/'+vname,"r"))
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
with open('test/'+vname,"rb") as fp:
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True)
self.failUnless(msg.ismodified(),"virus not removed")
self.assertTrue(msg.ismodified(),"virus not removed")
oname = vname + '.out'
msg.dump(open('test/'+oname,"w"))
msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"wb") as fp:
msg.dump(fp)
with open('test/'+oname,"rb") as fp:
msg = mime.message_from_file(fp)
txt2 = msg.get_payload()
if type(txt2) == list:
txt2 = txt2[part].get_payload()
self.failUnless(
self.assertTrue(
txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
def testDefang3(self):
@@ -100,51 +118,60 @@ class MimeTestCase(unittest.TestCase):
# virus6 has no parts - the virus is directly inline
def testDefang6(self,vname="virus6",fname='FAX20.exe'):
msg = mime.message_from_file(open('test/'+vname,"r"))
with open('test/'+vname,"rb") as fp:
msg = mime.message_from_file(fp)
mime.defang(msg)
oname = vname + '.out'
msg.dump(open('test/'+oname,"w"))
msg = mime.message_from_file(open('test/'+oname,"r"))
self.failIf(msg.ismultipart())
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())
txt2 = msg.get_payload()
self.failUnless(txt2 == mime.virus_msg % \
(fname,hostname,None),txt2)
self.assertTrue(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'):
msg = mime.message_from_file(open('test/'+vname,"r"))
with open('test/'+vname,"rb") as fp:
msg = mime.message_from_file(fp)
mime.defang(msg)
oname = vname + '.out'
msg.dump(open('test/'+oname,"w"))
msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"wb") as fp:
msg.dump(fp)
with open('test/'+oname,"rb") as fp:
msg = mime.message_from_file(fp)
parts = msg.get_payload()
txt2 = parts[1].get_payload()
txt3 = parts[2].get_payload()
self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \
(fname,hostname,None),txt2)
self.assertTrue(txt2.rstrip()+'\n' == mime.virus_msg % \
(fname,hostname,None),txt2)
if txt3 != '':
self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \
self.assertTrue(txt3.rstrip()+'\n' == mime.virus_msg % \
('story[1].asp',hostname,None),txt3)
def testParse2(self,fname="spam7"):
msg = mime.message_from_file(open('test/'+fname,"r"))
self.failUnless(msg.ismultipart())
with open('test/'+fname,"rb") as fp:
msg = mime.message_from_file(fp)
self.assertTrue(msg.ismultipart())
parts = msg.get_payload()
self.failUnless(len(parts) == 2)
self.assertTrue(len(parts) == 2)
name = parts[1].getname()
self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name)
self.assertTrue(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name)
def testZip(self,vname="zip1",fname='zip.zip'):
self.testDefang(vname,1,'zip.zip')
# test scan_zip flag
msg = mime.message_from_file(open('test/'+vname,"r"))
with open('test/'+vname,"rb") as fp:
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=False)
self.failIf(msg.ismodified())
self.assertFalse(msg.ismodified())
# test ignoring empty zip (often found in DSNs)
msg = mime.message_from_file(open('test/zip2','r'))
with open('test/zip2','rb') as fp:
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True)
self.failIf(msg.ismodified())
self.assertFalse(msg.ismodified())
# test corrupt zip (often an EXE named as a ZIP)
self.testDefang('zip3',1,'zip.zip')
# test zip within zip
@@ -161,22 +188,24 @@ 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
msg = mime.message_from_file(open('test/'+fname,'r'))
with open('test/'+fname,'rb') as fp:
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True)
self.failIf(msg.ismodified())
msg = mime.message_from_file(open('test/tmpytgcE5.fail','r'))
self.assertFalse(msg.ismodified())
with open('test/test2','rb') as fp:
msg = mime.message_from_file(fp)
rc = mime.check_attachments(msg,self._chk_attach)
self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
self.assertEquals(rc,Milter.CONTINUE)
self.assertEqual(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
self.assertEqual(rc,Milter.CONTINUE)
def testHTML(self,fname=""):
result = StringIO.StringIO()
result = StringIO()
filter = mime.HTMLScriptFilter(result)
msg = """<! Illegal declaration used as comment>
<![if conditional]> Optional SGML <![endif]>
@@ -185,8 +214,10 @@ class MimeTestCase(unittest.TestCase):
script = "<script lang=javascript> Dangerous script </script>"
filter.feed(msg + script)
filter.close()
#print result.getvalue()
self.failUnless(result.getvalue() == msg + filter.msg)
#print(result.getvalue())
#print('---')
#print(msg + filter.msg)
self.assertTrue(result.getvalue() == msg + filter.msg)
def suite(): return unittest.makeSuite(MimeTestCase,'test')
@@ -195,7 +226,7 @@ if __name__ == '__main__':
unittest.main()
else:
for fname in sys.argv[1:]:
fp = open(fname,'r')
msg = mime.message_from_file(fp)
with open(fname,'rb') as fp:
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True)
print msg.as_string()
print(msg.as_string())
+54 -109
View File
@@ -2,112 +2,57 @@ import unittest
import Milter
import sample
import mime
import rfc822
import StringIO
from Milter.test import TestBase
from Milter.testctx import TestCtx
class TestMilter(sample.sampleMilter):
_protocol = 0
class TestMilter(TestBase,sample.sampleMilter):
def __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
TestBase.__init__(self)
sample.sampleMilter.__init__(self)
class BMSMilterTestCase(unittest.TestCase):
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)
rc = ctx._feedMsg(fname)
milter = ctx.getpriv()
# self.assertTrue(milter.user == 'batman',"getsymval failed: "+
# "%s != %s"%(milter.user,'batman'))
self.assertEquals(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
open('test/'+fname+".tstout","wb").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.failUnless(rc == Milter.CONTINUE)
self.assertTrue(rc == Milter.CONTINUE)
rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
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")
fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
open('test/'+fname+".tstout","wb").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()
@@ -118,30 +63,30 @@ class BMSMilterTestCase(unittest.TestCase):
milter = TestMilter()
milter.connect('somehost')
rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
self.assertTrue(rc == Milter.ACCEPT)
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.")
fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue())
open('test/'+fname+".tstout","wb").write(fp.getvalue())
milter.close()
def testDefang2(self):
milter = TestMilter()
milter.connect('somehost')
rc = milter.feedMsg('samp1')
self.failUnless(rc == Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
self.assertTrue(rc == Milter.ACCEPT)
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg("virus3")
self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.assertTrue(rc == Milter.ACCEPT)
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
fp = milter._body
open("test/virus3.tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
open("test/virus3.tstout","wb").write(fp.getvalue())
#self.assertTrue(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")
self.assertTrue(rc == Milter.ACCEPT)
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
self.assertTrue(milter._headerschanged,"Message headers not adjusted")
fp = milter._body
open("test/virus6.tstout","w").write(fp.getvalue())
open("test/virus6.tstout","wb").write(fp.getvalue())
milter.close()
def suite(): return unittest.makeSuite(BMSMilterTestCase,'test')
+24 -11
View File
@@ -1,9 +1,11 @@
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):
@@ -11,37 +13,48 @@ class AddrCacheTestCase(unittest.TestCase):
self.fname = 'test.dat'
def tearDown(self):
os.remove(self.fname)
if os.path.exists(self.fname):
os.remove(self.fname)
def testAdd(self):
cache = AddrCache(fname=self.fname)
cache['foo@bar.com'] = None
cache.addperm('baz@bar.com')
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.assertTrue(cache.has_key('foo@bar.com'))
self.assertTrue(not cache.has_key('hello@bar.com'))
self.assertTrue('baz@bar.com' in cache)
self.assertEquals(cache['temp@bar.com'],'testing')
s = open(self.fname).readlines()
self.failUnless(len(s) == 2)
self.failUnless(s[0].startswith('foo@bar.com '))
self.assertTrue(len(s) == 2)
self.assertTrue(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.failUnless(not cache['temp@bar.com'])
self.assertTrue(not cache['temp@bar.com'])
def testDomain(self):
fp = open(self.fname,'w')
print >>fp,'spammer.com'
fp.close()
with open(self.fname,'w') as fp:
print('spammer.com',file=fp)
cache = AddrCache(fname=self.fname)
cache.load(self.fname,30)
self.failUnless('spammer.com' in cache)
self.assertTrue('spammer.com' in cache)
def testParseHeader(self):
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
h = Milter.utils.parse_header(s)
self.assertEqual(h,b'Last Few Coldplay Album Artworks Available\x00')
@unittest.expectedFailure
def testParseAddress(self):
s = Milter.utils.parseaddr('a(WRONG)@b')
self.assertEqual(s,('WRONG', 'a@b'))
def suite():
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__':