Compare commits

...

170 Commits

Author SHA1 Message Date
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
Stuart Gathman f3fbb1c99d Missing regex 2012-11-15 03:53:02 +00:00
Stuart Gathman 27887daf3f Release 0.9.7 2012-11-13 02:43:44 +00:00
Stuart Gathman 23defb880b Update doc version to 0.9.6 2012-11-10 03:38:47 +00:00
Stuart Gathman 7502c29e47 Use functools.wraps for noreply decorator 2012-08-28 19:42:05 +00:00
Stuart Gathman 594d3ad365 Doc updates. 2012-08-28 06:02:36 +00:00
Stuart Gathman b2e0b2ebc6 Fix CNAME following bug. 2012-08-28 06:02:14 +00:00
Stuart Gathman 04a241f1e9 Ignore leading/trailing whitespace parsing IP6 addresses. 2012-07-13 21:50:52 +00:00
Stuart Gathman 16bfe5d4da Exceptions on unsupported result code for callback decorators. 2012-04-13 20:33:35 +00:00
Stuart Gathman 70d19001c0 Replace redundant callback array with macros. If this doesn't break anything,
macros can be eliminated with code changes.
2012-04-12 23:32:50 +00:00
Stuart Gathman 0d001dd8e9 Support RFC2553 on BSD 2012-04-12 23:08:06 +00:00
Stuart Gathman 8f4a82794c Release 0.9.6 2012-03-03 18:51:56 +00:00
Stuart Gathman de0ec3430d Start new release. 2012-02-26 01:35:58 +00:00
Stuart Gathman c9e32e4b06 throw ValueError when message line contains a single % 2012-02-25 15:53:45 +00:00
Stuart Gathman 83a1762515 New example 2011-11-05 15:51:03 +00:00
Stuart Gathman feb6526cb8 Grace period 2011-11-05 15:50:02 +00:00
Stuart Gathman 3a3add814e Very simple actual milter. 2011-11-05 14:28:56 +00:00
Stuart Gathman 1ba522e501 Release 0.9.5 2011-08-19 04:57:20 +00:00
Stuart Gathman a43649f2ce Make addr2bin and dynip handle IP6. 2011-08-19 04:53:56 +00:00
Stuart Gathman de679b1514 Add parameterless class decorators for P_RCPT_REJ and P_HEAD_LEADSPC 2011-06-17 19:41:23 +00:00
Stuart Gathman b946759857 Document threading limitations and show multiprocessing example. 2011-06-10 01:39:59 +00:00
Stuart Gathman f6702e39dd Require python-2.6.5 2011-06-09 21:36:26 +00:00
Stuart Gathman 5a8aaf85d7 Release 0.9.5 2011-06-09 19:55:31 +00:00
Stuart Gathman 720db3d7bd Doc updates. 2011-06-09 18:14:23 +00:00
Stuart Gathman a46627959c Documentation updates. 2011-06-09 17:27:43 +00:00
Stuart Gathman 4e0d3da07d Print callback name for non-int return error. 2011-06-09 15:45:27 +00:00
Stuart Gathman 53c7519922 Generate special exception when callback return not int. 2011-06-08 23:13:48 +00:00
Stuart Gathman b3d6328167 Fix template so it actually runs - makes a better example that way :-) 2011-06-08 22:34:53 +00:00
Stuart Gathman 2133942c19 Tolerate illegal chars 2011-05-17 21:51:57 +00:00
Stuart Gathman eef3cde27e Python2.6 SMTP.close() fails when instance never connected. 2011-03-18 20:41:31 +00:00
Stuart Gathman 5290bc0668 Release 1.0 2011-03-05 03:12:02 +00:00
Stuart Gathman 92ad624c3b Release 1.0 2011-03-05 03:09:57 +00:00
Stuart Gathman 7c5899b0cd Release 1.0 2011-03-05 03:07:39 +00:00
Stuart Gathman c6ccea9099 Fix exception test case 2011-03-03 05:58:50 +00:00
Stuart Gathman eea110d120 release 0.9.4 2011-03-03 05:16:50 +00:00
Stuart Gathman 4b2c08c0cf Release 0.9.4 2011-03-03 05:14:18 +00:00
Stuart Gathman 953e8a61fa Release 0.9.4 2011-03-03 05:11:58 +00:00
Stuart Gathman fa4408540e Handle IP6 in iniplist() 2011-03-01 19:46:31 +00:00
Stuart Gathman 65986632de Handle multiple recipients. For CBV or auto whitelist of multiple emails. 2010-10-11 00:29:47 +00:00
Stuart Gathman e44321561b Fix typos. 2009-09-28 02:05:00 +00:00
Stuart Gathman 344ee43f22 Release 0.9.3 2009-08-21 18:55:34 +00:00
Stuart Gathman 99bf3209c6 Release 0.9.3 2009-08-21 18:53:59 +00:00
Stuart Gathman 2848a090e3 Document milterContext 2009-07-28 22:31:34 +00:00
Stuart Gathman c29a21d2dd Document getdiag, getversion. 2009-07-28 22:13:46 +00:00
Stuart Gathman 25a02d9de2 Disable negotiate callback when runtime version < 1,0,1 2009-07-28 21:53:27 +00:00
Stuart Gathman c20e82e3d4 Add getversion() to return runtime version. 2009-07-28 21:45:54 +00:00
Stuart Gathman a3889189f0 Increment del count. 2009-07-28 21:08:20 +00:00
Stuart Gathman f86bda2ba4 getdiag method 2009-07-28 20:58:55 +00:00
Stuart Gathman 3ed14cc6ab Heuristic for invalid source route. 2009-07-04 14:03:09 +00:00
Stuart Gathman aeff1f8ab5 Skip source route in parseaddr. 2009-07-04 14:00:52 +00:00
Stuart Gathman a7bd7b71d8 Add dummy _protocol class var. 2009-07-04 13:59:40 +00:00
Stuart Gathman 939fc61df7 Handle @ in localpart. 2009-07-02 19:41:12 +00:00
Stuart Gathman f6a3b57fb9 enable_protocols class decorator, doc updates 2009-06-16 21:45:45 +00:00
Stuart Gathman 3428477eca Doxygen updates. 2009-06-13 21:15:12 +00:00
Stuart Gathman 144fe264c4 Document _actions, _protocol 2009-06-13 20:24:52 +00:00
Stuart Gathman a3530d4c49 Doxygen updates 2009-06-10 18:01:59 +00:00
Stuart Gathman 307c54e1b1 More doxygen docs. 2009-06-09 03:13:14 +00:00
Stuart Gathman 66f8a1d437 Forgot to initialize optional parameter. 2009-06-09 01:54:44 +00:00
Stuart Gathman 73e1f469ce Upgrade to doxygen-1.5.7 2009-06-06 00:47:41 +00:00
Stuart Gathman 2e45d6e187 Doxygen docs. 2009-06-06 00:24:09 +00:00
Stuart Gathman 6a1996117c Release 0.9.2-3 2009-06-04 22:17:40 +00:00
Stuart Gathman 77c0ce6b2e Avoid getpriv() overhead. 2009-06-04 22:16:32 +00:00
Stuart Gathman 7311f65150 Set milter_protocol attribute of noreply wrapper 2009-06-04 22:02:09 +00:00
Stuart Gathman 84bd61aac1 Wrap @noreply callbacks to return NOREPLY only when so negotiated. 2009-06-04 21:47:34 +00:00
Stuart Gathman 372fad6ac9 Release 0.9.2-2 2009-06-02 21:38:09 +00:00
Stuart Gathman 60963b3c37 Streamline negotiate 2009-06-02 17:49:49 +00:00
Stuart Gathman 6221f8b753 Validate methods passed to @noreply, @nocallback 2009-06-01 22:28:33 +00:00
Stuart Gathman 344ecc7c07 Typo SMFIP_NO constants. 2009-05-29 20:44:58 +00:00
Stuart Gathman ee14614c3e Typo SMFIS_ALL_OPTS 2009-05-29 19:53:36 +00:00
Stuart Gathman 4bb2403223 Typo calling helo instead of negotiate. 2009-05-29 19:49:40 +00:00
Stuart Gathman d58546930a Init future flags in negotiate. 2009-05-29 19:41:01 +00:00
Stuart Gathman f8efbb23df Create Milter on either connect or negotiate 2009-05-29 19:30:05 +00:00
Stuart Gathman 26b006455e Null terminate keyword list. 2009-05-29 18:25:59 +00:00
Stuart Gathman 9b7ca633f3 Release 0.9.2 2009-05-29 01:22:34 +00:00
Stuart Gathman 5928e99520 Remove amazon test since it contains copyrighted material. 2009-05-29 01:20:44 +00:00
Stuart Gathman 6d3833da72 Release 0.9.2 2009-05-29 01:16:27 +00:00
Stuart Gathman 2937935fea Comment updates 2009-05-29 01:14:44 +00:00
Stuart Gathman 31aa39034b Start with all symbols from milter module. 2009-05-28 18:54:48 +00:00
Stuart Gathman cb31963492 Support new callbacks, including negotiate 2009-05-28 18:36:43 +00:00
Stuart Gathman ed17f9cecf First cut at support unknown, data, negotiate callbacks. 2009-05-21 21:53:05 +00:00
Stuart Gathman 0e1a2de41f Support non-DSN CBV (non-empty MAIL FROM) 2009-05-20 20:08:44 +00:00
Stuart Gathman 9f419e3fc8 Release 0.9.1 2009-02-06 04:59:54 +00:00
Stuart Gathman 6913fd3e66 Release 0.9.1 2009-02-06 04:29:49 +00:00
Stuart Gathman 780ac63ebe Oops! Missing options argument pointer for addrcpt. 2009-02-06 04:28:08 +00:00
Stuart Gathman b51c08ba3a More changes from Fedora review. 2009-02-06 02:35:01 +00:00
Stuart Gathman 2e7805e531 Fedora core changes 2009-01-27 02:28:52 +00:00
Stuart Gathman b1eae98453 Changes for Fedora 2009-01-08 03:44:51 +00:00
Stuart Gathman 9118364164 Fedora release 2008-12-16 04:21:05 +00:00
Stuart Gathman 577c0bd134 Release 0.9.0 2008-12-14 03:01:43 +00:00
Stuart Gathman a97dbb8fd9 Release 0.8.12 2008-12-14 02:55:42 +00:00
Stuart Gathman df036eb55f Remove project docs 2008-12-14 02:54:46 +00:00
Stuart Gathman 7eede7ae31 Release 0.8.12 2008-12-13 21:08:51 +00:00
Stuart Gathman 37d4f99aaf Release 0.8.12 2008-12-13 21:06:16 +00:00
Stuart Gathman f55ddbce83 Split off milter applications. 2008-12-13 20:45:30 +00:00
Stuart Gathman 30f4c27c45 Split off milter applications. 2008-12-13 20:29:56 +00:00
Stuart Gathman 67cb78ded5 Fix some reject messages. 2008-12-06 21:13:57 +00:00
Stuart Gathman a1bbc31b11 Doc updates. 2008-12-04 19:43:00 +00:00
Stuart Gathman 14b95998c9 SPF Pass policy 2008-12-04 19:42:46 +00:00
Stuart Gathman 368ffd5374 Milter support for chgfrom. 2008-11-23 03:06:47 +00:00
Stuart Gathman f12bcf9af9 Support smfi_chgfrom and smfi_addrcpt_par. 2008-11-21 20:42:52 +00:00
Stuart Gathman 394e7c6b8e Use /var/run/milter/milter.pid if available. 2008-11-01 04:27:59 +00:00
Stuart Gathman 66314dc675 Example config had different names than actual code :-) 2008-10-23 19:58:06 +00:00
Stuart Gathman dad2f4f087 Allow NS queries with glue. 2008-10-12 01:54:16 +00:00
74 changed files with 23995 additions and 7094 deletions
+4
View File
@@ -0,0 +1,4 @@
*.pyc
build/
test/*.out
test/*.tstout
+8 -2
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,6 +7,12 @@ real, usable Python extension.
Other contributors (in random order):
Daniel Troeder
for pointing out a typo in @noreply
arkanes@irc.freenode.net
for suggesting a class method to compute and cache protocol masks
habnabit@habnabit.org
for suggesting function attributes and decorators for protocol negotiation
Dwayne Litzenberger, B.A.Sc.
for library_dirs patch to compile on Debian
Dave MacQuigg
@@ -37,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
+1522
View File
File diff suppressed because it is too large Load Diff
-154
View File
@@ -1,154 +0,0 @@
On Sun, 11 Feb 2007, Rick Saul wrote:
> Stuart I was planning to move to centos4.4 in a couple of weeks anyway...
> Your advice of where to go from here.
Oh - you are asking for a howto.
Step one. Which DSPAM is right for you?
The DSPAM project makes dspam part of the LDA (Local Delivery Agent).
Pydspam puts dspam into the MTA (Mail Transfer Agent - sendmail with pymilter).
The advantage of doing dspam in the LDA is that any aliasing has already been
resolved. You need only configure mailboxes.
The advantage of doing dspam in the MTA is it can screen an entire
company as a gateway with multiple domains. Unfortunately, this
means you have to tell it about all the aliases that comprise each
account. (Also, pydspam is still uses dspam-2.6.5.2 - the Dspam API
has changed for newer versions.)
If the LDA is right for you, you'll want to use the official Dspam
package. http://www.nuclearelephant.com/projects/dspam/
If the MTA approach is what you want, then pydspam is what you want.
In either case, you will still want pymilter to block forgeries, Windows
executables, etc.
So, lets assume you want to install pymilter, and may or may not
wish to install pydspam.
Step two. Obtaining RPMS.
For basic pymilter you'll need:
python-2.4
milter-0.8.10
sendmail-8.13.x (with milter support enabled)
and for SPF you'll need:
pydns-2.3.3-2.4
pyspf-2.0.5-1.py24
and for SRS you'll need:
pysrs-0.30.11-1.py24
I'm pretty sure you will want to have SPF and SRS available.
Step three. Activate basic milter.
Activate the basic milter and pysrs by editing /etc/mail/sendmail.mc and adding:
define(`NO_SRS_FILE',`/etc/mail/no-srs-mailers')dnl
dnl define(`NO_SRS_FROM_LOCAL')dnl
HACK(`pysrs',`/var/run/milter/pysrs')dnl
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
You can then "make sendmail.cf" and restart sendmail.
Start milter and pysrs with "service milter start", "service pysrs start".
Tail /var/log/milter/milter.log while SMTP clients connect to your
sendmail instance. This should show you what the milter is doing.
By default, milter-0.8.10 rejects on SPF fail.
Step four. Tweaking the basic config.
Most pymilter configuration is in /etc/mail/pymilter.cfg. To activate
changes, "service milter restart".
By default, milter scans attachments for executable extensions. You can
turn this off by setting banned_exts to the empty list. There are options
to scan ZIP attachments and rfc822 attachments. When it finds a banned
file type, milter saves the original message in /var/log/milter/save,
and replaces the attachment with a plain text warning message.
Configure hello_blacklist with your own helo name and domains - which
you know cannot legitimately be used by external MTAs.
Configure trusted_relay with your secondary MX servers, if any. These
should also run pymilter with similar policies. (But this isn't
needed for initial testing.)
Configure internal_connect with subnets of your internal SMTP clients.
Internal connections skip SPF testing and other policies. You will
likely need to set this to allow outgoing mail if you have
an SPF policy already.
Configure internal_domains with domains used by your internal SMTP clients.
If they attempt to use any other domain, the attempt is blocked and the
client is logged as a "zombie". Conversely, any attempt by an external
MTA to use one of your internal domains is treated as a forgery and
blocked (a simplified form of local SPF).
Adjust porn_words and spam_words - these block emails with a Subject
containing the listed strings. They can be empty to disable Subject
string blocking.
Advanced SPF configuration.
The sendmail access file, or another readonly database with that
format, can be used for detail spf policy. SPF access policy
record are tagged with "SPF-{Result}:". Results are
Pass, Neutral, Softfail, Fail, PermError. Currently supported
policy keywords are OK, CBV, REJECT. Currently, TempError always
results in TEMPFAIL.
The default policies are set in pymilter.cfg. The defaults
if none of the config options are set are as follows:
SPF-Fail: REJECT
SPF-Softfail: CBV
SPF-Neutral: OK
SPF-PermError: REJECT
SPF-Pass: OK
The tag may be followed by a specific domain. For instance, to
require a Pass from aol.com:
SPF-Neutral:aol.com REJECT
SPF-Softfail:aol.com REJECT
The CBV policy requires a valid HELO name. If the EHLO name is
RFC2822 compliant, then a DSN is sent to the alleged sender. The
template for the DSN is selected according to the SPF result:
Fail: fail.txt
SoftFail: softfail.txt
Neutral: neutral.txt
PermError: permerror.txt
None: strike3.txt
An SPF-Pass is always accepted by the milter. Domains can be blacklisted
via sendmail in the access file or via a RHS DNS blacklist.
To be continued.
Forthcoming topics:
SRS config
pydspam config
wiretap config
--
Stuart D. Gathman <stuart@bmsi.com>
Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flammis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.
+1 -19
View File
@@ -1,7 +1,6 @@
include COPYING
include TODO
include NEWS
include HOWTO
include CREDITS
include README
include ChangeLog
@@ -9,28 +8,11 @@ include MANIFEST.in
include testsample.py
include testmime.py
include testutils.py
include testbms.py
include rejects.py
include report.py
include bms.py
include spf.py
include cid2spf.py
include spfquery.py
include ban2zone.py
include test.py
include sample.py
include sgmllib.py
include milter-template.py
include spfmilter.py
include spfmilter.rc
include spfmilter.cfg
include test/*
include doc/*
include Milter/*.py
include *.spec
include start.sh
include milter.rc
include milter.rc7
include milter.cfg
include rhsbl.m4
include *.txt
include *.html
+647 -91
View File
@@ -1,59 +1,606 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc.
## @package Milter
# A thin OO wrapper for the milter module.
#
# Clients generally subclass Milter.Base and define callback
# methods.
#
# @author Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001,2009 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
# A thin OO wrapper for the milter module
from __future__ import print_function
__version__ = '1.0.1'
import os
import re
import milter
import thread
try:
import thread
except:
# libmilter uses posix threads
import _thread as thread
from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
set_flags, setdbg, setbacklog, settimeout, error, \
ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \
V1_ACTS, V2_ACTS, CURR_ACTS
try: from milter import QUARANTINE
except: pass
__version__ = '0.8.5'
from milter import *
from functools import wraps
_seq_lock = thread.allocate_lock()
_seq = 0
def uniqueID():
"""Return a sequence number unique to this process.
"""Return a unique sequence number (incremented on each call).
"""
global _seq
_seq_lock.acquire()
seqno = _seq = _seq + 1
_seq_lock.release()
return seqno
class Milter:
"""A simple class interface to the milter module.
"""
## @private
OPTIONAL_CALLBACKS = {
'connect':(P_NR_CONN,P_NOCONNECT),
'hello':(P_NR_HELO,P_NOHELO),
'envfrom':(P_NR_MAIL,P_NOMAIL),
'envrcpt':(P_NR_RCPT,P_NORCPT),
'data':(P_NR_DATA,P_NODATA),
'unknown':(P_NR_UNKN,P_NOUNKNOWN),
'eoh':(P_NR_EOH,P_NOEOH),
'body':(P_NR_BODY,P_NOBODY),
'header':(P_NR_HDR,P_NOHDRS)
}
## @private
R = re.compile(r'%+')
## @private
def decode_mask(bits,names):
t = [ (s,getattr(milter,s)) for s in names]
nms = [s for s,m in t if bits & m]
for s,m in t: bits &= ~m
if bits: nms += hex(bits)
return nms
## Class decorator to enable optional protocol steps.
# P_SKIP is enabled by default when supported, but
# applications may wish to enable P_HDR_LEADSPC
# to send and receive the leading space of header continuation
# lines unchanged, and/or P_RCPT_REJ to have recipients
# detected as invalid by the MTA passed to the envcrpt callback.
#
# Applications may want to check whether the protocol is actually
# supported by the MTA in use. Base._protocol
# is a bitmask of protocol options negotiated. So,
# for instance, if <code>self._protocol & Milter.P_RCPT_REJ</code>
# is true, then that feature was successfully negotiated with the MTA
# and the application will see recipients the MTA has flagged as invalid.
#
# Sample use:
# <pre>
# class myMilter(Milter.Base):
# def envrcpt(self,to,*params):
# return Milter.CONTINUE
# myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ)
# </pre>
# @since 0.9.3
# @param klass the %milter application class to modify
# @param mask a bitmask of protocol steps to enable
# @return the modified %milter class
def enable_protocols(klass,mask):
klass._protocol_mask = klass.protocol_mask() & ~mask
return klass
## Milter rejected recipients. A class decorator that calls
# enable_protocols() with the P_RCPT_REJ flag. By default, the MTA
# does not pass recipients that it knows are invalid on to the milter.
# This decorator enables a %milter app to see all recipients if supported
# by the MTA. Use like this with python-2.6 and later:
# <pre>
# @@Milter.rejected_recipients
# class myMilter(Milter.Base):
# def envrcpt(self,to,*params):
# return Milter.CONTINUE
# </pre>
# @since 0.9.5
# @param klass the %milter application class to modify
# @return the modified %milter class
def rejected_recipients(klass):
return enable_protocols(klass,P_RCPT_REJ)
## Milter leading space on headers. A class decorator that calls
# enable_protocols() with the P_HDR_LEADSPC flag. By default,
# 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.
# With this flag, header continuation lines are preserved
# with their newlines and leading space. In addition, header folding
# done by the milter is preserved as well.
# Use like this with python-2.6 and later:
# <pre>
# @@Milter.header_leading_space
# class myMilter(Milter.Base):
# def header(self,hname,value):
# return Milter.CONTINUE
# </pre>
# @since 0.9.5
# @param klass the %milter application class to modify
# @return the modified %milter class
def header_leading_space(klass):
return enable_protocols(klass,P_HDR_LEADSPC)
## Function decorator to disable callback methods.
# If the MTA supports it, tells the MTA not to invoke this callback,
# increasing efficiency. All the callbacks (except negotiate)
# are disabled in Milter.Base, and overriding them reenables the
# callback. An application may need to use @@nocallback when it extends
# another %milter and wants to disable a callback again.
# The disabled method should still return Milter.CONTINUE, in case the MTA does
# not support protocol negotiation, and for when called from a test harness.
# @since 0.9.2
def nocallback(func):
try:
func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1]
except KeyError:
raise ValueError(
'@nocallback applied to non-optional method: '+func.__name__)
def wrapper(self,*args):
if func(self,*args) != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
% func.__name__)
return CONTINUE
return wrapper
## Function decorator to disable callback reply.
# If the MTA supports it, tells the MTA not to wait for a reply from
# this callback, and assume CONTINUE. The method should still return
# CONTINUE in case the MTA does not support protocol negotiation.
# The decorator arranges to change the return code to NOREPLY
# when supported by the MTA.
# @since 0.9.2
def noreply(func):
try:
nr_mask = OPTIONAL_CALLBACKS[func.__name__][0]
except KeyError:
raise ValueError(
'@noreply applied to non-optional method: '+func.__name__)
@wraps(func)
def wrapper(self,*args):
rc = func(self,*args)
if self._protocol & nr_mask:
if rc != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @noreply'
% func.__name__)
return NOREPLY
return rc
wrapper.milter_protocol = nr_mask
return wrapper
## 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
# connection in the negotiate callback. If the application then calls
# the feature anyway via an instance method, this exception is
# thrown.
# @since 0.9.2
class DisabledAction(RuntimeError):
pass
## A do "nothing" Milter base class representing an SMTP connection.
#
# Python milters should derive from this class
# unless they are using the low level milter module directly.
#
# Most of the methods are either "actions" or "callbacks". Callbacks
# are invoked by the MTA at certain points in the SMTP protocol. For
# instance when the HELO command is seen, the MTA calls the helo
# callback before returning a response code. All callbacks must
# return one of these constants: CONTINUE, TEMPFAIL, REJECT, ACCEPT,
# DISCARD, SKIP. The NOREPLY response is supplied automatically by
# the @@noreply decorator if negotiation with the MTA is successful.
# @@noreply and @@nocallback methods should return CONTINUE for two reasons:
# the MTA may not support negotiation, and the class may be running in a test
# harness.
#
# Optional callbacks are disabled with the @@nocallback decorator, and
# automatically reenabled when overridden. Disabled callbacks should
# still return CONTINUE for testing and MTAs that do not support
# negotiation.
# Each SMTP connection to the MTA calls the factory method you provide to
# create an instance derived from this class. This is typically the
# constructor for a class derived from Base. The _setctx() method attaches
# the instance to the low level milter.milterContext object. When the SMTP
# connection terminates, the close callback is called, the low level connection
# object is destroyed, and this normally causes instances of this class to be
# garbage collected as well. The close() method should release any global
# resources held by instances.
# @since 0.9.2
class Base(object):
"The core class interface to the %milter module."
## Attach this Milter to the low level milter.milterContext object.
def _setctx(self,ctx):
self.__ctx = ctx
## The low level @ref milter.milterContext object.
self._ctx = ctx
## A bitmask of actions this connection has negotiated to use.
# By default, all actions are enabled. High throughput milters
# may want to disable unused actions to increase efficiency.
# Some optional actions may be disabled by calling milter.set_flags(), or
# by overriding the negotiate callback. The bits include:
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
# CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</code>.
# 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
# which actions are available. This is especially useful in
# generic library code designed to work in multiple milters.
# @since 0.9.2
#
self._actions = CURR_ACTS # all actions enabled by default
## A bitmask of protocol options this connection has negotiated.
# An application may inspect this
# variable to determine which protocol steps are supported. Options
# of interest to applications: the SKIP result code is allowed
# only if the P_SKIP bit is set, rejected recipients are passed to the
# %milter application only if the P_RCPT_REJ bit is set, and
# header values are sent and received with leading spaces (in the
# continuation lines) intact if the P_HDR_LEADSPC bit is set (so
# that the application can customize indenting).
#
# The P_N* bits should be negotiated via the @@noreply and @@nocallback
# method decorators, and P_RCPT_REJ, P_HDR_LEADSPC should
# be enabled using the enable_protocols class decorator.
#
# The bits include: <code>
# P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN
# P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT
# P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP
# </code> (all under the Milter namespace).
# @since 0.9.2
self._protocol = 0 # no protocol options by default
if ctx:
ctx.setpriv(self)
# user replaceable callbacks
def log(self,*msg):
print 'Milter:',
for i in msg: print i,
print
## Defined by subclasses to write log messages.
def log(self,*msg): pass
## Called for each connection to the MTA. Called by the
# <a href="milter_api/xxfi_connect.html">
# 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.
# The format of hostaddr depends on the socket family:
# <dl>
# <dt><code>socket.AF_INET</code>
# <dd>A tuple of (IP as string in dotted quad form, integer port)
# <dt><code>socket.AF_INET6</code>
# <dd>A tuple of (IP as a string in standard representation,
# integer port, integer flow info, integer scope id)
# <dt><code>socket.AF_UNIX</code>
# <dd>A string with the socketname
# </dl>
# To vary behavior based on what port the client connected to,
# for example skipping blacklist checks for port 587 (which must
# be authenticated), use @link #getsymval getsymval('{daemon_port}') @endlink.
# The <code>{daemon_port}</code> macro must be enabled in sendmail.cf
# <pre>
# O Milter.macros.connect=j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}
# </pre>
# or sendmail.mc
# <pre>
# define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}'')dnl
# </pre>
# @param hostname the PTR name or bracketed IP of the SMTP client
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
# or <code>socket.AF_UNIX</code>
# @param hostaddr a tuple or string with peer IP or socketname
@nocallback
def connect(self,hostname,family,hostaddr): return CONTINUE
## Called when the SMTP client says HELO.
# Returning REJECT prevents progress until a valid HELO is provided;
# this almost always results in terminating the connection.
@nocallback
def hello(self,hostname): return CONTINUE
## Called when the SMTP client says MAIL FROM. Called by the
# <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
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
# For the From: header (author) defined in
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
# see @link #header the header callback @endlink.
@nocallback
def envfrom(self,f,*str): return CONTINUE
## Called when the SMTP client says RCPT TO. Called by the
# <a href="milter_api/xxfi_envrcpt.html">
# xxfi_envrcpt</a> callback.
# Returning REJECT rejects the current recipient, not the entire message.
# The recipient is the "envelope" recipient as defined by
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
# For recipients defined in
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
# for example To: or Cc:, see @link #header the header callback @endlink.
@nocallback
def envrcpt(self,to,*str): return CONTINUE
## Called when the SMTP client says DATA.
# Returning REJECT rejects the message without wasting bandwidth
# on the unwanted message.
# @since 0.9.2
@nocallback
def data(self): return CONTINUE
## Called for each header field in the message body.
@nocallback
def header(self,field,value): return CONTINUE
## Called at the blank line that terminates the header fields.
@nocallback
def eoh(self): return CONTINUE
## Called to supply the body of the message to the Milter by chunks.
# @param blk a block of message bytes
@nocallback
def body(self,blk): return CONTINUE
## Called when the SMTP client issues an unknown command.
# @param cmd the unknown command
# @since 0.9.2
@nocallback
def unknown(self,cmd): return CONTINUE
## Called at the end of the message body.
# Most of the message manipulation actions can only take place from
# the eom callback.
def eom(self): return CONTINUE
## Called when the connection is abnormally terminated.
# The close callback is still called also.
def abort(self): return CONTINUE
## Called when the connection is closed.
def close(self): return CONTINUE
## Return mask of SMFIP_N* protocol option bits to clear for this class
# The @@nocallback and @@noreply decorators set the
# <code>milter_protocol</code> function attribute to the protocol mask bit to
# pass to libmilter, causing that callback or its reply to be skipped.
# Overriding a method creates a new function object, so that
# <code>milter_protocol</code> defaults to 0.
# Libmilter passes the protocol bits that the current MTA knows
# how to skip. We clear the ones we don't want to skip.
# The negation is somewhat mind bending, but it is simple.
# @since 0.9.2
@classmethod
def protocol_mask(klass):
try:
return klass._protocol_mask
except AttributeError:
p = P_RCPT_REJ | P_HDR_LEADSPC # turn these new features off by default
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))
p |= (nr|nc) & ~ca
klass._protocol_mask = p
return p
## Negotiate milter protocol options. Called by the
# <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
# class and function decorators.
# Options are passed as
# a list of 4 32-bit ints which can be modified and are passed
# back to libmilter on return.
# Default negotiation sets P_NO* and P_NR* for callbacks
# marked @@nocallback and @@noreply respectively, leaves all
# actions enabled, and enables Milter.SKIP. The @@enable_protocols
# class decorator can customize which protocol steps are implemented.
# @param opts a modifiable list of 4 ints with negotiated options
# @since 0.9.2
def negotiate(self,opts):
try:
self._actions,p,f1,f2 = opts
opts[1] = self._protocol = p & ~self.protocol_mask()
opts[2] = 0
opts[3] = 0
#self.log("Negotiated:",opts)
except:
# don't change anything if something went wrong
return ALL_OPTS
return CONTINUE
# Milter methods which can be invoked from most callbacks
## Return the value of an MTA macro. Sendmail macro names
# are either single chars (e.g. "j") or multiple chars enclosed
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
# See <a href="milter_api/smfi_getsymval.html">
# smfi_getsymval</a> for default sendmail macros.
# @param sym the macro name
def getsymval(self,sym):
return self._ctx.getsymval(sym)
## 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
# must be doubled, or libmilter will silently ignore the setreply.
# Beginning with 0.9.6, we test for that case and throw ValueError to avoid
# head scratching. What will <i>really</i> irritate you, however,
# is that if you carefully double any '%%', your message will be
# sent - but with the '%%' still doubled!
# See <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> for more information.
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
# @param msg The text part of the SMTP reply. If msg is None,
# an empty message is used.
# @param ml Optional additional message lines.
def setreply(self,rcode,xcode=None,msg=None,*ml):
for m in (msg,)+ml:
if 1 in [len(s)&1 for s in R.findall(m)]:
raise ValueError("'%' must be doubled: "+m)
return self._ctx.setreply(rcode,xcode,msg,*ml)
## Tell the MTA which macro names will be used.
# This information can reduce the size of messages received from sendmail,
# and hence could reduce bandwidth between sendmail and your milter where
# that is a factor. The <code>Milter.SETSYMLIST</code> action flag must be
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT,
# M_DATA, M_EOM, M_EOH.
#
# May only be called from negotiate 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")
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(' ')
except: pass
a += m
return self._ctx.setsmlist(stage,' '.join(a))
# Milter methods which can only be called from eom callback.
## Add a mail header field.
# Calls <a href="milter_api/smfi_addheader.html">
# smfi_addheader</a>.
# The <code>Milter.ADDHDRS</code> action flag must be set.
#
# May be called from eom callback only.
# @param field the header field name
# @param value the header field value
# @param idx header field index from the top of the message to insert at
# @throws DisabledAction if ADDHDRS is not enabled
def addheader(self,field,value,idx=-1):
if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
return self._ctx.addheader(field,value,idx)
## Change the value of a mail header field.
# Calls <a href="milter_api/smfi_chgheader.html">
# smfi_chgheader</a>.
# The <code>Milter.CHGHDRS</code> action flag must be set.
#
# May be called from eom callback only.
# @param field the name of the field to change
# @param idx index of the field to change when there are multiple instances
# @param value the new value of the field
# @throws DisabledAction if CHGHDRS is not enabled
def chgheader(self,field,idx,value):
if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
return self._ctx.chgheader(field,idx,value)
## Add a recipient to the message.
# 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
# RCPT TO command (and as delivered to the envrcpt callback), for example
# "self.addrcpt('<foo@example.com>')".
# The <code>Milter.ADDRCPT</code> action flag must be set.
# If the optional <code>params</code> argument is used, then
# the <code>Milter.ADDRCPT_PAR</code> action flag must be set.
#
# May be called from eom callback only.
# @param rcpt the message recipient
# @param params an optional list of ESMTP parameters
# @throws DisabledAction if ADDRCPT or ADDRCPT_PAR is not enabled
def addrcpt(self,rcpt,params=None):
if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT")
if params and not self._actions & ADDRCPT_PAR:
raise DisabledAction("ADDRCPT_PAR")
return self._ctx.addrcpt(rcpt,params)
## Delete a recipient from the message.
# 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.
#
# May be called from eom callback only.
# @param rcpt the message recipient to delete
# @throws DisabledAction if DELRCPT is not enabled
def delrcpt(self,rcpt):
if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
return self._ctx.delrcpt(rcpt)
## Replace the message body.
# 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.
# The <code>Milter.MODBODY</code> action flag must be set.
#
# May be called from eom callback only.
# @param body a chunk of body data
# @throws DisabledAction if MODBODY is not enabled
def replacebody(self,body):
if not self._actions & MODBODY: raise DisabledAction("MODBODY")
return self._ctx.replacebody(body)
## Change the SMTP envelope sender address.
# 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),
# for example <code>self.chgfrom('<bar@example.com>')</code>.
# The <code>Milter.CHGFROM</code> action flag must be set.
#
# May be called from eom callback only.
# @since 0.9.1
# @param sender the new sender address
# @param params an optional list of ESMTP parameters
# @throws DisabledAction if CHGFROM is not enabled
def chgfrom(self,sender,params=None):
if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
return self._ctx.chgfrom(sender,params)
## Quarantine the message.
# 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.
# The <code>Milter.QUARANTINE</code> action flag must be set.
#
# May be called from eom callback only.
# @param reason a string describing the reason for quarantine
# @throws DisabledAction if QUARANTINE is not enabled
def quarantine(self,reason):
if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
return self._ctx.quarantine(reason)
## Tell the MTA to wait a bit longer.
# Calls <a href="milter_api/smfi_progress.html">
# smfi_progress</a>.
# Resets timeouts in the MTA that detect a "hung" milter.
def progress(self):
return self._ctx.progress()
## A logging but otherwise do nothing Milter base class.
# This is included for compatibility with previous versions of pymilter.
# The logging callbacks are marked @@noreply.
class Milter(Base):
"A simple class interface to the milter module."
## Provide simple logging to sys.stdout
def log(self,*msg):
print('Milter:',end=None)
for i in msg: print(i,end=None)
print()
@noreply
def connect(self,hostname,family,hostaddr):
"Called for each connection to sendmail."
self.log("connect from %s at %s" % (hostname,hostaddr))
return CONTINUE
@noreply
def hello(self,hostname):
"Called after the HELO command."
self.log("hello from %s" % hostname)
return CONTINUE
@noreply
def envfrom(self,f,*str):
"""Called to begin each message.
f -> string message sender
@@ -62,25 +609,24 @@ class Milter:
self.log("mail from",f,str)
return CONTINUE
@noreply
def envrcpt(self,to,*str):
"Called for each message recipient."
self.log("rcpt to",to,str)
return CONTINUE
@noreply
def header(self,field,value):
"Called for each message header."
self.log("%s: %s" % (field,value))
return CONTINUE
@noreply
def eoh(self):
"Called after all headers are processed."
self.log("eoh")
return CONTINUE
def body(self,unused):
"Called to transfer the message body."
return CONTINUE
def eom(self):
"Called at the end of message."
self.log("eom")
@@ -96,55 +642,49 @@ class Milter:
self.log("close")
return CONTINUE
# Milter methods which can be invoked from callbacks
def getsymval(self,sym):
return self.__ctx.getsymval(sym)
# If sendmail does not support setmlreply, then only the
# first msg line is used.
def setreply(self,rcode,xcode=None,msg=None,*ml):
return self.__ctx.setreply(rcode,xcode,msg,*ml)
# Milter methods which can only be called from eom callback.
def addheader(self,field,value,idx=-1):
return self.__ctx.addheader(field,value,idx)
def chgheader(self,field,idx,value):
return self.__ctx.chgheader(field,idx,value)
def addrcpt(self,rcpt):
return self.__ctx.addrcpt(rcpt)
def delrcpt(self,rcpt):
return self.__ctx.delrcpt(rcpt)
def replacebody(self,body):
return self.__ctx.replacebody(body)
# When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined.
def quarantine(self,reason):
return self.__ctx.quarantine(reason)
def progress(self):
return self.__ctx.progress()
## The milter connection factory
# This factory method is called for each connection to create the
# python object that tracks the connection. It should return
# an object derived from Milter.Base.
#
# Note that since python is dynamic, this variable can be changed while
# the milter is running: for instance, to a new subclass based on a
# change in configuration.
factory = Milter
def connectcallback(ctx,hostname,family,hostaddr):
## @private
# @brief Connect context to connection instance and return enabled callbacks.
def negotiate_callback(ctx,opts):
m = factory()
m._setctx(ctx)
return m.negotiate(opts)
## @private
# @brief Connect context if needed and invoke connect method.
def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
m = ctx.getpriv()
if not m:
# If not already created (because the current MTA doesn't support
# xmfi_negotiate), create the connection object.
m = factory()
m._setctx(ctx)
return m.connect(hostname,family,hostaddr)
def closecallback(ctx):
## @private
# @brief Disconnect milterContext and call close method.
def close_callback(ctx):
m = ctx.getpriv()
if not m: return CONTINUE
rc = m.close()
m._setctx(None) # release milterContext
try:
rc = m.close()
finally:
m._setctx(None) # release milterContext
return rc
## Convert ESMTP parameters with values to a keyword dictionary.
# @deprecated You probably want Milter.param2dict instead.
def dictfromlist(args):
"Convert ESMTP parm list to keyword dictionary."
"Convert ESMTP parms with values to keyword dictionary."
kw = {}
for s in args:
pos = s.find('=')
@@ -152,6 +692,18 @@ def dictfromlist(args):
kw[s[:pos].upper()] = s[pos+1:]
return kw
## Convert ESMTP parm list to keyword dictionary.
# Params with no value are set to None in the dictionary.
# @since 0.9.3
# @param str list of param strings of the form "NAME" or "NAME=VALUE"
# @return a dictionary of ESMTP param names and values
def param2dict(str):
"Convert ESMTP parm list to keyword dictionary."
pairs = [x.split('=',1) for x in str]
for e in pairs:
if len(e) < 2: e.append(None)
return dict([(k.upper(),v) for k,v in pairs])
def envcallback(c,args):
"""Call function c with ESMTP parms converted to keyword parameters.
Can be used in the envfrom and/or envrcpt callbacks to process
@@ -166,32 +718,16 @@ def envcallback(c,args):
pargs.append(s)
return c(*pargs,**kw)
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)
## Run the %milter.
# @param name the name of the %milter known to the MTA
# @param socketname the socket to be passed to milter.setconn()
# @param timeout the time in secs the MTA should wait for a response before
# considering this %milter dead
def runmilter(name,socketname,timeout = 0,rmsock=True):
# The default flags set include everything
# milter.set_flags(milter.ADDHDRS)
milter.set_connect_callback(connectcallback)
milter.set_connect_callback(connect_callback)
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
# For envfrom and envrcpt, we would like to convert ESMTP parms to keyword
# parms, but then all existing users would have to include **kw to accept
@@ -204,12 +740,28 @@ def runmilter(name,socketname,timeout = 0):
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
milter.set_abort_callback(lambda ctx: ctx.getpriv().abort())
milter.set_close_callback(closecallback)
milter.set_close_callback(close_callback)
milter.setconn(socketname)
if timeout > 0: milter.settimeout(timeout)
# disable negotiate callback if runtime version < (1,0,1)
ncb = negotiate_callback
if milter.getversion() < (1,0,1):
ncb = None
# The name *must* match the X line in sendmail.cf (supposedly)
milter.register(name)
milter.register(name,
data=lambda ctx: ctx.getpriv().data(),
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()
@@ -222,3 +774,7 @@ __all__ = globals().copy()
for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'):
del __all__[priv]
__all__ = __all__.keys()
## @example milter-template.py
## @example milter-nomix.py
#
+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)
+20 -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,17 +45,22 @@ 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):
if self.has_option(sect,opt):
return self.get(sect,opt)
return default
def getintdefault(self,sect,opt,default=None):
if self.has_option(sect,opt):
return self.getint(sect,opt)
return default
+58 -21
View File
@@ -1,12 +1,23 @@
# provide a higher level interface to pydns
## @package Milter.dns
# Provide a higher level interface to pydns.
from __future__ import print_function
import DNS
from DNS import DNSError
MAX_CNAME = 10
## Lookup DNS records by label and RR type.
# The response can include records of other types that the DNS
# server thinks we might need.
# @param name the DNS label to lookup
# @param qtype the name of the DNS RR type to lookup
# @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.
req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req()
#resp.show()
@@ -24,23 +35,28 @@ class Session(object):
def __init__(self):
self.cache = {}
## Additional DNS RRs we can safely cache.
# We have to be careful which additional DNS RRs we cache. For
# instance, PTR records are controlled by the connecting IP, and they
# could poison our local cache with bogus A and MX records.
# Each entry is a tuple of (query_type,rr_type). So for instance,
# the entry ('MX','A') says it is safe (for milter purposes) to cache
# any 'A' RRs found in an 'MX' query.
SAFE2CACHE = frozenset((
('MX','MX'), ('MX','A'),
('CNAME','CNAME'), ('CNAME','A'),
('A','A'),
('AAAA','AAAA'),
('PTR','PTR'),
('NS','NS'), ('NS','A'),
('TXT','TXT'),
('SPF','SPF')
))
SAFE2CACHE = {
('MX','A'): None,
('MX','MX'): None,
('CNAME','A'): None,
('CNAME','CNAME'): None,
('A','A'): None,
('AAAA','AAAA'): None,
('PTR','PTR'): None,
('TXT','TXT'): None,
('SPF','SPF'): None
}
## Cached DNS lookup.
# @param name the DNS label to query
# @param qtype the query type, e.g. 'A'
# @param cnames tracks CNAMES already followed in recursive calls
def dns(self, name, qtype, cnames=None):
"""DNS query.
@@ -55,15 +71,24 @@ class Session(object):
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
post: isinstance(__return__, types.ListType)
"""
if name.endswith('.'): name = name[:-1]
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
return [] # invalid DNS name (too long or empty)
name = name.lower()
result = self.cache.get( (name, qtype) )
cname = None
if result: return result
cnamek = (name,'CNAME')
cname = self.cache.get( cnamek )
if not result:
if cname:
cname = cname[0]
else:
safe2cache = Session.SAFE2CACHE
for k, v in DNSLookup(name, qtype):
if k == (name, 'CNAME'):
if k == cnamek:
cname = v
if (qtype,k[1]) in safe2cache:
if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache:
self.cache.setdefault(k, []).append(v)
result = self.cache.get( (name, qtype), [])
if not result and cname:
@@ -73,16 +98,28 @@ 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:
raise DNSError, 'CNAME loop'
if cname.lower().rstrip('.') in cnames:
raise DNSError('CNAME loop')
result = self.dns(cname, qtype, cnames=cnames)
if result:
self.cache[(name,qtype)] = result
return result
def dns_txt(self, domainname, enc='ascii'):
"Get a list of TXT records for a domain name."
if domainname:
try:
return [''.join(s.decode(enc) for s in a)
for a in self.dns(domainname, 'TXT')]
except UnicodeEncodeError:
raise DNSError('Non-ascii character in SPF TXT record.')
return []
DNS.DiscoverNameServers()
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))
+88 -19
View File
@@ -5,6 +5,27 @@
# 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
#
# Revision 1.20 2010/10/11 00:29:47 customdesigned
# Handle multiple recipients. For CBV or auto whitelist of multiple emails.
#
# Revision 1.19 2009/07/02 19:41:12 customdesigned
# Handle @ in localpart.
#
# Revision 1.18 2009/06/10 18:01:59 customdesigned
# Doxygen updates
#
# Revision 1.17 2009/05/20 20:08:44 customdesigned
# Support non-DSN CBV (non-empty MAIL FROM)
#
# Revision 1.16 2007/09/25 01:24:59 customdesigned
# Allow arbitrary object, not just spf.query like, to provide data for create_msg
#
# Revision 1.15 2007/09/24 20:13:26 customdesigned
# Remove explicit spf dependency.
#
@@ -23,7 +44,32 @@
# Revision 1.10 2006/05/24 20:56:35 customdesigned
# Remove default templates. Scrub test.
#
## @package Milter.dsn
# Support DSNs and CallBackValidations (CBV).
#
# A Delivery Status Notification (bounce) is sent to the envelope
# sender (original MAIL FROM) with a null MAIL FROM (<>) to notify the
# original sender # of delays or problems with delivery. A Callback Validation
# starts the DSN process, but stops before issuing the DATA command. The
# purpose is to check whether the envelope recipient is accepted (and is
# therefore a valid email). The null MAIL FROM tells the remote
# MTA to never reply according to RFC2821 (but some braindead MTAs
# reply anyway, of course).
#
# Milters should cache CBV results and should avoid sending DSNs
# unless the sender is authenticated somehow (e.g. SPF Pass). However,
# when email is quarantined, and is not known to be a forgery, sending a DSN
# is better than silently disappearing, and a DSN is better than sending
# a normal message as notification - because MAIL FROM signing schemes
# can reject bounces of forged emails. Whatever you do, don't copy those
# assinine commercial filters that send a normal message to notify you
# that some virus is forging your email.
#
# <b>DSNs should *only* be sent to MAIL FROM addresses.</b> Never send
# 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
@@ -31,12 +77,25 @@ import Milter
import time
import dns
def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
## Send DSN.
# Try the published MX names in order, rejecting obviously bogus entries
# (like <code>localhost</code>).
# @param mailfrom the original sender we are notifying or validating
# @param receiver the HELO name of the MTA we are sending the DSN on behalf of.
# Be sure to send from an IP that matches the HELO.
# @param msg the DSN message in RFC2822 format, or None for CBV.
# @param timeout total seconds to wait for a response from an MX
# @param session Milter.dns.Session object from current incoming mail
# session to reuse its cache, or None to create a fresh one.
# @param ourfrom set to a valid email to send a normal notification from, or
# to validate emails not obtained from MAIL FROM.
# @return None on success or (status_code,msg) on failure.
def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
"""Send DSN. If msg is None, do callback verification.
Mailfrom is original sender we are sending DSN or CBV to.
Receiver is the MTA sending the DSN.
Return None for success or (code,msg) for failure."""
user,domain = mailfrom.split('@')
user,domain = mailfrom.rsplit('@',1)
if not session: session = dns.Session()
try:
mxlist = session.dns(domain,'MX')
@@ -59,24 +118,34 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
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('<>',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: <>')
if code != 250:
raise smtplib.SMTPSenderRefused(code, resp, '<>')
code,resp = smtp.rcpt(mailfrom)
if code not in (250,251):
return (code,resp) # permanent error
smtp.quit()
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 = {}
for rcpt in mailfrom:
code,resp = smtp.rcpt(rcpt)
if code not in (250,251):
badrcpts[rcpt] = (code,resp)# permanent error
smtp.quit()
if len(badrcpts) == 1:
return badrcpts.values()[0] # permanent error
if badrcpts:
return badrcpts
return None # success
except smtplib.SMTPRecipientsRefused,x:
return x.recipients[mailfrom] # permanent error
if len(x.recipients) == 1:
return x.recipients.values()[0] # permanent error
return x.recipients
except smtplib.SMTPSenderRefused,x:
return x.args[:2] # does not accept DSN
except smtplib.SMTPDataError,x:
@@ -87,7 +156,7 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
pass # MX didn't accept connections, try next one
except socket.timeout:
pass # MX too slow, try next one
smtp.close()
if hasattr(smtp,'sock'): smtp.close()
if time.time() > toolate:
return (450,'No MX response within %f minutes'%(timeout/60.0))
return (450,'No MX servers available') # temp error
@@ -162,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()))
+10 -8
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}')
@@ -48,15 +49,16 @@ def is_dynip(host,addr):
True
"""
if host.startswith('[') and host.endswith(']'):
return True
return True # no ptr
if 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
@@ -67,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
@@ -85,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))
+56 -9
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()
elif now < r.firstseen + self.greylist_time:
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)
+118
View File
@@ -0,0 +1,118 @@
"""Pure Python IP6 parsing and formatting
Copyright (c) 2006 Stuart Gathman <stuart@bmsi.com>
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
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
RE_IP4 = re.compile(PAT_IP4+'$')
def inet_ntop(s):
"""
Convert ip6 address to standard hex notation.
Examples:
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0xFFFF,0x0102,0x0304))
'::FFFF:1.2.3.4'
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0,0,0,0x0102,0x0304))
'1234:5678::102:304'
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0x1234,0x5678,0,0x0102,0x0304))
'::1234:5678:0:102:304'
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0x0102,0x0304,0,0,0))
'1234:5678:0:102:304::'
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0,0,0))
'::'
"""
# convert to 8 words
a = struct.unpack("!HHHHHHHH",s)
n = (0,0,0,0,0,0,0,0) # null ip6
if a == n: return '::'
# check for ip4 mapped
if a[:5] == (0,0,0,0,0) and a[5] in (0,0xFFFF):
ip4 = '.'.join([str(i) for i in struct.unpack("!BBBB",s[12:])])
if a[5]:
return "::FFFF:" + ip4
return "::" + ip4
# find index of longest sequence of 0
for l in (7,6,5,4,3,2,1):
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:]
return "%x:%x:%x:%x:%x:%x:%x:%x" % a
def inet_pton(p):
"""
Convert ip6 standard hex notation to ip6 address.
Examples:
>>> struct.unpack('!HHHHHHHH',inet_pton('::'))
(0, 0, 0, 0, 0, 0, 0, 0)
>>> struct.unpack('!HHHHHHHH',inet_pton('::1234'))
(0, 0, 0, 0, 0, 0, 0, 4660)
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::'))
(4660, 0, 0, 0, 0, 0, 0, 0)
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::5678'))
(4660, 0, 0, 0, 0, 0, 0, 22136)
>>> struct.unpack('!HHHHHHHH',inet_pton('::FFFF:1.2.3.4'))
(0, 0, 0, 0, 0, 65535, 258, 772)
>>> struct.unpack('!HHHHHHHH',inet_pton('1.2.3.4'))
(0, 0, 0, 0, 0, 65535, 258, 772)
>>> try: inet_pton('::1.2.3.4.5')
... except ValueError as x: print(x)
::1.2.3.4.5
"""
if p == '::':
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)
a = s.split('::')
if len(a) == 2:
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',
*[int(s,16) for s in l] + [0]*(8-len(l)))
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',
*[int(s,16) for s in a[0].split(':')])
except ValueError: pass
raise ValueError(p)
+200
View File
@@ -0,0 +1,200 @@
## @package Milter.test
# A test framework for milters
from __future__ import print_function
import mime
try:
from io import BytesIO
except:
from StringIO import StringIO as BytesIO
import Milter
Milter.NOREPLY = Milter.CONTINUE
## Test mixin for unit testing %milter applications.
# This mixin overrides many Milter.MilterBase methods
# with stub versions that simply record what was done.
# @since 0.9.8
class TestBase(object):
def __init__(self,logfile='test/milter.log'):
self._protocol = 0
self.logfp = open(logfile,"a")
## The MAIL FROM for the current email being fed to the %milter
self._sender = None
## List of recipients deleted
self._delrcpt = []
## List of recipients added
self._addrcpt = []
## Macros defined
self._macros = { }
## The message body.
self._body = None
## True if the %milter replaced the message body.
self._bodyreplaced = False
## True if the %milter changed any headers.
self._headerschanged = False
## True if the %milter changed the envelope from.
self._envfromchanged = False
## Reply codes and messages set by the %milter
self._reply = None
## The rfc822 message object for the current email being fed to the %milter.
self._msg = None
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):
# FIXME: track stage, and use _symlist
return self._macros.get(name,'')
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self._bodyreplaced = True
else:
raise IOError("replacebody not called from eom()")
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 & SETSYMLIST: raise DisabledAction("SETSYMLIST")
# not used yet, but just for grins we save the data
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(' ')
except: pass
a += m
self._symlist[stage] = set(a)
## Feed a file like object to the %milter. Calls envfrom, envrcpt for
# each recipient, header for each header field, body for each body
# block, and finally eom. A return code from the %milter other than
# CONTINUE returns immediately with that return code.
#
# This is a convenience method, a test could invoke the callbacks
# in sequence on its own - and for some complex tests, this may
# be necessary.
# @param fp the file with rfc2822 message stream
# @param sender the MAIL FROM
# @param rcpt RCPT TO - additional recipients may follow
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
self._body = None
self._bodyreplaced = False
self._headerschanged = False
self._reply = None
self._sender = '<%s>'%sender
msg = mime.message_from_file(fp)
rc = self.envfrom(self._sender)
if rc != Milter.CONTINUE: return rc
for rcpt in (rcpt,) + rcpts:
rc = self.envrcpt('<%s>'%rcpt)
if rc != Milter.CONTINUE: return rc
for h,val in msg.items():
rc = self.header(h,val)
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
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()
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)
## Call the connect and helo callbacks.
# The helo callback is not called if connect does not return CONTINUE.
# @param host the hostname passed to the connect callback
# @param helo the hostname passed to the helo callback
# @param ip the IP address passed to the connect callback
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
self._body = None
self._bodyreplaced = False
opts = [ Milter.CURR_ACTS,~0,0,0 ]
rc = self.negotiate(opts)
rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE:
self.close()
return rc
rc = self.hello(helo)
if rc != Milter.CONTINUE:
self.close()
return rc
+131 -27
View File
@@ -1,23 +1,64 @@
## @package Milter.utils
# Miscellaneous functions.
#
import re
import struct
import socket
import email.Errors
import email.errors
from email.header import decode_header
import email.base64mime
from fnmatch import fnmatchcase
from email.Header import decode_header
#import email.Utils
import rfc822
from binascii import a2b_base64
ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$')
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$'
'|::(?:%(hex4)s:){5}%(ls32)s$'
'|(?:%(hex4)s)?::(?:%(hex4)s:){4}%(ls32)s$'
'|(?:(?:%(hex4)s:){0,1}%(hex4)s)?::(?:%(hex4)s:){3}%(ls32)s$'
'|(?:(?:%(hex4)s:){0,2}%(hex4)s)?::(?:%(hex4)s:){2}%(ls32)s$'
'|(?:(?:%(hex4)s:){0,3}%(hex4)s)?::%(hex4)s:%(ls32)s$'
'|(?:(?:%(hex4)s:){0,4}%(hex4)s)?::%(ls32)s$'
'|(?:(?:%(hex4)s:){0,5}%(hex4)s)?::%(hex4)s$'
'|(?:(?:%(hex4)s:){0,6}%(hex4)s)?::$'
% {
'ls32': r'(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|%s)'%PAT_IP4,
'hex4': r'[0-9a-f]{1,4}'
}, re.IGNORECASE)
# from spf.py
def addr2bin(str):
"Convert a string IPv4 address into an unsigned integer."
return struct.unpack("!L", socket.inet_aton(str))[0]
def addr2bin(s):
"""Convert a string IPv4 address into an unsigned integer."""
if s.find(':') >= 0:
try:
return bin2long6(inet_pton(s))
except:
raise socket.error("Invalid IP6 address: "+s)
try:
return struct.unpack("!L", socket.inet_aton(s))[0]
except socket.error:
raise socket.error("Invalid IP4 address: "+s)
MASK = 0xFFFFFFFFL
def bin2long6(s):
"""Convert binary IP6 address into an unsigned Python long integer."""
h, l = struct.unpack("!QQ", s)
return h << 64 | l
def cidr(i,n):
return ~(MASK >> n) & MASK & i
if hasattr(socket,'has_ipv6') and socket.has_ipv6:
def inet_ntop(s):
return socket.inet_ntop(socket.AF_INET6,s)
def inet_pton(s):
return socket.inet_pton(socket.AF_INET6,s.strip())
else:
from pyip6 import inet_ntop, inet_pton
MASK = 0xFFFFFFFF
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
def cidr(i,n,mask=MASK):
return ~(mask >> n) & mask & i
def iniplist(ipaddr,iplist):
"""Return whether ip is in cidr list
@@ -27,21 +68,59 @@ 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('2607:f8b0:4004:801::',['google.com/40'])
True
>>> iniplist('4.2.2.2',['nothing.example.com'])
False
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
True
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
Traceback (most recent call last):
...
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
"""
ipnum = addr2bin(ipaddr)
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)
for pat in 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])
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
# 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.
@@ -54,12 +133,12 @@ def parseaddr(t):
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
('God@heaven', 'jeff@spec.org')
>>> parseaddr('Real Name ((comment)) <addr...@example.com>')
('Real Name', 'addr...@example.com')
('Real Name (comment)', 'addr...@example.com')
>>> parseaddr('a(WRONG)@b')
('WRONG', 'a@b')
"""
#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('<')
@@ -68,7 +147,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] == '>':
@@ -76,9 +155,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.
@@ -91,13 +180,27 @@ def parse_addr(t):
['user@bar', 'example.com']
>>> parse_addr('foo')
['foo']
>>> parse_addr('@mx.example.com:user@example.com')
['user', 'example.com']
>>> parse_addr('@user@example.com')
['@user', 'example.com']
"""
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
if t.startswith('"'):
if t.endswith('"'): return [t[1:-1]]
pos = t.find('"@')
if pos > 0: return [t[1:pos],t[pos+2:]]
return t.split('@')
if t.startswith('@'):
try: t = t.split(':',1)[1]
except IndexError: pass
return t.rsplit('@',1)
## Decode headers gratuitously encoded to hide the content.
# Spammers often encode headers to obscure the content from
# spam filters. This function decodes gratuitously encoded
# headers.
# @param val the raw header value
# @return the decoded value or the original raw value
def parse_header(val):
"""Decode headers gratuitously encoded to hide the content.
@@ -109,17 +212,18 @@ def parse_header(val):
for s,enc in h:
if enc:
try:
u.append(unicode(s,enc))
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 email.Errors.HeaderParseError: pass
except ValueError: pass
except email.errors.HeaderParseError: pass
return val
+6 -32
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:
@@ -69,8 +68,7 @@ Not-so-quick Installation
First install Sendmail. Make sure you read libmilter/README in the Sendmail
source directory, and make sure you enable libmilter before you build. The
8.11 series had libmilter marked as FFR (For Future Release); 8.12
officially
supports libmilter, but it's still not built by default.
officially supports libmilter, but it's still not built by default.
Install Python, and enable threading in Modules/Setup.
@@ -111,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
@@ -195,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
+2 -251
View File
@@ -1,251 +1,2 @@
The recent feature to let a REJECT policy for SPF None be overridden
by whitelisting is working for CSI and CMS. However, there could be
a sender that we want to REJECT even when whitelisted - because they
normally get a guessed PASS. Need another policy name - or else just
add them to local SPF so they won't ever get 'None'.
When policy is OK, do not use cbv_cache for blacklist.
Add postmaster option or general rcpt list to dsn. Can send dsn to
user and postmaster on the same connection.
Check ESMTP NOTIFY before sending real DSNs. Just use CBV if DSNs are
not wanted.
Support CBV to local domains and cache results so that invalid users
can be rejected without maintaining valid user lists.
Now that we blacklist IPs for too many bad rcpts, delay SPF until RCPT TO.
When content filtering is not installed, reject BLACKLISTed MFROM
immediately. There is no use waiting until EOM.
Configuration is problematic when handling incoming, but not outgoing mail.
The problem comes when alice@example.com sends mail to bill@example.com,
and we are the MX for example.com, but alice is sending from some other
MTA. The mail is flagged external, so we don't list example.com in
internal_domains (or we would get "spam from self"). But, if we try to do a
CBV, we get "fraudulent MX", because the MX is ourself! So we need to
avoid doing CBV on such domains. Currently, we try to make sure the SPF
policies don't do CBV. The real solution is for users to use SMTP AUTH,
but some of them are stubborn.
We now don't check internal domains for incoming mail if there is an
SPF record.
On the other hand, if alice is sending internally, or with SMTP AUTH, she
*does* need the domain to be in internal_domains. The solution to that
is to use the new SMTP AUTH access configuration to specify which domains
can be used by smtp AUTH (by user if desired).
It would be cleaner if CBV would know which domains we have agreed to
be MX for. Some ideas for external connections:
a) check access file for To:example.com RELAY
b) check mailertable
c) check mx_domains config list
d) if there is an SPF record, don't check internal_domains
(let SPF block unauthorized machines)
But that still doesn't handle the roaming user, who won't use SMTP
AUTH, but sends through some hotel MTA. Maybe we don't want to support
him?
When setting up pydspam, both sender and rcpt must resolve to dspam users
for falsepositive recognition. Usually, this means adding
honeypot@mail.example.com to alias list for honeypot in pymilter.cfg.
This needs to be documented. I was caught by it setting up a new site.
Add signature (x-sig=AB7485f=TS) to Received-SPF, so it can be used
to blacklist sources of delayed DSNs.
rcpt-addr may let us know when a recipient is unknown. That should count
against reputation.
Need to use wildcards in blacklist.log: *.madcowsrecord.net
Need to exclude emails like !*-admin@example.com in whitelist_sender.
Need to exclude robot users from autowhitelist. Don't want to have to
list all users, so implement something like !*-admin@bmsi.com,@bmsi.com.
GOSSiP feedback from user training is ignored because UMIS has already been
removed from queue. Maybe keep UMIS in queue, and add method to
alter last feedback for ID.
Generate DSNs according to RFC 3464
Get temperror policy from access file.
Reporting explanation for failure should show source if sender
provided explanation.
Bug in Auto-whitelist. Recent Auto-whitelist doesn't override expired entry.
SPF permerror diagnostics should include corrected mechanism.
Delay SPF check until RCPT TO. Cache result to avoid repeating
for multiple RCPT. This avoids overhead for invalid RCPT, and
allows for per RCPT local policy.
Check SPF for outgoing mail (including local policy for internal addresses).
This could also solve the second part of the mail from relay problem below.
Whitelisted senders from trusted relay get PROBATION. Need to extracted
SPF result from headers - and in the case of mail internal to relay
(e.g. bmsi.com), supply 'pass' result.
Add auto-blacklisted senders to blacklist.log with timestamp.
Add emails blacklisted via CBV so that they are remembered across milter
restarts.
Make all dictionaries work like honeypot. Do not train as ham unless
whitelisted. Train on blacklisted messages, or spam feedback. This
can be called Train On Error. Should be possible to startup
with training on everything to get dictionary built fast, then switch
to train on error to minimize labor.
Allow unsigned DSNs from selected domains (that don't accept signed MFROM,
e.g. verizon.net).
Allow verified hostnames for trusted_relay. E.g. HELO name that
passes SPF.
When do we get two hello calls? STARTTLS is one reason.
Option: accept mail from auto-whitelisted senders even with spf-fail,
but do not update dspam. This can be done for individual senders or domains
using the access file.
pysrs: SRS doesn't get applied to proper recipients when there are
multiple recipients. This requires debugging cf scripts - yuk.
auto_whitelist false_positives from quarantine - perhaps only when
user selects special button (use special header to communicate
that from dspamcgi.py to milter.)
Use send_dsn.log for blacklist also. AddrCache needs localpart
wildcard (e.g. empty localpart).
Quarantined mail is missing headers modified/added by milter after
checking dspam.
Send DSN for permerror before processing extended result. An additional
DSN may be sent based on extended result. Send permerror DSN to
postmaster@sending_domain.
Rescind whitelist for banned extensions, in case sender is infected.
Train honeypot on error only.
Find rfc2822 policy for MFROM quoting.
Support explicit errors for SPF policy in access file:
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
Defer TEMPERROR in SPF evaluation - give precedence to security
(only defer for PASS mechanisms).
Create null config that does nothing - except maybe add Received-SPF
headers. Many admins would like to turn features on one at a time.
Can't output messages with malformed rfc822 attachments.
Move milter,Milter,mime,spf modules to pymilter
milter package will have bms.py application
Web admin interface
message log for automated stats and blacklisting
Skip dspam when SPF pass? NO
Report 551 with rcpt on SPF fail?
check spam keywords with character classes, e.g.
{a}=[a@ãä], {i}=[i1í], {e}=[eë], {o}=[o0ö]
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
forwarder accounts, and a util provides a special local alias for the
user to give to the forwarder. (Or user just adds arbitrary alias
unique to that forwarder to a database.) Alias only works for mail from that
forwarder. Milter gets forwarder domain from alias and uses it to
SPF check forwarder.
Framework for modular Python milter components within a single VM.
Python milters can be already be composed through sendmail by running each in
a separate process. However, a significant amount of memory is wasted
for each additional Python VM, and communication between milters
is cumbersome (e.g., adding mail headers, writing external files).
Copy incoming wiretap mail, even though sendmail alias works perfectly
for the purpose, to avoid having to change two configs for a wiretap.
Provide a way to reload milter.cfg without stopping/restarting milter.
Allow selected Windows extensions for specific domains via milter.cfg
Fix setup.py so that _FFR_QUARANTINE is automatically defined when
available in libmilter.
Keep separate ismodified flag for headers and body. This is important
when rejecting outgoing mail with viruses removed (so as not to
embarrass yourself), and also removing Received headers with hidepath.
Need a test module to feed sample messages to a milter though a live
sendmail and SMTP. The mockup currently used is probably not very accurate,
and doesn't test the threading code.
DONE Table of sendmail macros for documentation. In API docs on milter.org.
DONE For selected domains, check rcpts via CBV before accepting mail. Cache
results. This will kick out dictonary attacks against a mail domain
behind a gateway sooner.
DONE Convert DSN to REJECT unless sender gets SPF pass or best guess pass. Make
configurable by SPF result with NOTSPAM policy (reject or deliver without DSN).
Maybe policy should be NODSN - still verify sender with CBV.
DONE Add parseaddr test case for 'foo@bar.com <baz@barf.biz>'
DONE Require signed MFROM for all incoming bounces when signing all outgoing
mail - except from trusted relays.
DONE Added Message-ID header to DSN with SRS signed sender. When seen on
incoming rfc ignorant failure message, blacklist sender.
DONE Option to add Received-SPF header, but never reject on SPF.
I think the above will handle this.
DONE Received-SPF header field should show identity that was checked.
DONE When training with spam, REJECT after data so that mistakenly blacklisted
senders at least get an error.
DONE Milter won't start when it can't change permissions on *.lock to match
*.log. Should maybe ignore that error - the effect will be to set
the permissions to default.
DONE Milter won't start when a whitelist/blacklist file is missing.
DONE Delayed failure detection should parse From header to find email address.
DONE When bms.py can't find templates, it passes None to dsn.create_msg(),
which uses local variable as backup, which no longer exist. Do plain
CBV in that case instead.
DONE Find and use X-GOSSiP: header for SPAM: and FP: submissions. Would need
to keep tags longer.
DONE Parse incoming 3464 DSNs for "Action: failed" to recognize delayed
failures. This works regardless of Subject.
DONE Reports PROBATION even when rejecting message (works, but confusing in
log).
DONE Delayed_failure detection needs to handle multi-line header fields.
Also, delayed_failure should be recognized when addressed to
postmaster@helodomain
DONE DSN for Permerror shows 'None' for error under some condition.
DONE Allow blacklisted emails as well as domains in blacklist.log. Use same
data structure as autowhitelist.log.
DONE Backup copies for outgoing/incoming mail.
DONE Don't match dynamic ptr in bestguess.
Lookup exact RFC syntax of real name / email and make
Milter.utils.parse_addr() pass all unit tests.
-15
View File
@@ -1,15 +0,0 @@
#!/usr/bin/python2.4
import socket
import sys
from glob import glob
banned_ips = set(socket.inet_aton(ip)
for fn in sys.argv[1:]
for ip in open(fn))
banned_ips = list(banned_ips)
banned_ips.sort()
for ip in banned_ips:
a = socket.inet_ntoa(ip).split('.')
a.reverse()
print "%s\tIN A 127.0.0.2"%('.'.join(a))
-2018
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

-222
View File
@@ -1,222 +0,0 @@
Title: Recent Changes
<h2> Recent Changes </h2>
<h3> 0.8.10 </h3>
SRS rejections now log the recipient.
I have finally implemented plain CBV (no DSN). The CBV policy
will do a plain CBV from now on, and the DSN policy is required
if you want to send a DSN.
I started checking the MAIL FROM fullname (human readable part
of an email) for porn keywords. There is now a banned IP database.
IPs are banned for too many bad MAIL FROMs or RCPT TOs, and remain banned
for 7 days.
<h3> 0.8.9 </h3>
I use the <code>%ifarch</code> hack to build milter and milter-spf
packages as noarch, while pymilter is built as native.
I removed the spf dependency from dsn.py, so pymilter can be used without
installing pyspf, and added a Milter.dns module to let python milters do
general DNS lookups without loading pyspf.
<h3> 0.8.8 </h3>
Programs do not belong in the /var/log directory. I moved the
milter apps to /usr/lib/pymilter. Since having the programs and
data in the same directory is convenient for debugging, it will
still use an executable present in the datadir.
Several general utility classes and functions are now in the Milter package
for possible use by other python milters. In addition to the trivial example
milter, a simple SPF only milter is included as a realistic example.
The spec file now build 3 RPMs:
<ul>
<li> pymilter is the milter module and Milter package for use by all python
milters.
<li> milter is the all-singing, all-dancing python milter application, with
supporting <code>/etc/init.d</code>, logrotate and other scripts.
<li> milter-spf is the simple SPF only milter application.
</ul>
<h3> 0.8.7 </h3>
The spf module has been moved to the
<a href="http://cheeseshop.python.org/pypi/pyspf">pyspf</a> package.
Download <a href="http://sourceforge.net/project/showfiles.php?group_id=139894&package_id=191419">here</a>.
<h3> 0.8.6 </h3>
Python milter has been moved to
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
project</a> for development and release downloads.
<h3> 0.8.5 </h3>
Release 0.8.5 fixes some build bugs reported by Stephen Figgins. It
fixes many small things, like not auto-whitelisting recipients of
outgoing mail when the subject contains "autoreply:". There is a
simple trusted forwarder implementation. If you have more than
2 or so forwarders, we will need a way to "compile" SPF records into an
IP set and TTL for it to be efficient (like libspf2 does).
<h3> GOSSiP </h3>
An alpha release of <a href="pygossip.html">pygossip</a> has been commited to
CVS, module pygossip. A version of the bms.py milter has been commited to CVS
which supports calling GOSSiP to track domain reputation in a local database.
<h3> New website design </h3>
Hey, I'm no artist, so I just used the
<a href="http://ht2html.sourceforge.net/">ht2html</a> package
by <a href="http://barry.wooz.org/">Barry Warsaw</a>. The mascot
is by <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>,
or maybe his wife. I chose Maxwell's daemon because it tirelessly
and invisibly sorts molecules, just as milters sort mail.
Christian has also provided a fun
<a href="http://alphard.ethz.ch/hafner/PPS/PPS2002/Maxwell/simulation.htm">
simulation</a> that lets you try your hand at sorting molecules.
<h3> 0.8.4 </h3>
Release 0.8.4 makes configuring SPF policy via access.db actually work.
The honeypot idea is enhanced by auto-whitelisting recipients of
email sent from selected domains. Whitelisted messages are then used
to train the honeypot. This makes the honeypot screener entirely self
training. The smfi_progress() API is now automatically supported when present.
An optional idx parameter to milter.addheader() invokes smfi_insheader().
<h3> 0.8.3 </h3>
Release 0.8.3 uses the standard logging module, and supports configuring
more detailed SPF policy via the sendmail access map. SMTP AUTH connections
are considered INTERNAL. Preventing forgery between internal domains is
just a matter of specifying the user-domain map - I'll define something
for the next version. We now send DSNs when mail is quarantined (rejecting
if DSN fails) and for SPF syntax errors (PermError). There is an
experimental option to add a Sender header when it is missing and the From
domain doesn't match the MAIL FROM domain. Next release, we may start
renaming and replacing an existing Sender header when neither it nor the
From domain matches MAIL FROM. Since bogus MAIL FROMs are rejected
(to varying degrees depending on the configured SPF policy), and
both Sender and From and displayed by default in many email clients,
this provides some phishing protection without rejecting mail based
on headers.
<h3> 0.8.2 </h3>
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
in line with the newly official RFC. It adds
<a href="http://ses.codeshare.ca/">SES</a>
support (the original SES without body hash) for pysrs-0.30.10, and honeypot
support for pydspam-1.1.9. There is a new method in the base milter module.
milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or
TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
<h3> 0.8.0 </h3>
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
release. It supports Python-2.4, and provides an option to accept mail
that gets an SPF softfail or fails the 3 strikes rule, provided the
alleged sender accepts a DSN explaining the problem. Python-2.3 is
no longer supported by the reworked mime.py module, although API changes
could be backported. There are too many incompatible changes to the
python email package.
<h3> Older Releases </h3>
Release 0.7.2 tightens the authentication screws with a "3 strikes and
you're out" policy. A sender must have a valid PTR, HELO, or SPF record
to send email. Specific senders can be whitelisted using the
"delegate" option in the spf configuration section by adding a
default SPF record for them. The PTR and HELO are required
by RFC anyway, so this is not an unreasonable requirement.
There is now a coherent policy for an SPF softfail result. A softfail
is accepted if there is a valid PTR or HELO, or if the domain
is listed in the "accept_softfail" option of the spf configuration section.
A neutral result is accepted by default if there is a valid PTR or
HELO, (and the SPF record was not guessed), unless the domain is listed in the
"reject_neutral" option. Common forms of PTR records for dynamic IPs are
recognized, and do not count as a valid PTR. This does not prevent anyone
from sending mail from a dynamic IP - they just need to configure a
valid HELO name or publish an SPF record.
<p>
As SPF adoption continues to rise, forged spam is not getting through. So
spammers are publishing their SPF records as predicted. The 0.7.2 RPM
now provides the <code>rhsbl</code> sendmail hack so that spammer domains
can be blacklisted. With the RPM installed, add a line like the following
to your <code>sendmail.mc</code>.
<pre>
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
</pre>
<p>
Of course, spammers are now starting to register
throwaway domains. The next thing we need is a custom DNS server,
in Python, that
can recognize patterns. For instance, one spammer registers ded304.com,
ded305.com, ded306.com, etc. We also need the custom DNS server to
let SPF classic clients check SES (which will be part of pysrs).
The <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
framework provides a custom DNS server - but I
would like a smaller implementation for our use.
<p>
The RPM for release 0.7.0 moves the config file and socket locations to
/etc/mail and /var/run/milter respectively. We now parse Microsoft CID records
- but only hotmail.com uses them. They seem to have applied for a patent on
the brilliant idea of examining the mail headers to see who the message is
from. We aren't doing that here, so not to worry - but I am not a lawyer, so
if you are worried, change spf.py around line 626 to return None instead of
calling CIDParser(). There is a new option to reject mail with no PTR
and no SPF.
<p>
Microsoft is pushing an anti-opensource license for their pending patent
along with their sender-ID proposal before the IETF.
It is royalty free - but requires anyone distributing a binary they've
compiled from source to sign a license agreement. The Apache Software
Foundation <a
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
the problem with sender-ID</a>, and Debian <a
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
software in general</a> and the <a
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
particular</a>, Python milter will not be able to implement sender-ID in its
current form. This was, no doubt, Microsoft's intent all along.
<p>
Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I
recommend ignoring it and continuing to implement and improve SPF until a
working and unencumbered proposal for RFC2822 headers surfaces.
<p>
<a href="http://openspf.com">
<img src="SPF.gif" align=left alt="SPF logo"></a>
Release 0.6.6 adds support for <a href="http://openspf.com/">SPF</a>,
a protocol to prevent forging of the envelope from address.
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
The included spf.py module is an updated version of the original 1.6
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
The updated version tracks the draft RFC and test suite.
<p>
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
dspam bayes filter project</a> and <a href="dspam.html">
packaged it for python</a>.
Release 0.6.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers.
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
-55
View File
@@ -1,55 +0,0 @@
Title: Credits
<h1> CREDITS </h1>
<a href="mailto:Jim Niemira <urmane@urmane.org>">Jim Niemira</a>
wrote the original C module and some quick
and dirty python to use it.
<a href="http://gathman.org/vitae">Stuart D. Gathman</a>
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
real, usable Python extension.
<h2>Other contributors (in random order):</h2>
<dl>
<dt> <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>
<dd>for the pymilter mascot image of
<a href="http://maxwelld.netfirms.com/">
Maxwell's daemon</a>
<dt>Stephen Figgins
<dd>for reporting problems building with sendmail-8.12, and when
building milter.so for the first time.
<dt>Dave MacQuigg
<dd>for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter.
<dt>Terence Way
<dd>for providing a Python port of SPF
<dt>Scott Kitterman
<dd>for doing lots of testing and debugging of SPF against draft standard,
and for putting up a <a href="http://www.kitterman.com/spf/validate.html">
web page that validates SPF</a> records using spf.py
<dt>Alexander Kourakos
<dd>for plugging several memory leaks
<dt>George Graf at Vienna University of Economics and Business Administration
<dd>for handling None passed to setreply and chgheader.
<dt>Deron Meranda
<dd>for IPv6 patches
<dt>Jason Erikson
<dd>for handling NULL hostaddr in connect callback.
<dt>John Draper
<dd>for porting Python milter to OpenBSD, and starting to work on tutorials
then pointing out that it would be easier to just write the MTA in Python.
<dt>Eric S. Johansson
<dd>for helpful design discussions while working on camram
<dt>Alex Savguira
<dd>for finding bugs with international headers and
suggesting the scan_zip option.
<dt><a href="http://www.bmsi.com">Business Management Systems</a>
<dd>for hosting the website, and providing paying clients who need milter
service so I can work on it as part of my day job.
</dl>
If I have left anybody out, send me a reminder:
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">stuart@bmsi.com</a>
-293
View File
@@ -1,293 +0,0 @@
Title: Python Milter FAQ
<h1> Python Milter <a name=faq>FAQ</a> </h1>
<menu>
<li> <a href="#compiling">Compiling Python Milter</a>
<li> <a href="#running">Running Python Milter</a>
<li> <a href="#spf">Using SPF</a>
<li> <a href="#srs">Using SRS</a>
</menu>
<ol>
<h3> <a name="compiling">Compiling Python Milter </a> </h3>
<li> Q. I have tried to download the current milter code and my virus scan
traps several viruses in the download.
<p> A. The milter source includes a number of deactivated viruses in
the test directory. All but the first and last lines of the base64
encoded virus data has been removed. I suppose I should randomize
the first and last lines as well, since pymilter just deletes executables,
and doesn't look for signatures.
<li> Q. I have installed sendmail from source, but Python milter won't
compile.
<p> A. Even though libmilter is officially supported in sendmail-8.12,
you need to build and install it in separate steps. Take a look
at the <a href="/aix/sendmail12.spec">RPM spec file</a> for sendmail-8.12.
The %prep section shows you how to create
a site.config.m4 that enables MILTER. The %build section shows you how
to build libmilter in a separate invocation of make. The %install section
shows you how to install libmilter with a separate invocation of make.
<p>
<li> Q. Why is mfapi.h not found when I try to compile Python milter on
RedHat 7.2?
<p> A. RedHat forgot to include the header in the RPM. See the
<a href="requirements.html#rh72">RedHat 7.2 requirements</a>.
<p>
<li> Q. Python milter compiles ok, but I get an error like this when
I try to import the milter module:
<pre>
ImportError: /usr/lib/python2.4/site-packages/milter.so: undefined symbol: smfi_setmlreply
</pre>
<p> A. Your libmilter.a is from sendmail-8.12 or earlier. You need
sendmail-8.13 or later to support setmlreply. You can disable
setmlreply by changing setup.py. Change:
<pre>
define_macros = [ ('MAX_ML_REPLY',32) ]
</pre>
in setup.py to
<pre>
define_macros = [ ('MAX_ML_REPLY',1) ]
</pre>
<h3> <a name="running">Running Python Milter </a></h3>
<li> Q. The sample.py milter prints a message, then just sits there.
<pre>
To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=pythonfilter
Xpythonfilter, S=local:inet:1030@localhost
See the sendmail README for libmilter.
sample milter startup
</pre>
<p> A. You need to tell sendmail to connect to your milter. The
sample milter tells you what to add to your sendmail.cf to tell
sendmail to use the milter. You can also add an INPUT_MAIL_FILTER
macro to your sendmail.mc file and rebuild sendmail.cf - see the sendmail
README for milters.
<p>
<li> Q. I've configured sendmail properly, but still nothing happens
when I send myself mail!
<p> A. Sendmail only milters SMTP mail. Local mail is not miltered.
You can pipe a raw message through sendmail to test your milter:
<pre>
$ cat rawtextmsg | sendmail myname@my.full.domain
</pre>
Now check your milter log.
<p>
<li> Q. Why do I get this ImportError exception?
<pre>
File "mime.py", line 370, in ?
from sgmllib import declstringlit, declname
ImportError: cannot import name declstringlit
</pre>
<p> A. <code>declstringlit</code> is not provided by sgmllib in all versions
of python. For instance, python-2.2 does not have it. Upgrade to
milter-0.4.5 or later to remove this dependency.
<p>
<li> Q. Why do I get <code>milter.error: cannot add recipient</code>?
<pre>
</pre>
<p> A. You must tell libmilter how you might mutate the message with
<code>set_flags()</code> before calling <code>runmilter()</code>. For
instance, <code>Milter.set_flags(Milter.ADDRCPT)</code>. You must add together
all of <code>ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS</code> that apply.
<p> NOTE - recent versions default flags to enabling all features. You
must now call <code>set_flags()</code> if you wish to disable features for
efficiency.
<p>
<li> Q. Why does sendmail sometimes print something like:
"...write(D) returned -1, expected 5: Broken pipe"
in the sendmail log?
<p> A. Libmilter expects "rcpt to" shortly after getting "mail from".
"Shortly" is defined by the timeout parameter you passed to
<code>Milter.runmilter()
</code> or <code>milter.settimeout()</code>. If the timeout is 10 seconds,
and looking up the first recipient in DNS takes more than
10 seconds, libmilter will give up and break the connection.
<code>Milter.runmilter()</code> defaulted to 10 seconds in 0.3.4. In 0.3.5
it will keep the libmilter default of 2 hours.
<p>
<li> Q. Why does milter block messages with big5 encoding? What if I
want to receive them?
<p> A. sample.py is a sample. It is supposed to be easily modified
for your specific needs. We will of course continue to move generic
code out of the sample as the project evolves. Think of sample.py as
an active config file.
<p>
If you are running bms.py, then the block_chinese option in
<code>/etc/mail/pymilter.cfg</code> controls this feature.
<p>
<li> Q. Why does sendmail coredump with milters on OpenBSD?
<p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD.
OpenBSD users report that this problem has been fixed, so upgrading
OpenBSD will fix this. Otherwise, you can
use an internet domain socket instead. For example, in
<code>sendmail.cf</code> use
<pre>
Xpythonfilter, S=inet:1234@localhost
</pre>
and change sample.py accordingly.
<p>
<li> Q. How can I change the bounce message for an invalid recipient?
I can only change the recipient in the eom callback, but the eom callback
is never called when the recipient is invalid!
<p> A. Configure sendmail to use virtusertable, and send all unknown
addresses to /dev/null. For example,
<h4>/etc/mail/virtusertable</h4>
<pre>
@mycorp.com dev-null
dan@mycorp.com dan
sally@mycorp.com sally
</pre>
<h4>/etc/aliases</h4>
<pre>
dev-null: /dev/null
</pre>
Now your milter will get to the eom callback, and can change the
envelope recipient at will. Thanks to Dredd at
<a href=http://www.milter.org/>milter.org</a> for this solution.
<p>
<li> Q. I am having trouble with the setreply method. It always outputs
"milter.error: cannot set reply".
<p> A. Check the sendmail log for errors. If sendmail is getting
milter timeouts, then your milter is taking too long and sendmail gave
up waiting. You can adjust the timeouts in your sendmail config. Here
is a milter declaration for sendmail.cf with all timeouts specified:
<pre>
Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
</pre>
<li> Q. There is a Python traceback in the log file! What happened to
my email?
<p> A. By default, when the milter fails with an untrapped exception, a
TEMPFAIL result (451) is returned to the sender. The sender will then retry
every hour or so for several days. Hopefully, someone will notice the
traceback, and workaround or fix the problem. Beginning with milter-0.8.2,
you can call <code>milter.set_exception_policy(milter.CONTINUE)</code>
to cause an untrapped exception to continue processing with the
next callback or milter instead. For
completeness, you can also set the exception policy to
<code>milter.REJECT</code>.
<li> Q. I read some notes such as "Check valid domains allowed by internal
senders to detect PCs infected with spam trojans." but could not
understand the idea. Could you clarify the content ?
<p> A. The <code>internal_domains</code> configuration specifies which
MAIL FROM domains are used by internal connections. If an internal
PC tries to use some other domain, it is assumed to be a "Zombie".
<p>
Here is a sample log line:
<pre>
2005Jun22 12:01:04 [12430] REJECT: zombie PC at 192.168.100.171 sending MAIL FROM debby@fedex.com
</pre>
No, fedex.com does not use pymilter, and there is no one named debby at my
client. But the idiot using the PC at 192.168.100.171 has downloaded and
installed some stupid weatherbar/hotbar/aquariumscreensaver that is actually a
spam bot.
<p>
The <code>internal_domains</code> option is simplistic, it assumes all
valid senders of the domains are internal. SPF provides a much more general
check of IP and MAIL FROM for external email. Pymilter should soon
have a local policy feature for more general checking of internal mail.
<li> Q. <code>mail_archive</code> isn't working. Or I don't understand how
it's suppose to work. I have
<code>mail_archive = /var/mail/mail_archive</code>
in <code>pymilter.cfg</code> but nothing ever gets dumped into
<code>/var/mail/mail_archive</code>.
<p> A. The 'mail' user needs to have write access. Permission failures
should be logged as a traceback in milter.log if it doesn't.
<h3> <a name="spf">Using SPF </a></h3>
<li> Q. So how do I use the SPF support? The sample.py milter doesn't seem
to use it.
<p> A. The bms.py milter supports spf. The RedHat RPMs will set almost
everything up for you. For other systems:
<ol type=i>
<li> Arrange to run bms.py in the background (as a service perhaps) and
redirect output and errors to a logfile. For instance, on AIX you'll want
to use SRC (System Resource Controller).
<li> Copy pymilter.cfg to the /etc/mail or the directory you run bms.py in,
and edit it. The comments should explain the options.
<li> Start bms.py in the background as arranged.
<li> Add Xpythonfilter to sendmail.cf or add an INPUT_MAIL_FILTER to
sendmail.mc. Regen sendmail.cf if you use sendmail.mc and restart
sendmail.
<li> Arrange to rotate log files and remove old defang files in
<code>tempdir</code>. The RedHat RPM uses <code>logrotate</code> for
logfiles and a simple cron script using <code>find</code> to clean
<code>tempdir</code>.
</ol>
In CVS, there is <code>spfmilter.py</code>. Run that as a service,
and it does just SPF. It uses the sendmail <code>access</code>
file to configure SPF responses just like <code>bms.py</code>, but
supports only REJECT and OK.
<li> Q. The SPF DSN is sent at least once for domains that don't publish a SPF.
How do I stop this behavior?
<p> A. The SPF response is controlled by <code>/etc/mail/access</code>
(actually the file you specify with <code>access_file</code> in
the <code>[spf]</code> section of <code>pymilter.cfg</code>).
Responses are OK, CBV, and REJECT. CBV sends the DSN.
<p>
You can change the defaults. For instance, I have:
<pre>
SPF-None: REJECT
SPF-Neutral: CBV
SPF-Softfail: CBV
SPF-Permerror: CBV
</pre>
I have best_guess = 1, so SPF none is converted to PASS/NEUTRAL for policy
lookup, and 3 strikes (no PTR, no HELO, no SPF) becomes "SPF NONE" for local
policy purposes (the Received-SPF header always shows the official SPF
result.)
<p>
You can change the default for specific domains:
<pre>
# these guys aren't going to pay attention to CBVs anyway...
SPF-None:cia.gov REJECT
SPF-None:fbi.gov REJECT
SPF-Neutral:aol.com REJECT
SPF-Softfail:ebay.com REJECT
</pre>
<h3> <a name="srs">Using SRS </a></h3>
<li> Q. The SRS part doesn't seem to work as whenever I try to start
<code>/etc/init.d/pysrs</code>, I get this in
<code>/var/log/milter/pysrs.log</code>:
<pre>
ConfigParser.NoOptionError: No option 'fwdomain' in section: 'srs'
</pre>
<p> A. You need to specify the forward domain - i.e. the domain you want
SRS to rewrite stuff too.
<p>
For instance, I have:
<pre>
# sample SRS configuration
[srs]
secret = don't you wish
maxage = 8
hashlength = 5
;database=/var/log/milter/srs.db
fwdomain = bmsi.com
sign=bmsi.com,mail.bmsi.com,gathman.org
srs=bmsaix.bmsi.com,bmsred.bmsi.com,stl.gathman.org,bampa.gathman.org
</pre>
The <code>sign</code> is for local domains which are signed.
The <code>srs</code> list is for other domains which you are relaying,
and which need to have SRS checked/undone for bounces.
</ol>
-23
View File
@@ -1,23 +0,0 @@
<!-- -*- html -*- -->
<h3>Subsections</h3>
<li><a href="milter.html">Introduction</a>
<li><a href="changes.html">Changes</a>
<li><a href="requirements.html">Requirements</a>
<li><a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download</a>
<li><a href="faq.html">FAQ</a>
<li><a href="policy.html">Policies</a>
<li><a href="logmsgs.html">Log&nbsp;Messages</a>
<li><a href="http://bmsi.com/mailman/listinfo/pymilter">Mailing&nbsp;List</a>
<li><a href="credits.html">CREDITS</a>
<li><a href="http://sourceforge.net"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=139894&amp;type=1" width="88" height="31" border="0" alt="SourceForge.net Logo" /></a>
<h3>Links</h3>
<li><a href="https://www.milter.org/developers/api/index">C&nbsp;API</a>
<li><a href="http://www.milter.org/">Milter.Org</a>
<li><a href="http://www.python.org/">Python.Org</a>
<li><a href="http://www.sendmail.org/">Sendmail.Org</a>
<li><a href="http://www.openspf.org/">SPF</a>
<li><a href="pysrs.html">pysrs</a>
<li><a href="http://cheeseshop.python.org/pypi/pyspf">pyspf</a>
<li><a href="http://bmsi.com/python/pygossip.html">pygossip</a>
<li><a href="http://bmsi.com/python/dspam.html">pydspam</a>
<li><a href="http://bmsi.com/libdspam/dspam.html">libdspam</a>
-91
View File
@@ -1,91 +0,0 @@
Title: Python Milter Log Documentation
<style>
DT { font-weight: bolder; padding-top: 1em }
</style>
<h1> Milter Log Documentation </h1>
The milter log from the bms.py application has a variety of "tags" in it that
indicate what it did.
<dl>
<dt> DSPAM: honeypot SCREENED
<dd> message was quarantined to the honeypot quarantine
<dt> REJECT: hello SPF: fail 550 access denied
<dt> REJECT: hello SPF: softfail 550 domain in transition
<dt> REJECT: hello SPF: neutral 550 access neither permitted nor denied
<dd> message was rejected because there was an SPF policy for the
HELO name, and it did not pass.
<dt> CBV: sender-17-44662668-643@bluepenmagic.com
<dd> we performed a call back verification
<dt> dspam
<dd> dspam identifier was added to the message
<dt> REJECT: spam from self: jsconnor.com
<dd> message was reject because HELO was us (jsconnor.com)
<dt> INNOC: richh
<dd> message was used to update richh's dspam dictionary
<dt> HONEYPOT: pooh@bwicorp.com
<dd> message was sent to a honeypot address (pooh@bwicorp.com), the
message was added to the honeypot dspam dictionary as spam
<dt> REJECT: numeric hello name: 63.217.19.146
<dd> message was rejected because helo name was invalid (numeric)
<dt> eom
<dd> message was successfully received
<dt> TEMPFAIL: CBV: 450 No MX servers available
<dd> we tried to do a call back verification but could not look up
MX record, we told the sender to try again later
<dt> CBV: info@emailpizzahut.com (cached)
<dd> call back verification was needed, we had already done it recently
<dt> abort after 0 body chars
<dd> sender hung up on us
<dt> REJECT: SPF fail 550 SPF fail: see
http://openspf.com/why.html?sender=m.hendersonxk@163.net&ip=213.47.161.100
<dd> message was reject because its sender's spf policy said to
<dt> REJECT: Subject: Cialis - No prescription needed!
<dd> message was rejected because its subject contained a bad expression
<dt> REJECT: zombie PC at 192.168.3.37 sending MAIL FROM seajdr@amritind.com
<dd> message was rejected because the connect ip was internal, but the
sender was not. This is usually because a Windows PC is infected with
malware.
<dt> X-Guessed-SPF: pass
<dd> When the SPF result is NONE, we guess a result based on the generic
SPF policy "v=spf1 a/24 mx/24 ptr".
<dt> DSPAM: tonyc tonyc@example.com
<dd> message was sent to tonyc@example.com and it was identified as spam
and placed in the tonyc dspam quarantine
<dt> REJECT: CBV: 550 calvinalstonis@ix.netcom.com...User unknown
<dt> REJECT: CBV: 553 sorry, that domain isn't in my list
<dt> REJECT: CBV: 554 delivery error: dd This user doesn't have an account
<dd> message was rejected because call back verification gave us a fatal
error
<dt> Auto-Whitelist: user@example.com
<dd> recipient has been added to auto_whitelist.log because the message
was sent from an internal IP and the recipient is not internal.
<dt> WHITELIST user@example.com
<dd> message is whitelisted because sender appears in auto_whitelist.log
<dt> BLACKLIST user@example.com
<dd> message is blacklisted because sender appears in blacklist.log or
failed a CBV test.
<dt> TRAINSPAM: honeypot X-Dspam-Score: 0.002278
<dd> message was used to train screener dictionary as spam
<dt> TRAIN: honeypot X-Dspam-Score: 0.980203
<dd> message was used to train screener dictionary as ham
</dl>
<br>
+88
View File
@@ -0,0 +1,88 @@
## @mainpage Writing Milters in Python
#
# At the lowest level, the <code>milter</code> module provides a thin wrapper
# around the <a href="milter_api/index.html"> sendmail
# 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.
#
# At the next level, the <code>Milter</code> module (note the case difference)
# provides a Python friendly object oriented wrapper for the low level API. To
# use the Milter module, an application registers a 'factory' to create an
# object for each connection from a MTA to sendmail. These connection objects
# must provide methods corresponding to the libmilter event callbacks.
#
# 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. The callback return codes are
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
#
# 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 callback methods are overridden.
#
# The Milter.Milter class provides an alternate default
# implementation that logs the main milter callbacks, but otherwise does
# nothing. It is provided for compatibility.
#
# The 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="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="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
# confused. Threads may still be useful, but you may need to provide an
# alternate means of causing graceful shutdown.
#
# You may find the
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
# multiprocessing</a> module useful. It can be a drop-in
# replacement for threading as illustrated in
# <a href="milter-template_8py-example.html">milter-template.py</a>.
#
# @section Useful python packages for milters
#
# <a href="https://pypi.python.org/pypi/pyspf">pyspf</a> checks the
# SMTP envelope sender (MAIL FROM, passed to the Milter.Base.envfrom callback)
# against a Sender Policy published in DNS by the sending domain. This
# can prevent forgery of the MAIL FROM. SPF is Sender Policy Framework.
#
# <a href="https://launchpad.net/dkimpy">pydkim</a> checks a DKIM signature
# of the email body and headers against a public key published in DNS by
# the signing domain. DKIM is DomainKeys Identified Mail.
#
# The <a href="https://pypi.python.org/pypi/authres/">authres</a> module
# parses and formats the Authentication-Results email header, providing
# a standard place to summarize the results from DKIM, SPF, rDNS, SMTP AUTH,
# and other email authentication methods.
#
# <a href="https://pypi.python.org/pypi/pydspam/">pydspam</a> wraps
# the libdspam API of the <a href="http://dspam.sourceforge.net/">DSPAM</a>
# project.
#
# @section Milters written with pymilter
#
# <a href="https://github.com/croessner/vrfydmn">Verify Domain</a> is a
# Postfix milter that rejects/fixes manipulated From: header
# on a mail host with multiple virtual domains.
#
# <a href="https://pypi.python.org/pypi/milter/">BMS Milter</a> has several
# milters, a big complicated spam filter that integrates multiple
# authentication protocols with pydpsm, and two simple ones: spfmilter.py and
# dkim-milter.py.
#
-264
View File
@@ -1,264 +0,0 @@
Title: Python Milters
<P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
<IMG SRC="http://bmsi.com/art/brain1.gif"
ALT="Viewable With Any Browser" BORDER="0"></A>
<img src="http://bmsi.com/art/banner_4.gif" width="468" height="60" border="0"
usemap="#banner_4" alt="Your vote?">
<map name="banner_4">
<area shape="rect" coords="330,25,426,59"
href="http://education-survey.org/" alt="I Disagree">
<area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
</map>
</P>
<table rules="none">
<tr><td>
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align="top">
Mascot by <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>
</td>
<td>
<h1 align=center>Sendmail Milters in Python</h1>
<h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a>
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
Stuart D. Gathman</a><br>
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
Last updated Aug 26, 2008</h4>
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
<a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
<a href="#overview">Overview</a> |
<a href="/python/dspam.html">pydspam</a> |
<a href="/libdspam/dspam.html">libdspam</a>
<p>
<a href="//www.python.org">
<img src="python55.gif" align=left alt="A Python"></a>
<a href="//www.sendmail.org/">Sendmail</a> introduced a
<a href="https://www.milter.org/developers/api/index"> new API</a> beginning with version 8.10 -
libmilter. The milter module for <a href="//www.python.org">Python</a>
provides a python interface to libmilter that exploits all its features.
<p>
Sendmail 8.12 officially releases libmilter.
Version 8.12 seems to be more robust, and includes new privilege
separation features to enhance security. Even better, sendmail 8.13
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more
efficient and secure. Sendmail 8.14 finally supports modifying
MAIL FROM via the milter API. Unfortunately, I haven't gotten around
to supporting that yet in python milter.
</td></tr>
</table>
<h3><a name=overview>Overview</a></h3>
This package provides a robust toolkit for Python <a
href="#milter">milters</a>, and the beginnings of a general purpose mail
filtering system written in Python.
<p>
At the lowest level, the 'milter' module provides a thin wrapper around the
<a href="https://www.milter.org/developers/api/index">
sendmail libmilter API</a>. This API lets you register callbacks for
a number of events in the
<a href="http://www.cs.concordia.ca/~group/fig/public/email/relay/milter+ruleset-checks.html">process of sendmail receiving a message via SMTP</a>.
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.
<p>
At the next level, the 'Milter' 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.
<p>
Each event method returns a code to tell sendmail whether to proceed
with processing the message. This is a big advantage of milters over
other mail filtering systems. Unwanted mail can be stopped in its
tracks at the earliest possible point.
<p>
The Milter.Milter class provides default implementations for event
methods that
do nothing, and also provides wrappers for the libmilter methods to mutate
the message.
<p>
The 'spf' module provides an implementation of <a href="http://openspf.com">
SPF</a> useful for detecting email forgery.
<p>
The 'mime' module provides a wrapper for the Python email package that
fixes some bugs, and simplifies modifying selected parts of a MIME message.
<p>
Finally, the bms.py application is both a sample of how to use the
Milter and spf modules, and the beginnings of a general purpose SPAM filtering,
wiretapping, SPF checking, and Win32 virus protecting milter. It can
make use of the <a href="pysrs.html">pysrs</a> package when available for
SRS/SES checking and the <a href="dspam.html">pydspam</a> package for Bayesian
content filtering. SPF checking
requires <a href="http://pydns.sourceforge.net/">
pydns</a>. Configuration documentation is currently included as comments
in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
See also the <a href="HOWTO">HOWTO</a> and <a href="logmsgs.html">
Milter Log Message Tags</a>.
<p>
Python milter is under GPL. The authors can probably be convinced to
change this to LGPL if needed.
<h3>What is a <a name="milter">milter</a>?</h3>
Milters can run on the same machine as sendmail, or another machine. The
milter can even run with a different operating system or processor than
sendmail.
Sendmail talks to the milter via a local or internet socket.
Sendmail keeps the
milter informed of events as it processes a mail connection. At any
point, the milter can cut the conversation short by telling sendmail
to ACCEPT, REJECT, or DISCARD the message. After receiving a complete
message from sendmail, the milter can again REJECT or DISCARD it, but it
can also ACCEPT it with changes to the headers or body.
<h3> What can you do with a milter? </h3>
<menu>
<li> A milter can DISCARD or REJECT spam based based on algorithms scripted
in python rather than sendmail's cryptic "cf" language.
<li> A milter can alter or remove attachments from mail that are poisonous to
Windows.
<li> A milter can scan for viruses and clean them when detected.
<li> A milter scans outgoing as well as incoming mail.
<li> A milter can add and delete recipients to forward or secretly
copy mail.
<li> For more ideas, check the <a href="//www.milter.org">Milter Web Page</a>.
</menu>
<a href="https://www.milter.org/developers/api/index">
Documentation</a> for the C API is provided with sendmail. Miltermodule
provides a thin python wrapper for the C API. Milter.py provides a simple
OO wrapper on top of that.
<p>
The Python milter package includes a sample milter that replaces dangerous
attachments with a warning message, discards mail addressed to
MAILER-DAEMON, and demonstrates several SPAM abatement strategies.
The MimeMessage class to do this used to be based on the
<code>mimetools</code> and <code>multifile</code> standard python packages.
As of milter version 0.6.0, it is based on the email standard
python packages, which were derived from the
<a href="http://sourceforge.net/projects/mimelib">mimelib</a> project.
The MimeMessage class patches several bugs in the email package,
and provides some backward compatibility.
<p>
The "defang" function of the sample milter was inspired by
<a href="http://www.roaringpenguin.com/mimedefang/">MIMEDefang</a>,
a Perl milter with flexible attachment processing options. The latest
version of MIMEDefang uses an apache style process pool to avoid reloading
the Perl interpreter for each message. This makes it fast enough for
production without using Perl threading.
<p>
<a href="http://sourceforge.net/projects/mailchecker">mailchecker</a> is
a Python project to provide flexible attachment processing for mail. I
will be looking at plugging mailchecker into a milter.
<p>
<a href="http://software.libertine.org/tmda/">TMDA</a> is a Python project
to require confirmation the first time someone tries to send to your
mailbox. This would be a nice feature to have in a milter.
<p>
There is also a <a href="http://www.milter.org/">Milter community website</a>
where milter software and gory details of the API are discussed.
<h3> Is a milter written in python efficient? </h3>
The python milter process is multi-threaded and startup cost is incurred
only once. This is much more efficient than some implementations that
start a new interpreter for each connection. Testing in a production
environment did not use a significant percentage of the CPU. Furthermore,
python is easily extended in C for any step requiring expensive CPU
processing.
<p>
For example, the HTML parsing feature to remove scripts from HTML attachments
is rather CPU intensive in pure python. Using the C replacement for sgmllib
greatly speeds things up.
<h3> Goals </h3>
<menu>
<li> Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
forwarder accounts (perhaps in <code>~/.forwarders</code>), and a util
provides a special local alias for the user to give to the forwarder.
Alias only works for mail from that forwarder. Milter gets forwarder
domain from alias and uses it to SPF check forwarder. Requires
milter to have read access to <code>~/.forwarders</code> or else
a way for user to submit entries to milter database.
<li> The bms.py milter has too many features. Create a framework where
numerous small feature modules can be plugged together in the
configuration.
<li> Create a pure python substitute for miltermodule and libmilter that
implements the <a
href="http://www.duh.org/cvsweb.cgi/~checkout~/pmilter/doc/milter-protocol.txt?rev=1">
libmilter protocol</a> in python.
<li> Find or write a faster implementation of sgmllib. The
<a href="http://www.effbot.org/zone/sgmlop-index.htm">sgmlop package</a>
is not very compatible with
<a href="http://www.python.org/doc/2.1.3/lib/module-sgmllib.html">
Python-2.1 sgmllib</a>, but it is a start, and is supported in
milter-0.4.5 or later.
<li> Implement all or most of the features of
<a href="http://www.roaringpenguin.com/mimedefang/">MIMEDefang</a>.
<li> Follow the official <a href="http://www.python.org/peps/pep-0008.html">
Python coding standards</a> more closely.
<li> Make unit test code more like other python modules.
</menu>
<h3> Confirmed Installations </h3>
Please <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">email</a>
me if you do <i>not</i> successfully install milter. The confirmed
installations are too numerous to list at this point.
<h2> Enough Already! </h2>
Nearly a dozen people have emailed me begging for a feature to copy
outgoing and/or incoming mail to a backup directory by user. Ok, it
looks like this is a most requested feature for 0.5.6. In the meantime,
here are some things to consider:
<ul>
<li> If you want to equivalent of a Bcc added to each message, this
is very easy to do in the python code for bms.py. See below.
<li> If you want to copy to a file in a directory (thus avoiding having to
set up aliases), this is slightly more involved. The bms.py milter already
copies the message to a temporary file for use in replacing the message body
when banned attachments are found. You have to open a file, and copy the
Mesage object to it in eom().
<li> Finally, you are probably aware that most email clients already
keep a copy of outgoing mail? Presumably there is a good reason for
keeping another copy on the server.
</ul>
<p>
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
determining whether you want to copy (e.g. whether the sender is local). For
example,
<pre>
def envfrom(...
...
if len(t) == 2:
self.rejectvirus = t[1] in reject_virus_from
if t[0] in wiretap_users.get(t[1],()):
self.add_recipient(wiretap_dest)
if t[1] == 'mydomain.com':
self.add_recipient('&lt;copy-%s&gt;' % t[0])
...
</pre>
<p>
To make this a generic feature requires thinking about how the configuration
would look. Feel free to make specific suggestions about config file
entries. Be sure to handle both Bcc and file copies, and designating what
mail should be copied. How should "outgoing" be defined? Implementing it is
easy once the configuration is designed.
<hr>
<p>
<a href="http://validator.w3.org/check/referer">
<img border=0 src="http://bmsi.com/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
<a href="http://www.redhat.com">
<img src="http://bmsi.com/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
</p>
+251
View File
@@ -0,0 +1,251 @@
# Document miltermodule for Doxygen
#
## @package milter
#
# A thin wrapper around libmilter. Most users will not import
# milter directly, but will instead import Milter and subclass
# Milter.Base. This module gives you ultimate low level control
# from python.
#
## 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>
# to hold a PyThreadState and a user defined Python object for the connection.
#
# Most application interaction with libmilter takes places via
# the milterContext object for the connection. It is passed to
# callback functions as the first parameter.
#
# The <code>Milter</code> module creates a python class for each connection,
# and converts function callbacks to instance method invocations.
#
class milterContext(object):
## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>.
def getsymval(self,sym): pass
## Calls <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> or
# <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="milter_api/smfi_addheader.html">smfi_addheader</a>.
def addheader(self,name,value,idx=-1): pass
## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>.
def chgheader(self,name,idx,value): pass
## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>.
def addrcpt(self,rcpt,params=None): pass
## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>.
def delrcpt(self,rcpt): pass
## 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="milter_api/smfi_quarantine.html">smfi_quarantine</a>.
def quarantine(self,reason): pass
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>.
def progress(self): pass
## 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.
# 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()
# 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.
# @param flags Bit or mask of optional actions to enable
def set_flags(flags): pass
def set_connect_callback(cb): pass
def set_helo_callback(cb): pass
def set_envfrom_callback(cb): pass
def set_envrcpt_callback(cb): pass
def set_header_callback(cb): pass
def set_eoh_callback(cb): pass
def set_body_callback(cb): pass
def set_abort_callback(cb): pass
def set_close_callback(cb): pass
## Sets the return code for untrapped Python exceptions during a callback.
# The default is TEMPFAIL. You should not depend on this handler. Your
# application should have its own top level exception handler for each
# callback. You can then choose your own reply message, log the stack track
# were you please, and so on. However, if you miss one, this last ditch
# handler will print a standard stack trace to sys.stderr, and return to
# sendmail.
# @param code one of #TEMPFAIL,#REJECT,#CONTINUE, or since 1.0, #ACCEPT
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.
# Callback functions must be set using the set_*_callback() functions before
# registering the %milter.
# Three additional callbacks are specified as keyword parameters. These
# were added by recent versions of libmilter. The keyword parameters is
# a nicer way to do it, I think, since it makes clear that you have to do
# it before registering. I may move all the callbacks in the future (perhaps
# keeping the set functions for compatibility). Note that Milter.Base
# automatically maps all callbacks to member functions, and negotiates which
# member functions are actually overridden by an application class.
# @param name the %milter name by which the MTA finds us
# @param negotiate the
# <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="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="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="milter_api/smfi_main.html">
# smfi_main</a>.
def main(): pass
## Set the libmilter debugging level.
# <a href="milter_api/smfi_setdbg.html">smfi_setdbg</a>
# sets the %milter library's internal debugging level to a new level
# so that code details may be traced. A level of zero turns off debugging. The
# greater (more positive) the level the more detailed the debugging. Six is the
# current, highest, useful value. Must be called before calling main().
def setdbg(lev): pass
## Set timeout for MTA communication.
# Calls <a href="milter_api/smfi_settimeout.html">
# smfi_settimeout</a>. Must be called before calling main().
def settimeout(secs): pass
## Set socket backlog.
# Calls <a href="milter_api/smfi_setbacklog.html">
# smfi_setbacklog</a>. Must be called before calling main().
def setbacklog(n): pass
## Set the socket used to communicate with the MTA.
# The MTA can communicate with the milter by means of a
# unix, inet, or inet6 socket. By default, a unix domain socket
# is used. It must not exist,
# and sendmail will throw warnings if, eg, the file is under a
# group or world writable directory. milter.setconn() will not fail with
# an invalid socket - this will be detected only when calling milter.main()
# or milter.opensocket().
# @param s the socket address in proto:address format
# <pre>
# milter.setconn('unix:/var/run/pythonfilter') # a named pipe
# milter.setconn('local:/var/run/pythonfilter') # a named pipe
# milter.setconn('inet:8800') # listen on ANY interface
# milter.setconn('inet:7871@@publichost') # listen on a specific interface
# milter.setconn('inet6:8020')
# milter.setconn('inet6:8020@[2001:db8:1234::1]') # listen on specific IP
# </pre>
def setconn(s): pass
## Stop the %milter gracefully.
def stop(): pass
## Retrieve diagnostic info.
# Return a tuple with diagnostic info gathered by the milter module.
# The first two fields are counts of milterContext objects created
# and deleted. Additional fields may be added later.
# @return a tuple of diagnostic data
def getdiag(): pass
## Retrieve the runtime libmilter version.
# Return the runtime libmilter version. This can be different
# from the compile time version when sendmail or libmilter is upgraded
# after pymilter is compiled.
# @return a tuple of <code>(major,minor,patchlevel)</code>
def getversion(): pass
## The compile time libmilter version.
# Python code might need to deal with pymilter compiled
# against various versions of libmilter. This module constant
# contains the contents of the <code>SMFI_VERSION</code> macro when
# the milter module was compiled.
VERSION = 0x1000001
-249
View File
@@ -1,249 +0,0 @@
Title: Python Milter Mail Policy
<h1> Python Milter Mail Policy </h1>
These are the policies implemented by the <code>bms.py</code> milter
application. The milter and Milter modules do not implement any policies
by themselves.
<h3> Classify connection </h3>
When the SMTP client connects, the connection IP address is
saved for later verification, and the connection
is classified as INTERNAL or EXTERNAL by matching the ip
address against the <code>internal_connect</code> configuration.
IP addresses with no PTR, and PTR names that look like
the kind assigned to dynamic IPs (as determined by a heuristic
algorithm) are flagged as DYNAMIC. IPs that match the
<code>trusted_relay</code> configuration are flagged as TRUSTED.
<p>
Examples from the log file (<i>not</i> the SMTP error message returned):
<pre>
2005Jul29 13:56:53 [71207] connect from p50863492.dip0.t-ipconnect.de at ('80.134.52.146', 1858) EXTERNAL DYN
2005Jul29 18:10:15 [74511] connect from foopub at ('1.2.3.4', 46513) EXTERNAL TRUSTED
2005Jul29 14:41:00 [71805] connect from foobar at ('192.168.0.1', 41205) INTERNAL
2005Jul29 14:41:15 [71806] connect from cncln.online.ln.cn at ('218.25.240.137', 35992) EXTERNAL
</pre>
<p>
Certain obviously evil PTR names are blocked at this point:
"localhost" (when IP is not 127.*) and ".".
<pre>
2005Jul29 14:49:50 [71918] connect from localhost at ('221.132.0.6', 50507) EXTERNAL
2005Jul29 14:49:50 [71918] REJECT: PTR is localhost
</pre>
<h3> HELO Check </h3>
The HELO name provided by the client is saved for later verification
(for example by SPF). We could validate the HELO at this point
by verifying that an A record for the HELO name matches the connect ip.
However, currently we only block certain obvious problems.
HELO names that look like an IP4 address
and ones that match the <code>hello_blacklist</code> configuration
are immediately rejected. The hello_blacklist typically contains
the current MTAs own HELO name or email domains.
Clients that attempt to skip HELO are immediately rejected.
<pre>
2005Jul29 18:10:15 [74512] hello from example.com
2005Jul29 18:10:15 [74512] REJECT: spam from self: example.com
2005Jul29 18:17:09 [74581] hello from 80.191.244.69
2005Jul29 18:17:09 [74581] REJECT: numeric hello name: 80.191.244.69
</pre>
<h3> MAIL FROM Check </h3>
Before calling our milter, sendmail checks a DNS blacklist to
block banned sender domains. We never see a blocked domain.
<p>
The MAIL FROM address is saved for possible use by the smart-alias
feature. First, the <code>internal_domains</code> is used for
a simple screening if defined. If the MAIL FROM for an INTERNAL connection
is NOT in <code>internal_domains</code>, then it is rejected (the
PC is most likely infected and attempting to send out spam).
If the MAIL FROM for an EXTERNAL connection IS in
<code>internal_domains</code>, then the message is immediately rejected.
This is quick and effective for most small company MTAs. For more
complex mail networks, it is too simplistic, and should not be defined.
SPF will handle the complex cases.
<h4> wiretap </h4>
The wiretap feature can screen and/or monitor mail to/from certain
users. If the MAIL FROM is being wiretapped, the recipients are
altered accordingly.
<!--table-stop-->
<h2> SPF check </h2>
The MAIL FROM, connect IP, and HELO name are checked against
any SPF records published via DNS for the alleged sender (MAIL FROM)
to determine the official SPF policy result.
The offical SPF result is then logged in the Received-SPF header field,
but certain results are subjected to further processing to create
an effective result for policy purposes.
<p>
If the official result is 'none', we try to turn it into an effective result of
'pass' or 'fail'. First, we check for a local substitute SPF record
under the domain defined in the <code>[spf]delegate</code> configuration.
It is often useful to add local SPF records for correspondents that are
too clueless to add their own. If there is no local substitute, we use a "best
guess" SPF record of "v=spf1 a/24 mx/24 ptr" for MAIL FROM or "v=spf1 a/24
mx/24" for HELO. In addition, a HELO that is a subdomain of MAIL FROM and
resolves to the connect IP results in an effective result of 'pass'.
<p>
If there is no local SPF record, and the effective result is still not
'pass', we check for either a valid HELO name or a valid PTR record for
the connect IP. A valid HELO or PTR cannot look like a dynamic name
as determined by the heuristic in <code>Milter.dynip</code>.
<p>
If HELO has an SPF record, and the result is anything but pass, we reject
the connection:
<pre>
2005Jul30 19:45:16 [93991] connect from [221.200.41.54] at ('221.200.41.54', 3581) EXTERNAL DYN
2005Jul30 19:45:18 [93991] hello from adelphia.net
2005Jul30 19:45:19 [93991] mail from <wendy.stubbsua@link-it.com> ()
2005Jul30 19:45:19 [93991] REJECT: hello SPF: fail 550 access denied
</pre>
Note that HELO does not have any forwarding issues like MAIL FROM, and so
any result other than 'pass' or 'none' should be treated like 'fail'.
<p>
Only if nothing about the SMTP envelope can be validated does the effective
result remain 'none. I call this the "3 strikes" rule.
<p>
If the official result is 'permerror' (a syntax error in the sender's
policy), we use the 'lax' option in pyspf to try various heuristics to guess
what they really meant. For instance, the invalid mechanism "ip:1.2.3.4" is
treated as "ip4:1.2.3.4". The result of lax processing is then used
as the effective result for policy purposes.
<p>
With an effective SPF result in hand, we consult the sendmail access
database to find our receiver policy for the sender.
<table border=1>
<tr><th>REJECT</th><td>
Reject the sender with a 550 5.7.1 SMTP code. The SMTP rejection
includes a detailed description of the problem.
</td></tr>
<tr><th>CBV</th><td>
Do a Call Back Validation by connecting to an MX of the sender
and checking that using the sender as the RCPT TO is not rejected.
We quit the CBV connection before actualling sending a message.
If the CBV is rejected, our SMTP connection is rejected with the
same error code and message. CBV results are cached.
</td></tr>
<tr><th>DSN</th><td>
Do a Call Back Validation by connecting to an MX of the sender
and checking that using the sender as the RCPT TO is not rejected.
Unlike a CBV, we continue on to data and send a detailed message
explaining the problem. This can be useful for reporting PermError
or SoftFail to the sender. Keep in mind that for any result other
than 'pass', the sender could be forged, and your DSN could annoy the
wrong person. However, a SoftFail result is requesting such feedback
for debugging and a PermError result needs to be fixed by the sender ASAP
whether forged or not. DSN results are cached so that senders are
annoyed only weekly.
</td></tr>
<tr><th>OK</th><td>
Accept the sender. The message may still be rejected via reputation
or content filtering.
</td></tr>
</table>
<h3> SPF policy syntax </h3>
First, the full sender is checked:
<pre>
SPF-Fail:abeb@adelphia.net DSN
</pre>
This says to accept mail from that adelphia.net user despite the
SPF fail, but only after annoying them with a DSN about their ISP's broken
policy.
<p>
If there is no match on the full sender, the domain is checked:
<pre>
SPF-Neutral:aol.com REJECT
</pre>
This says to reject mail from AOL with an SPF result of neutral.
This means AOL users can't use their AOL address with another mail service
to send us mail. This is good because the other mail service is
likely a badly configured greeting card site or a virus.
<p>
Finally, a default policy for the result is checked. While there are program
defaults, you should have defaults in the access database for SPF results:
<pre>
SPF-Neutral: CBV
SPF-Softfail: DSN
SPF-PermError: DSN
SPF-TempError: REJECT
SPF-None: REJECT
SPF-Fail: REJECT
SPF-Pass: OK
</pre>
<h2> Reputation </h2>
If the sender has not been rejected by this point, and if a GOSSiP server is
configured, we consult GOSSiP for the reputation score of the sender and
SPF result. The score is a number from -100 to 100 with a confidence
percentage from 0 to 100. A really bad reputation (less than -50 with
confidence greater than 3) is rejected. Note that the reputation is tracked
independently for each SPF result and sender combination. So aol.com:neutral
might have a really bad reputation, while aol.com:pass would be ok.
Furthermore, when a sender finally publishes an SPF policy and starts
getting SPF pass, their reputation is effectively reset.
<h2> Whitelists and Blacklists </h2>
The administrator can whitelist or blacklist senders and sending domains by
appending them to <code>${datadir}/auto_whitelist.log</code> or
<code>${datadir}/blacklist.log</code> respectively. In addition,
recipients of internal senders (except for automatic replies like vacation
messages and return receipts) are automatically whitelisted for 60 days, and
senders that fail CBV or DSN checks are automatically blacklisted for 30 days.
Whitelisted and blacklisted senders are used to automatically train the
bayesian content filter before being delivered or rejected, respectively.
<p>
Real Soon Now users will be able to maintain their own whitelist and
blacklist that applies only when they are the recipient.
<h2> Content Filter </h2>
Most messages have been rejected or delivered by now, but spammers
are always finding new places to send their junk from. For instance,
we get around 10000 emails a day, of which around 500 are first time
spam senders. A bayesian filter is trained by the whitelists and
blacklists, and scores the message. What is likely spam is either
rejected or quarantined. If the sender is an effective SPF pass,
then they get a DSN notifying them that their message has been
quarantined. (A DSN failure gets the sender auto blacklisted.)
Else, if the reject_spam option is set, the message is rejected.
Otherwise, a CBV is done (failure gets the sender auto blacklisted)
and the message is silently quarantined.
<p>
Normally, you don't want email messages to silently disappear into
a black hole, so you should set the reject_spam option. However,
if you don't want your correspondent's email to get rejected, you can
check your quarantine frequently instead.
<h3> Honeypot </h3>
You can also blacklist recipients by listing them as aliases of the
'honeypot' dspam user. These are collectively called
the honeypot. Any email to these recipients is used to train the
spam filter as spam and chalk up a reputation demerit for the sender, then
discarded. It might be a good idea to blacklist the sender if it has SPF pass
as well, but I'm afraid of accidents.
<h3> Reputation </h3>
Reputation is tracked by sending domain and effective SPF result.
The GOSSiP server tracks the spam/ham status of the last 1024 messages
for each domain:result combination. When the server is queried during
the SMTP envelope phase (MAIL FROM), it also queries any configured
peers, and the scores are combined. Domains with a history of spam for
a given SPF result are rejected at MAIL FROM. The GOSSiP system has
a command line utility to reset (delete) a reputation for cases where a
sender that was infected with malware is repaired. In addition,
the confidence score of a reputation decays with time, so a bad sender
will eventually be able to try again without manual intervention.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

-99
View File
@@ -1,99 +0,0 @@
Title: Requirements
<h2> Requirements </h2>
<menu>
<li> While the miltermodule will work with python 1.5, you probably
want to use python 2.0 or better. The python code uses a number of
python 2 features. The email support requires python 2.4.
<li> Python must be configured with thread support. This is because
pymilter uses sendmail's libmilter which requires thread support.
<li> You must compile sendmail with libmilter enabled. In versions of
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
is not installed by default.
Sendmail 8.12 still does not enable libmilter by default. You must
explicitly select the "MILTER" option when compiling.
<li> When compiling Python milter against sendmail versions earlier than
8.13, you must set MAX_ML_REPLY to 1 in setup.py. There is no way to tell from
the libmilter includes that smfi_setmlreply is not supported.
<li> You probably want to use sendmail-8.13, since that supports multi-line
SMTP error descriptions and SOCKETMAP. You want SOCKETMAP for use with
pysrs.
<li> Python milter has been tested against sendmail-8.11 through sendmail-8.13.
<li> Python milter must be compiled for the specific version of sendmail
it will run with. (Since the result is dynamically loaded, there could
conceivably be multiple versions available and selected at startup - but
that will have to wait.) This situation may only exist for sendmail
versions prior to 8.12. The protocol seems designed for backward
compatibility - and 8.12 is the first official milter release.
<li> Mea Culpa! After reading the Python Style guide, I realize that
my Python code is not up to snuff. Apparently mixed tabs and spaces
are anathema to those using Windows editors, where tabs can be expanded using
any arbitrary algorithm. Other than that, my
intuition matched Guido's pretty well - although I like to indent by 2
rather than 4. I will arrange to have tabs expanded to spaces when
exporting new versions. Until then, beware!
</menu>
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
to represent the stuff that comes with AIX. You might also want
my <a href="/aix/python.spec">python-2.1.1</a> spec file for AIX. It
does not include Tk or curses modules, sorry. If y'all trust me, you can
download rpms for AIX 4.x from my <a href="/aix">AIX RPM directory</a>.
<p>
Sendmail-8.12 renames
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system
shared library. Therefore, I rename libsm.a back to libsmutil.a for
AIX. This presents a problem for setup.py.
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
If you are running Redhat 7.2, the distributed version of sendmail
now enables libmilter by default. RedHat 7.2 bundles
the development libraries with the main sendmail package, so
there is no sendmail-devel package. However, they forgot to include the
headers! So you'll have to get the SRPM and modify it. I suggest
moving the static libs to a devel package and adding the headers. If
this is too much trouble, you can get the <a href="mfapi.h">mfapi.h</a>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
<p>
If you do modify the SRPM, I suggest renaming libsmutil.a
to libsm.a - just like sendmail-8.12 will. If you manually install
mfapi.h or don't rename libsmutil.a, you'll
need to force <code>libs = ["milter", "smutil"]</code> in setup.py.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
If you are running Redhat 6.2, the distributed version of sendmail
does not enable libmilter. You can download the Redhat 7.2 sendmail.spec
modified to compile on RedHat 6.2:
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
sendmail-rhmilter.spec</a>. The <a
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
SRPM for sendmail-8.11.6</a> is available from
<a href="http://www.redhat.com">Redhat</a> under
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
Errata for RH6.2</a>. But that doesn't include the latest security
patches since RH6.2 is no longer supported.
<p>
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> directory.
The lastest RPMs were built by taking a RH7.2 SRPMS and removing some
RPM features from the spec file that RH6.2 doesn't support, then
recompiling on RH6.2. You can check this by installing the RH7.2 SRPM,
then diffing my sendmail.spec with theirs. Then run
"rpm -bb sendmail-rhmilter.spec" when you are satisfied.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
You'll need to install the sendmail-devel package to compile milter.
-35
View File
@@ -1,35 +0,0 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: SPF fail (EMAIL FORGERY)
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification.
*** WARNING! YOU ARE SENDING FROM AN UNAUTHORIZED LOCATION ***
The email administrator for '%(sender_domain)' (YOUR administrator)
has FORBIDDEN you to send email from this location. IMMEDIATELY contact your
email administrator and follow his instructions to properly send mail.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy indicated that the above email was forged.
Because we believe your policy is in error, we have accepted the
email anyway. Please ask your email administrator to review
your SPF policy. You may also have neglected to follow your
postmaster's instructions for configuring outgoing email.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
Stuart D Gathman
postmaster@%(receiver)s
+17
View File
@@ -0,0 +1,17 @@
web:
doxygen
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-devel-* doc/html/milter_api
rsync -ravKk doc/html/ spidey2.bmsi.com:/Public/pymilter
cd doc/html; zip -r ../../doc .
VERSION=1.0
CVSTAG=pymilter-1_0
PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz
$(SRCTAR):
cvs export -r$(CVSTAG) -d $(PKG) pymilter
tar cvfz $(PKG).tar.gz $(PKG)
rm -r $(PKG)
cvstar: $(SRCTAR)
+80
View File
@@ -0,0 +1,80 @@
## A very simple milter to prevent mixing of internal and external mail.
# Internal is defined as using one of a list of internal top level domains.
# This code is open-source on the same terms as Python.
from __future__ import print_function
import Milter
import time
import sys
from Milter.utils import parse_addr
internal_tlds = ["corp", "personal"]
## Determine if a hostname is internal or not.
# True if internal, False otherwise
def is_internal(hostname):
components = hostname.split(".")
return components.pop() in internal_tlds:
# Determine if internal and external hosts are mixed based on a list
# of hostnames
def are_mixed(hostnames):
hostnames_mapped = map(is_internal, hostnames)
# Num internals
num_internal_hosts = hostnames_mapped.count(True)
# Num externals
num_external_hosts = hostnames_mapped.count(False)
return num_external_hosts >= 1 and num_internal_hosts >= 1
class NoMixMilter(Milter.Base):
def __init__(self): # A new instance with each new connection.
self.id = Milter.uniqueID() # Integer incremented with each call.
## def envfrom(self,f,*str):
@Milter.noreply
def envfrom(self, mailfrom, *str):
self.mailfrom = mailfrom
self.domains = []
t = parse_addr(mailfrom)
if len(t) > 1:
self.domains.append(t[1])
else:
self.domains.append('local')
self.internal = False
return Milter.CONTINUE
## def envrcpt(self, to, *str):
def envrcpt(self, to, *str):
self.R.append(to)
t = parse_addr(to)
if len(t) > 1:
self.domains.append(t[1])
else:
self.domains.append('local')
if are_mixed(self.domains):
# FIXME: log recipients collected in self.mailfrom and self.R
self.setreply('550','5.7.1','Mixing internal and external TLDs')
return Milter.REJECT
return Milter.CONTINUE
def main():
socketname = "/var/run/nomixsock"
timeout = 600
# Register to have the Milter factory create instances of your class:
Milter.factory = NoMixMilter
print("%s milter startup" % time.strftime('%Y%b%d %H:%M:%S'))
sys.stdout.flush()
Milter.runmilter("nomixfilter",socketname,timeout)
logq.put(None)
bt.join()
print("%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S'))
if __name__ == "__main__":
main()
+45 -34
View File
@@ -1,38 +1,32 @@
## 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
from socket import AF_INET, AF_INET6
from Milter.utils import parse_addr
if True:
from multiprocessing import Process as Thread, Queue
else:
from threading import Thread
from Queue import Queue
def parse_addr(t):
"""Split email into user,domain.
logq = Queue(maxsize=4)
>>> parse_addr('user@example.com')
['user', 'example.com']
>>> parse_addr('"user@example.com"')
['user@example.com']
>>> parse_addr('"user@bar"@example.com')
['user@bar', 'example.com']
>>> parse_addr('foo')
['foo']
"""
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
if t.startswith('"'):
if t.endswith('"'): return [t[1:-1]]
pos = t.find('"@')
if pos > 0: return [t[1:pos],t[pos+2:]]
return t.split('@')
class myMilter(Milter.Milter):
class myMilter(Milter.Base):
def __init__(self): # A new instance with each new connection.
self.id = Milter.uniqueID() # Integer incremented with each call.
@@ -40,6 +34,7 @@ class myMilter(Milter.Milter):
# each connection runs in its own thread and has its own myMilter
# instance. Python code must be thread safe. This is trivial if only stuff
# in myMilter instances is referenced.
@Milter.noreply
def connect(self, IPname, family, hostaddr):
# (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
# (self, 'ip6.mxout.example.com', AF_INET6,
@@ -80,45 +75,47 @@ class myMilter(Milter.Milter):
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
## def envrcpt(self, to, *str):
def envrcpt(self, recipient, *str):
@Milter.noreply
def envrcpt(self, to, *str):
rcptinfo = to,Milter.dictfromlist(str)
self.R.append(rcptinfo)
return Milter.CONTINUE
@Milter.noreply
def header(self, name, hval):
self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
return Milter.CONTINUE
@Milter.noreply
def eoh(self):
self.fp.write("\n") # terminate headers
return Milter.CONTINUE
@Milter.noreply
def body(self, chunk):
self.fp.write(chunk)
return Milter.CONTINUE
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')
return Milter.ACCEPT
def close(self):
# always called, even when abort is called. Clean up
# any external resources here.
@@ -131,25 +128,39 @@ class myMilter(Milter.Milter):
## === Support Functions ===
def log(self,*msg):
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print i,
print
logq.put((msg,self.id,time.time()))
def background():
while True:
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),
end=None)
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print(i,end=None)
print()
sys.stdout.flush()
## ===
def main():
bt = Thread(target=background)
bt.start()
socketname = "/home/stuart/pythonsock"
timeout = 600
# Register to have the Milter factory create instances of your class:
Milter.factory = myMilter
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
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)
print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
logq.put(None)
bt.join()
print("%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S'))
if __name__ == "__main__":
main()
-235
View File
@@ -1,235 +0,0 @@
[milter]
# the directory with log and data files
datadir = /var/log/milter
# the socket used to communicate with sendmail. Must match sendmail.cf
socket=/var/run/milter/pythonsock
# where to save original copies of defanged and failed messages
tempdir = /var/log/milter/save
# how long to wait for a response from sendmail before giving up
;timeout=600
log_headers = 0
# Connection ips and hostnames are matched against this glob style list
# to recognize internal senders. You probably need to change this.
# The default is a good guess to try and prevent newbie frustration.
internal_connect = 192.168.0.0/16,127.*
# mail that is not an internal_connect and claims to be from an
# internal domain is rejected. Furthermore, internal mail that
# does not claim to be from an internal domain is rejected.
# You should enable SPF instead if you can. SPF is much more comprehensive and
# flexible. However, SPF is not currently checked for outgoing
# (internal_connect) mail because it doesn't yet handle authorizing
# internal IPs locally.
;internal_domains = mycorp.com,localhost.localdomain
# connections from a trusted relay can trust the first Received header
# SPF checks are bypassed for internal connections and trusted relays.
;trusted_relay = 1.2.3.4, 66.12.34.56
# Relaying to these domains is allowed from internal connections only.
# You might want to restrict aol.com, for instance, so that stupid
# users don't forward their spam to aol for filtering and get your MTA
# blacklisted by aol.
;private_relay = aol.com, yahoo.com
# Reject external senders with hello names no legit external sender would use.
# SPF will do this also, but listing your own domain and mailserver here
# will save some DNS lookups when rejecting certain viruses.
;hello_blacklist = mycorp.com, 66.12.34.56
# Reject mail for domains mentioned unless user is mentioned here also
;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
# Treat localparts in milter.cfg as case-insensitive
case_sensitive_localpart = true
# features intended to filter or block incoming mail
[defang]
# do virus scanning on attached messages also
scan_rfc822 = 0
# do virus scanning on attached zipfiles also
scan_zip = 0
# Comment out scripts in HTML attachments. Can be CPU intensive.
scan_html = 0
# reject messages with asian fonts because we can't read them
block_chinese = 0
# list users who hate forwarded mail
;block_forward = egghead@mycorp.com, busybee@mycorp.com
# reject mail with these case insensitive strings in the subject
porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck, xanaax,
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra,
x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin,
valium, rolex, sexual, fuck, adv1t, vgaira, medz, acai berry
# reject mail with these case sensitive strings in the subject
spam_words = $$$, !!!, XXX, FREE, HGH
# attachments with these extensions will be replaced with a warning
# message. A copy of the original will be saved.
banned_exts = 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
# See http://bmsi.com/python/pysrs.html for details
[srs]
config=/etc/mail/pysrs.cfg
# SRS options can be set here also, but must match the sendmail plugin
;secret="shhhh!"
;maxage=21
;hashlength=4
;database=/var/log/milter/srsdata
;fwdomain = mydomain.com
# turn this on after a grace period to reject spoofed DSNs
reject_spoofed = 0
# Many braindead MTAs send DSNs with a non-DSN MFROM (e.g. to report that
# some virus claiming to be sent by you). This heuristic
# refuses mail from user names commonly abused in that way.
;banned_users = postmaster, mailer-daemon, clamav
# See http://www.openspf.com for more info on SPF.
[spf]
# namespace where SPF records can be supplied for domains without one
# records are searched for under _spf.domain.com
;delegate = domain.com
# domains where a neutral SPF result should cause mail to be rejected
;reject_neutral = aol.com
# use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published
;best_guess = 0
# Reject senders that have neither PTR nor valid HELO nor SPF records, or send
# DSN otherwise
;reject_noptr = 0
# always accept softfail from these domains, or send DSN otherwise
;accept_softfail = bounces.amazon.com
# Treat fail from these domains like softfail: because their SPF record
# or an important sender is screwed up. Must have valid HELO, however.
;accept_fail = custhelp.com
# Use sendmail access map or similar format for detailed spf policy.
# SPF entries in the access map will override any defaults set above.
;access_file = /etc/mail/access.db
# Add MAIL FROM as Sender when Sender is missing and From domain
# doesn't match MAIL FROM. Outlook and other email clients will then display
# something like: "Sent by sender@domain.com on behalf of from@example.com"
;supply_sender = 0
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
# features intended to clean up outgoing mail
[scrub]
# domains that block visible private nodes
;hide_path = jcpenney.com
# reject, don't just replace with warning, viruses from these domains
;reject_virus_from = mycorp.com
# features intended for spying on users and coworkers
[wiretap]
blind = 1
#
# wiretap lets you surreptitiously monitor a users outgoing email
# (sendmail aliases let you monitor incoming mail)
#
;users = disloyal@bigcorp.com, bigmouth@bigcorp.com
# multiple destinations can use smart_alias
;dest = spy@bigcorp.com
# discard outgoing mail without alerting sender
# can be used in conjunction with wiretap to censor outgoing mail
;discard_users = canned@bigcorp.com
# archive copies all delivered mail to a file
;mail_archive = /var/log/mail_archive
#
# smart aliases trigger on both sender and recipient
# alias = sender, recipient[, destination]
#
[smart_alias]
# multiple wiretap monitors. Smart aliases are applied after wiretap.
;spy1 = disloyal@bigcorp.com,spy@bigcorp.com
;spy2 = bigmouth@bigcorp.com,spy@bigcorp.com
# mail from client@clientcorp.com to sue@bigcorp.com is redirected to
# local alias copycust
;copycust = client@clientcorp.com,sue@bigcorp.com
# mail from cust@othercorp.com to walter@bigcorp.com is redirected to
# boss@bigcorp.com
;walter = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com
# additional copies can be added
;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com,
; walter@bigcorp.com
;bulk = soruce@telex.com,bob@jsconnor.com
;bulk1 = soruce@telex.com,larry@jsconnor.com,bulk
# See http://bmsi.com/python/dspam.html
[dspam]
# Select a well moderated dspam dictionary to reject spammy headers.
# To filter on the entire message, use the full setup below.
# only EXTERNAL messages are dspam filtered
;dspam_dict=/var/lib/dspam/moderator.dict
# Recipients of mail sent from these senders are added to the auto_whitelist.
# Auto_whitelisted senders with an SPF PASS are never rejected by dspam, and
# messages from auto_whitelisted senders will be used to train screener
# dictionaries as innocent mail.
;whitelist_senders = @mycorp.com
# Opt-out recipients entirely from dspam screening and header triage
;dspam_exempt=getitall@mycorp.com
# Do not scan mail (ostensibly) from these senders
;dspam_whitelist=getitall@sender.com
# Reject spam to these domains instead of quarantining it.
;dspam_reject=othercorp.com
# Scan internal mail - often a good source of stats on legit mail.
;dspam_internal=1
# directory for dspam user quarantine, signature db, and dictionaries
# defining this activates the dspam application
# dspam and dspam-python must be installed
;dspam_userdir=/var/lib/dspam
# do not dspam messages larger than this
;dspam_sizelimit=180000
# Map email addresses and aliases to dspam users
;dspam_users=david,goliath,spam,falsepositive
# List dspam users which train on all delivered messages, as opposed to
# "train on error" which trains only when a spam or falsepositive is reported.
# Training mode will build the dictionary faster, but requires close attention
# so as not to miss any spam or false positives.
;dspam_train=goliath
;david=david@foocorp.com,david.yelnetz@foocorp.com,david@bar.foocorp.com
;goliath=giant@foocorp.com,goliath.philistine@foocorp.com
# address to forward spam to. milter will process these and not deliver
;spam=spam@foocorp.com
# address to forward false positives to. milter will process and not deliver
;falsepositive=ham@foocorp.com
# account which receives only spam: all received messages are marked as spam.
;honeypot=spam-me@example.com
# the dspam_screener is a list of dspam users who screen mail for all
# recipients who are not dspam_users. Spam goes to the screeners quarantine,
# and the original recipients are saved so that false positives can be properly
# delivered.
;dspam_screener=david,goliath
# The dspam CGI can also be used: logins must match dspam users
# Optional pygossip interface
#
# GOSSiP tracks reputation of domain:qualifier pairs. For instance,
# the reputation of example.com:SPF is tracked separately from
# example.com:neutral. Currently qualifiers are
# SPF,neutral,softfail,fail,permerror,GUESS,HELO
[gossip]
# Use a dedicated GOSSiP server. If not specified, a local database
# will be used.
;server=host:11900
# To include peers of a peer in reputation, set ttl=2
;ttl=1
# If a local database is used, also consult these GOSSiP servers about
# domains. Peer reputation is also tracked as to how often they
# agree with us, and weighted accordingly.
;peers=host1:port,host2
[greylist]
dbfile=greylist.db
grey_time=5 # mins (Google retries in 5 mins)
grey_expire=6 # hours (some legit sites don't retry for 6 hours)
grey_retain=36 # days (keep "first monday" type mailings on file)
+160
View File
@@ -0,0 +1,160 @@
diff --git a/miltermodule.c b/miltermodule.c
index aa10a08..af9a144 100644
--- a/miltermodule.c
+++ b/miltermodule.c
@@ -343,7 +343,7 @@ static struct MilterCallback {
{ NULL , NULL }
};
-staticforward struct smfiDesc description; /* forward declaration */
+static struct smfiDesc description; /* forward declaration */
static PyObject *MilterError;
/* The interpreter instance that called milter.main */
@@ -355,7 +355,7 @@ typedef struct {
static milter_Diag diag;
-staticforward PyTypeObject milter_ContextType;
+static PyTypeObject milter_ContextType;
typedef struct {
PyObject_HEAD
@@ -700,7 +700,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
result = PyEval_CallObject(cb, arglist);
Py_DECREF(arglist);
if (result == NULL) return _report_exception(self);
- if (!PyInt_Check(result)) {
+ if (!PyLong_Check(result)) {
const struct MilterCallback *p;
const char *cbname = "milter";
char buf[40];
@@ -715,7 +715,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
PyErr_SetString(MilterError,buf);
return _report_exception(self);
}
- retval = PyInt_AS_LONG(result);
+ retval = PyLong_AS_LONG(result);
Py_DECREF(result);
_release_thread(self->t);
return retval;
@@ -732,7 +732,7 @@ makeipaddr(struct sockaddr_in *addr) {
sprintf(buf, "%d.%d.%d.%d",
(int) (x>>24) & 0xff, (int) (x>>16) & 0xff,
(int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff);
- return PyString_FromString(buf);
+ return PyUnicode_FromString(buf);
}
#ifdef HAVE_IPV6_SUPPORT
@@ -740,8 +740,8 @@ static PyObject *
makeip6addr(struct sockaddr_in6 *addr) {
char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */
const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf);
- if (s) return PyString_FromString(s);
- return PyString_FromString("inet6:unknown");
+ if (s) return PyUnicode_FromString(s);
+ return PyUnicode_FromString("inet6:unknown");
}
#endif
@@ -832,7 +832,7 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
for (i=0;i<count;i++) {
/* There's some error checking performed in do_mkvalue() for a string */
/* that's not currently done here - it probably should be */
- PyObject *o = PyString_FromStringAndSize(argv[i], strlen(argv[i]));
+ PyObject *o = PyUnicode_FromStringAndSize(argv[i], strlen(argv[i]));
if (o == NULL) { /* out of memory */
Py_DECREF(arglist);
return _report_exception(self);
@@ -963,7 +963,7 @@ milter_wrap_negotiate(SMFICTX *ctx,
int i;
for (i = 0; i < 4; ++i) {
*pa[i] = (i <= len)
- ? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
+ ? PyLong_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
: fa[i];
}
if (PyErr_Occurred()) {
@@ -1551,11 +1551,6 @@ static PyMethodDef context_methods[] = {
{ NULL, NULL }
};
-static PyObject *
-milter_Context_getattr(PyObject *self, char *name) {
- return Py_FindMethod(context_methods, self, name);
-}
-
static struct smfiDesc description = { /* Set some reasonable defaults */
"pythonfilter",
SMFI_VERSION,
@@ -1604,14 +1599,13 @@ static PyMethodDef milter_methods[] = {
};
static PyTypeObject milter_ContextType = {
- PyObject_HEAD_INIT(&PyType_Type)
- 0,
+ PyVarObject_HEAD_INIT(&PyType_Type,0)
"milterContext",
sizeof(milter_ContextObject),
0,
milter_Context_dealloc, /* tp_dealloc */
0, /* tp_print */
- milter_Context_getattr, /* tp_getattr */
+ 0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
0, /* tp_repr */
@@ -1625,6 +1619,13 @@ static PyTypeObject milter_ContextType = {
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
+ NULL, /* Documentation string */
+ 0, /* call function for all accessible objects */
+ 0, /* delete references to contained objects */
+ 0, /* rich comparisons */
+ 0, /* weak reference enabler */
+ 0, 0, /* Iterators */
+ context_methods, /* Attribute descriptor and subclassing stuff */
};
static const char milter_documentation[] =
@@ -1634,17 +1635,27 @@ Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
See <sendmailsource>/libmilter/README for details on setting it up.\n";
static void setitem(PyObject *d,const char *name,long val) {
- PyObject *v = PyInt_FromLong(val);
+ PyObject *v = PyLong_FromLong(val);
PyDict_SetItemString(d,name,v);
Py_DECREF(v);
}
-void
-initmilter(void) {
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "milter", /* m_name */
+ milter_documentation,/* m_doc */
+ -1, /* m_size */
+ milter_methods, /* m_methods */
+ NULL, /* m_reload */
+ NULL, /* m_traverse */
+ NULL, /* m_clear */
+ NULL, /* m_free */
+};
+
+PyMODINIT_FUNC PyInit_milter(void) {
PyObject *m, *d;
- m = Py_InitModule4("milter", milter_methods, milter_documentation,
- (PyObject*)NULL, PYTHON_API_VERSION);
+ m = PyModule_Create(&moduledef);
d = PyModule_GetDict(m);
MilterError = PyErr_NewException("milter.error", NULL, NULL);
PyDict_SetItemString(d,"error", MilterError);
@@ -1710,4 +1721,5 @@ initmilter(void) {
setitem(d,"DISCARD", SMFIS_DISCARD);
setitem(d,"ACCEPT", SMFIS_ACCEPT);
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
+ return m;
}
-85
View File
@@ -1,85 +0,0 @@
#!/bin/bash
#
# milter This shell script takes care of starting and stopping milter.
#
# chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail.
# processname: milter
# config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid
python="python2.4"
pidof() {
set - ""
if set - `ps -e -o pid,cmd | grep "${python} bms.py"` &&
[ "$2" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
[ -x /usr/lib/pymilter/start.sh ] || exit 0
RETVAL=0
prog="milter"
start() {
# Start daemons.
echo -n "Starting $prog: "
if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /usr/lib/pymilter/start.sh milter bms
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc -d 9 milter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/milter ]; then
stop
start
RETVAL=$?
fi
;;
status)
status milter
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
-85
View File
@@ -1,85 +0,0 @@
#!/bin/bash
#
# milter This shell script takes care of starting and stopping milter.
#
# chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail.
# processname: milter
# config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid
python="python2.4"
pidof() {
set - ""
if set - `ps -e -o pid,wchan,cmd | grep "rt_sig ${python} bms.py"` &&
[ "$3" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
[ -x /usr/lib/pymilter/start.sh ] || exit 0
RETVAL=0
prog="milter"
start() {
# Start daemons.
echo -n "Starting $prog: "
if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /usr/lib/pymilter/start.sh milter bms
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc milter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/milter ]; then
stop
start
RETVAL=$?
fi
;;
status)
status milter
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
+475 -77
View File
@@ -35,6 +35,82 @@ $ python setup.py help
libraries=["milter","smutil","resolv"]
* $Log$
* Revision 1.35 2013/03/14 22:11:25 customdesigned
* Release 0.9.8
*
* Revision 1.34 2013/03/09 05:42:14 customdesigned
* Make TestBase members private, fix getsymlist misspelling.
*
* Revision 1.33 2013/03/09 00:25:23 customdesigned
* Better untrapped exception message. const char for doc comments.
*
* Revision 1.32 2013/01/13 01:46:16 customdesigned
* Doc updates.
*
* Revision 1.31 2012/04/12 23:32:50 customdesigned
* Replace redundant callback array with macros. If this doesn't break anything,
* macros can be eliminated with code changes.
*
* Revision 1.30 2012/04/12 23:08:06 customdesigned
* Support RFC2553 on BSD
*
* Revision 1.29 2011/06/09 15:45:27 customdesigned
* Print callback name for non-int return error.
*
* Revision 1.28 2011/06/08 23:13:48 customdesigned
* 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.
*
@@ -179,10 +255,10 @@ $ python setup.py help
#endif
#define _FFR_MULTILINE (MAX_ML_REPLY > 1)
#include <pthread.h>
#include <netinet/in.h>
#include <Python.h>
#include <libmilter/mfapi.h>
//#include <pthread.h> // shouldn't be needed - use Python API
#include <Python.h> // Python C API
#include <libmilter/mfapi.h> // libmilter API
#include <netinet/in.h> // socket API
/* See if we have IPv4 and/or IPv6 support in this OS and in
@@ -203,10 +279,10 @@ $ python setup.py help
#define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */
/* Now see if it supports the RFC-2553 socket's API spec. Early
* IPv6 "prototype" implementations existed before the RFC was
* published. Unfortunately I know of now good way to do this
* published. Unfortunately I know of no good way to do this
* other than with OS-specific tests.
*/
#ifdef linux
#if defined(__FreeBSD_kernel__) || defined(__linux__)
#define HAVE_IPV6_RFC2553
#include <arpa/inet.h>
#endif
@@ -220,25 +296,64 @@ $ python setup.py help
#endif
#endif
enum callbacks {
CONNECT,HELO,ENVFROM,ENVRCPT,HEADER,EOH,BODY,EOM,ABORT,CLOSE,
#ifdef SMFIS_ALL_OPTS
UNKNOWN,DATA,NEGOTIATE,
#endif
NUMCALLBACKS
};
/* Yes, these are static. If you need multiple different callbacks, */
/* it's cleaner to use multiple filters, or convert to OO method calls. */
static PyObject *connect_callback = NULL;
static PyObject *helo_callback = NULL;
static PyObject *envfrom_callback = NULL;
static PyObject *envrcpt_callback = NULL;
static PyObject *header_callback = NULL;
static PyObject *eoh_callback = NULL;
static PyObject *body_callback = NULL;
static PyObject *eom_callback = NULL;
static PyObject *abort_callback = NULL;
static PyObject *close_callback = NULL;
#define connect_callback callback[CONNECT].cb
#define helo_callback callback[HELO].cb
#define envfrom_callback callback[ENVFROM].cb
#define envrcpt_callback callback[ENVRCPT].cb
#define header_callback callback[HEADER].cb
#define eoh_callback callback[EOH].cb
#define body_callback callback[BODY].cb
#define eom_callback callback[EOM].cb
#define abort_callback callback[ABORT].cb
#define close_callback callback[CLOSE].cb
#define unknown_callback callback[UNKNOWN].cb
#define data_callback callback[DATA].cb
#define negotiate_callback callback[NEGOTIATE].cb
/* Yes, these are static. If you need multiple different callbacks,
it's cleaner to use multiple filters, or convert to OO method calls. */
static struct MilterCallback {
PyObject *cb;
const char *name;
} callback[NUMCALLBACKS+1] = {
{ NULL ,"connect" },
{ NULL ,"helo" },
{ NULL ,"envfrom" },
{ NULL ,"envrcpt" },
{ NULL ,"header" },
{ NULL ,"eoh" },
{ NULL ,"body" },
{ NULL ,"eom" },
{ NULL ,"abort" },
{ NULL ,"close" },
#ifdef SMFIS_ALL_OPTS
{ NULL ,"unknown" },
{ NULL ,"data" },
{ NULL ,"negotiate" },
#endif
{ NULL , NULL }
};
staticforward struct smfiDesc description; /* forward declaration */
static PyObject *MilterError;
/* The interpreter instance that called milter.main */
static PyInterpreterState *interp;
typedef struct {
unsigned int contextNew;
unsigned int contextDel;
} milter_Diag;
static milter_Diag diag;
staticforward PyTypeObject milter_ContextType;
@@ -249,10 +364,10 @@ typedef struct {
PyThreadState *t; /* python thread state */
} milter_ContextObject;
/* Return a borrowed reference to the python Context. Create a
new Context if needed. The new Python Context is owned by
the SMFICTX. The python interpreter is locked on successful
return, otherwise not. */
/* Return a borrowed reference to the python Context. Called by callbacks
invoked by libmilter. Create a new Context if needed. The new
Python Context is owned by the SMFICTX. The python interpreter is locked on
successful return, otherwise not. */
static milter_ContextObject *
_get_context(SMFICTX *ctx) {
milter_ContextObject *self = smfi_getpriv(ctx);
@@ -277,6 +392,7 @@ _get_context(SMFICTX *ctx) {
PyThreadState_Delete(t);
return NULL;
}
++diag.contextNew;
self->t = t;
self->ctx = ctx;
Py_INCREF(Py_None);
@@ -286,7 +402,8 @@ _get_context(SMFICTX *ctx) {
return self;
}
/* Find the SMFICTX from a Python Context. The interpreter must be locked. */
/* Find the SMFICTX from a Python Context. Called by context methods invoked
from python. The interpreter must be locked. */
static SMFICTX *
_find_context(PyObject *c) {
SMFICTX *ctx = NULL;
@@ -314,6 +431,7 @@ milter_Context_dealloc(PyObject *s) {
}
Py_DECREF(self->priv);
PyObject_DEL(self);
++diag.contextDel;
}
/* Throw an exception if an smfi call failed, otherwise return PyNone. */
@@ -334,11 +452,12 @@ _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\
CHGBODY - filter may replace body\n\
CHGFROM - filter may replace body\n\
ADDRCPT - filter may add recipients\n\
DELRCPT - filter may delete recipients\n\
CHGHDRS - filter may change/delete headers";
@@ -361,7 +480,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
callback = 0;
else {
if (!PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
PyErr_SetString(PyExc_TypeError, "callback parameter must be callable");
return NULL;
}
Py_INCREF(callback);
@@ -374,7 +493,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\
@@ -401,7 +520,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\
@@ -412,7 +531,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\
@@ -425,7 +544,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\
@@ -438,7 +557,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\
@@ -451,7 +570,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";
@@ -461,7 +580,7 @@ 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\
@@ -474,7 +593,7 @@ 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\
@@ -487,7 +606,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\
@@ -501,7 +620,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\
@@ -514,7 +633,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";
@@ -525,7 +644,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;
@@ -540,23 +660,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;
}
@@ -573,9 +700,23 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
result = PyEval_CallObject(cb, arglist);
Py_DECREF(arglist);
if (result == NULL) return _report_exception(self);
retval = PyInt_AsLong(result);
if (!PyInt_Check(result)) {
const struct MilterCallback *p;
const char *cbname = "milter";
char buf[40];
Py_DECREF(result);
for (p = callback; p->name; ++p) {
if (cb == p->cb) {
cbname = p->name;
break;
}
}
sprintf(buf,"The %s callback must return int",cbname);
PyErr_SetString(MilterError,buf);
return _report_exception(self);
}
retval = PyInt_AS_LONG(result);
Py_DECREF(result);
if (PyErr_Occurred()) return _report_exception(self);
_release_thread(self->t);
return retval;
}
@@ -763,6 +904,83 @@ milter_wrap_abort(SMFICTX *ctx) {
return generic_noarg_wrapper(ctx,abort_callback);
}
#ifdef SMFIS_ALL_OPTS
static int
milter_wrap_unknown(SMFICTX *ctx, const char *cmd) {
PyObject *arglist;
milter_ContextObject *c;
if (unknown_callback == NULL) return SMFIS_CONTINUE;
c = _get_context(ctx);
if (!c) return SMFIS_TEMPFAIL;
arglist = Py_BuildValue("(Os)", c, cmd);
return _generic_wrapper(c, unknown_callback, arglist);
}
static int
milter_wrap_data(SMFICTX *ctx) {
return generic_noarg_wrapper(ctx,data_callback);
}
static int
milter_wrap_negotiate(SMFICTX *ctx,
unsigned long f0,
unsigned long f1,
unsigned long f2,
unsigned long f3,
unsigned long *pf0,
unsigned long *pf1,
unsigned long *pf2,
unsigned long *pf3) {
PyObject *arglist, *optlist;
milter_ContextObject *c;
int rc;
if (negotiate_callback == NULL) return SMFIS_ALL_OPTS;
c = _get_context(ctx);
if (!c)
return SMFIS_REJECT; // do not contact us again for current connection
optlist = Py_BuildValue("[kkkk]",f0,f1,f2,f3);
if (optlist == NULL)
arglist = NULL;
else
arglist = Py_BuildValue("(OO)", c, optlist);
PyThreadState *t = c->t;
c->t = 0; // do not release thread in _generic_wrapper
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))
: fa[i];
}
if (PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
rc = SMFIS_REJECT;
}
#endif
}
else if (rc != SMFIS_ALL_OPTS)
rc = SMFIS_REJECT;
Py_DECREF(optlist);
_release_thread(t);
return rc;
}
#endif
static int
milter_wrap_close(SMFICTX *ctx) {
/* xxfi_close can be called out of order - even before connect.
@@ -793,19 +1011,65 @@ milter_wrap_close(SMFICTX *ctx) {
return r;
}
static char milter_register__doc__[] =
"register(name) -> None\n\
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.";
static PyObject *
milter_register(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "s:register", &description.xxfi_name))
return NULL;
return _generic_return(smfi_register(description), "cannot register");
milter_register(PyObject *self, PyObject *args, PyObject *kwds) {
static char *kwlist[] = { "name","unknown","data","negotiate", NULL };
static PyObject** const cbp[3] =
{ &unknown_callback, &data_callback, &negotiate_callback };
PyObject *cb[3] = { NULL, NULL, NULL };
int i;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|OOO:register", kwlist,
&description.xxfi_name, &cb[0],&cb[1],&cb[2]))
return NULL;
for (i = 0; i < 3; ++i) {
PyObject *callback = cb[i];
if (callback != NULL && callback != Py_None) {
if (!PyCallable_Check(callback)) {
char err[80];
sprintf(err,"%s parameter must be callable",kwlist[i]);
PyErr_SetString(PyExc_TypeError, err);
return NULL;
}
}
}
for (i = 0; i < 3; ++i) {
PyObject *callback = cb[i];
if (callback != NULL) { // if keyword specified
if (callback == Py_None) {
callback = NULL;
}
else {
Py_INCREF(callback);
}
PyObject *oldval = *cbp[i];
*cbp[i] = callback;
if (oldval) {
Py_DECREF(oldval);
}
}
}
return _generic_return(smfi_register(description), "cannot register");
}
static char milter_main__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.";
static PyObject *
milter_opensocket(PyObject *self, PyObject *args) {
char rmsock = 0;
if (!PyArg_ParseTuple(args, "b:opensocket", &rmsock))
return NULL;
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
}
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().";
@@ -829,7 +1093,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.";
@@ -840,7 +1104,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.";
@@ -852,7 +1116,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.";
@@ -865,7 +1129,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\
@@ -885,7 +1149,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.";
@@ -898,7 +1162,31 @@ milter_stop(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_stop(), "cannot stop");
}
static char milter_getsymval__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.";
static PyObject *
milter_getdiag(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, ":getdiag")) return NULL;
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
}
static const char milter_getversion__doc__[] =
"getversion() -> tuple\n\
Return runtime libmilter version as a tuple of major,minor,patchlevel.";
static PyObject *
milter_getversion(PyObject *self, PyObject *args) {
unsigned int major, minor, patch;
if (!PyArg_ParseTuple(args, ":getversion")) return NULL;
if (smfi_version(&major,&minor,&patch) != MI_SUCCESS) {
PyErr_SetString(MilterError, "smfi_version failed");
return NULL;
}
return Py_BuildValue("(kkk)", major,minor,patch);
}
static const char milter_getsymval__doc__[] =
"getsymval(String) -> String\n\
Returns a symbol's value. Context-dependent, and unclear from the dox.";
@@ -913,7 +1201,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\
@@ -977,7 +1265,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\
@@ -1013,7 +1301,31 @@ milter_addheader(PyObject *self, PyObject *args) {
#endif
}
static char milter_chgheader__doc__[] =
#ifdef SMFIF_CHGFROM
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\
in set_flags() before calling register.\n\
This function can only be called from the EOM callback.";
static PyObject *
milter_chgfrom(PyObject *self, PyObject *args) {
char *sender;
char *params = NULL;
SMFICTX *ctx;
PyThreadState *t;
if (!PyArg_ParseTuple(args, "s|z:chgfrom", &sender, &params))
return NULL;
ctx = _find_context(self);
if (ctx == NULL) return NULL;
t = PyEval_SaveThread();
return _thread_return(t,smfi_chgfrom(ctx, sender, params),
"cannot change sender");
}
#endif
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\
@@ -1041,26 +1353,37 @@ milter_chgheader(PyObject *self, PyObject *args) {
"cannot change header");
}
static char milter_addrcpt__doc__[] =
"addrcpt(string) -> None\n\
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\
If params is used, you must pass ADDRCPT_PAR to set_flags().\n\
This function can only be called from the EOM callback.";
static PyObject *
milter_addrcpt(PyObject *self, PyObject *args) {
char *rcpt;
char *params = 0;
SMFICTX *ctx;
PyThreadState *t;
int rc;
if (!PyArg_ParseTuple(args, "s:addrcpt", &rcpt)) return NULL;
if (!PyArg_ParseTuple(args, "s|z:addrcpt", &rcpt, &params)) return NULL;
ctx = _find_context(self);
if (ctx == NULL) return NULL;
t = PyEval_SaveThread();
return _thread_return(t,smfi_addrcpt(ctx, rcpt), "cannot add recipient");
if (params)
#ifdef SMFIF_ADDRCPT_PAR
rc = smfi_addrcpt_par(ctx,rcpt,params);
#else
rc = MI_FAILURE;
#endif
else
rc = smfi_addrcpt(ctx,rcpt);
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.";
@@ -1075,11 +1398,10 @@ milter_delrcpt(PyObject *self, PyObject *args) {
ctx = _find_context(self);
if (ctx == NULL) return NULL;
t = PyEval_SaveThread();
return _thread_return(t,smfi_delrcpt(ctx, rcpt),
"cannot delete recipient");
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
}
static char milter_replacebody__doc__[] =
static const char milter_replacebody__doc__[] =
"replacebody(string) -> None\n\
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\
@@ -1097,11 +1419,11 @@ milter_replacebody(PyObject *self, PyObject *args) {
ctx = _find_context(self);
if (ctx == NULL) return NULL;
t = PyEval_SaveThread();
return _thread_return(t,smfi_replacebody(ctx, bodyp, bodylen),
"cannot replace message body");
return _thread_return(t,smfi_replacebody(ctx,
(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\
@@ -1127,7 +1449,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\
@@ -1145,7 +1467,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.";
@@ -1166,7 +1488,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.";
@@ -1183,6 +1505,27 @@ milter_progress(PyObject *self, PyObject *args) {
}
#endif
#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_setsymlist(PyObject *self, PyObject *args) {
SMFICTX *ctx;
PyThreadState *t;
int stage = 0;
char *smlist = 0;
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_setsymlist(ctx,stage,smlist),
"cannot set macro list");
}
#endif
static PyMethodDef context_methods[] = {
{ "getsymval", milter_getsymval, METH_VARARGS, milter_getsymval__doc__},
{ "setreply", milter_setreply, METH_VARARGS, milter_setreply__doc__},
@@ -1198,6 +1541,12 @@ static PyMethodDef context_methods[] = {
#endif
#ifdef SMFIR_PROGRESS
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
#endif
#ifdef SMFIF_CHGFROM
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
#endif
#ifdef SMFIF_SETSYMLIST
{ "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__doc__},
#endif
{ NULL, NULL }
};
@@ -1220,7 +1569,12 @@ static struct smfiDesc description = { /* Set some reasonable defaults */
milter_wrap_body,
milter_wrap_eom,
milter_wrap_abort,
milter_wrap_close
milter_wrap_close,
#ifdef SMFIS_ALL_OPTS
milter_wrap_unknown,
milter_wrap_data,
milter_wrap_negotiate
#endif
};
static PyMethodDef milter_methods[] = {
@@ -1235,15 +1589,17 @@ static PyMethodDef milter_methods[] = {
{ "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__},
{ "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__},
{ "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__},
{ "set_exception_policy", milter_set_exception_policy,METH_VARARGS, milter_set_exception_policy__doc__},
{ "register", milter_register, METH_VARARGS, milter_register__doc__},
{ "register", milter_register, METH_VARARGS, milter_register__doc__},
{ "set_exception_policy", milter_set_exception_policy, METH_VARARGS, milter_set_exception_policy__doc__},
{ "register", (PyCFunction)milter_register,METH_VARARGS|METH_KEYWORDS, milter_register__doc__},
{ "opensocket", milter_opensocket, METH_VARARGS, milter_opensocket__doc__},
{ "main", milter_main, METH_VARARGS, milter_main__doc__},
{ "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__},
{ "settimeout", milter_settimeout, METH_VARARGS, milter_settimeout__doc__},
{ "setbacklog", milter_setbacklog, METH_VARARGS, milter_setbacklog__doc__},
{ "setconn", milter_setconn, METH_VARARGS, milter_setconn__doc__},
{ "stop", milter_stop, METH_VARARGS, milter_stop__doc__},
{ "getdiag", milter_getdiag, METH_VARARGS, milter_getdiag__doc__},
{ "getversion", milter_getversion, METH_VARARGS, milter_getversion__doc__},
{ NULL, NULL }
};
@@ -1271,7 +1627,7 @@ static PyTypeObject milter_ContextType = {
Py_TPFLAGS_DEFAULT, /* tp_flags */
};
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\
@@ -1299,6 +1655,9 @@ initmilter(void) {
setitem(d,"CHGBODY", SMFIF_CHGBODY);
setitem(d,"MODBODY", SMFIF_MODBODY);
setitem(d,"ADDRCPT", SMFIF_ADDRCPT);
#ifdef SMFIF_ADDRCPT_PAR
setitem(d,"ADDRCPT_PAR", SMFIF_ADDRCPT_PAR);
#endif
setitem(d,"DELRCPT", SMFIF_DELRCPT);
setitem(d,"CHGHDRS", SMFIF_CHGHDRS);
setitem(d,"V1_ACTS", SMFI_V1_ACTS);
@@ -1306,6 +1665,45 @@ initmilter(void) {
setitem(d,"CURR_ACTS", SMFI_CURR_ACTS);
#ifdef SMFIF_QUARANTINE
setitem(d,"QUARANTINE",SMFIF_QUARANTINE);
#endif
#ifdef SMFIF_CHGFROM
setitem(d,"CHGFROM",SMFIF_CHGFROM);
#endif
#ifdef SMFIF_SETSYMLIST
setitem(d,"SETSYMLIST",SMFIF_SETSYMLIST);
setitem(d,"M_CONNECT",SMFIM_CONNECT);/* connect */
setitem(d,"M_HELO",SMFIM_HELO); /* HELO/EHLO */
setitem(d,"M_ENVFROM",SMFIM_ENVFROM);/* MAIL From */
setitem(d,"M_ENVRCPT",SMFIM_ENVRCPT);/* RCPT To */
setitem(d,"M_DATA",SMFIM_DATA); /* DATA */
setitem(d,"M_EOM",SMFIM_EOM); /* end of message (final dot) */
setitem(d,"M_EOH",SMFIM_EOH); /* end of header */
#endif
#ifdef SMFIS_ALL_OPTS
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
setitem(d,"P_NR_CONN",SMFIP_NR_CONN);
setitem(d,"P_NR_HELO",SMFIP_NR_HELO);
setitem(d,"P_NR_MAIL",SMFIP_NR_MAIL);
setitem(d,"P_NR_RCPT",SMFIP_NR_RCPT);
setitem(d,"P_NR_DATA",SMFIP_NR_DATA);
setitem(d,"P_NR_UNKN",SMFIP_NR_UNKN);
setitem(d,"P_NR_EOH",SMFIP_NR_EOH);
setitem(d,"P_NR_BODY",SMFIP_NR_BODY);
setitem(d,"P_NR_HDR",SMFIP_NR_HDR);
setitem(d,"P_NOCONNECT",SMFIP_NOCONNECT);
setitem(d,"P_NOHELO",SMFIP_NOHELO);
setitem(d,"P_NOMAIL",SMFIP_NOMAIL);
setitem(d,"P_NORCPT",SMFIP_NORCPT);
setitem(d,"P_NODATA",SMFIP_NODATA);
setitem(d,"P_NOUNKNOWN",SMFIP_NOUNKNOWN);
setitem(d,"P_NOEOH",SMFIP_NOEOH);
setitem(d,"P_NOBODY",SMFIP_NOBODY);
setitem(d,"P_NOHDRS",SMFIP_NOHDRS);
setitem(d,"P_HDR_LEADSPC",SMFIP_HDR_LEADSPC);
setitem(d,"P_SKIP",SMFIP_SKIP);
setitem(d,"ALL_OPTS",SMFIS_ALL_OPTS);
setitem(d,"SKIP",SMFIS_SKIP);
setitem(d,"NOREPLY",SMFIS_NOREPLY);
#endif
setitem(d,"CONTINUE", SMFIS_CONTINUE);
setitem(d,"REJECT", SMFIS_REJECT);
+119 -91
View File
@@ -1,4 +1,16 @@
# $Log$
# Revision 1.8 2011/11/05 15:51:03 customdesigned
# New example
#
# Revision 1.7 2009/06/13 21:15:12 customdesigned
# Doxygen updates.
#
# Revision 1.6 2009/06/09 03:13:13 customdesigned
# More doxygen docs.
#
# Revision 1.5 2005/07/20 14:49:43 customdesigned
# Handle corrupt and empty ZIP files.
#
# Revision 1.4 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
@@ -70,31 +82,44 @@
# with old milter code.
#
# This module provides a "defang" function to replace naughty attachments
# with a warning message.
## @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.
# Author: Stuart D. Gathman <stuart@bmsi.com>
# 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 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():
@@ -103,60 +128,62 @@ def zipnames(txt):
names += zipnames(zipf.read(nm))
return names
class MimeGenerator(Generator):
## Fix multipart handling in email.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
# - Provide a headerchange event for integration with Milter
# Headerchange attribute can be assigned a function to be called when
# changing headers. The signature is:
# headerchange(msg,name,value) -> None
# - Track modifications to headers of body or any part independently
## Enhance email.message.Message
#
# Tracks modifications to headers of body or any part independently.
class MimeMessage(Message):
"""Version of email.Message.Message compatible with old mime module
"""
def __init__(self,fp=None,seekable=1):
Message.__init__(self)
self.headerchange = None
self.submsg = None
self.modified = False
## @var headerchange
# Provide a headerchange event for integration with Milter.
# The headerchange attribute can be assigned a function to be called when
# changing headers. The signature is:
# headerchange(msg,name,value) -> None
self.headerchange = None
def get_param(self, param, failobj=None, header='content-type', unquote=True):
val = Message.get_param(self,param,failobj,header,unquote)
@@ -181,23 +208,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):
@@ -216,9 +243,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()
@@ -268,18 +295,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()
@@ -290,7 +317,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."
@@ -314,7 +341,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:
@@ -329,8 +356,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
@@ -361,7 +386,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
@@ -376,18 +401,18 @@ class _defang:
# emulate old defang function
defang = _defang()
import sgmllib
from 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):
@@ -418,7 +443,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
@@ -431,25 +456,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
@@ -461,11 +486,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):
@@ -478,16 +506,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(
@@ -510,14 +538,14 @@ if __name__ == '__main__':
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)
-39
View File
@@ -1,39 +0,0 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: SPF %(result)s (POSSIBLE FORGERY)
Auto-Submitted: auto-generated (sender verification)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy (or lack thereof) indicated that the above email was not
sent via an authorized SMTP server, but may still be legitimate. Since there
is no positive confirmation that the message is really from you, we have
to give it extra scrutiny - including verifying that the sender really
exists by sending you this DSN. We will remember this sender and not
bother you again for a while. You can avoid this message entirely for
legitimate mail by using an authorized SMTP server. Contact your mail
administrator and ask how to configure your email client to use an
authorized server.
If you never sent the above message, then your domain has been forged.
Your mail admin needs to publish a strict SPF record so that I can reject
those forgeries instead of bugging you about them.
See http://openspf.org for details.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
+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" $@
-35
View File
@@ -1,35 +0,0 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: Critical SPF configuration error
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your spf record has a permanent error. The error was:
%(perm_error)s
We will reinterpret your record using "lax" processing heuristics
which may result in your mail being accepted anyway. But you or your
mail administrator need to fix your SPF record as soon as possible.
We are sending you this message to alert you to the fact that
you have problems with your email configuration.
If you need further assistance, please do not hesitate to
contact me again.
Kind regards,
postmaster@%(receiver)s
+192
View File
@@ -0,0 +1,192 @@
%if 0%{?rhel} == 7
%define pythonbase python34
%else
%define pythonbase python3
%endif
%define __python python3
%define libdir %{_libdir}/pymilter
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
Summary: Python interface to sendmail milter API
Name: %{pythonbase}-pymilter
Version: 1.0.1
Release: 1%{dist}
Source: https://github.com/sdgathman/pymilter/archive/pymilter-%{version}.tar.gz
Source1: pymilter.te
# Patch miltermodule to python3
# FIXME: replace with reverse patch at some point (make py3 the default)
Patch: milter.patch
License: GPLv2+
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html
# python-2.6.4 gets RuntimeError: not holding the import lock
Requires: %{pythonbase} >= 2.6.5, sendmail-milter >= 8.13
%if 0%{?fedora} >= 23
# Need python2.6 specific pydns, not the version for system python
Recommends: %{pythonbase}-pydns
%endif
# Needed for callbacks, not a core function but highly useful for milters
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13
%description
This is a python extension module to enable python scripts to
attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV.
%package selinux
Summary: SELinux policy module for pymilter
Group: System Environment/Base
Requires: policycoreutils, selinux-policy, %{name}
BuildRequires: policycoreutils, checkpolicy
%if 0%{?epel} >= 6
BuildRequires: policycoreutils-python
%else
BuildRequires: policycoreutils-python-utils
%endif
%description selinux
SELinux policy module for using pymilter with sendmail with selinux enforcing
%prep
%setup -q -n pymilter-%{version}
%patch -p1 -b .py3
cp %{SOURCE1} pymilter.te
%build
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
checkmodule -m -M -o pymilter.mod pymilter.te
semodule_package -o pymilter.pp -m pymilter.mod
%install
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install --root=$RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
mkdir -p $RPM_BUILD_ROOT%{libdir}
# install selinux modules
mkdir -p %{buildroot}%{_datadir}/selinux/targeted
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%files
%defattr(-,root,root,-)
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%{python_sitearch}/*
%{libdir}
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%files selinux
%doc pymilter.te
%{_datadir}/selinux/targeted/*
%clean
rm -rf $RPM_BUILD_ROOT
%post selinux
/usr/sbin/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
&>/dev/null || :
%postun selinux
if [ $1 -eq 0 ] ; then
/usr/sbin/semodule -s targeted -r pymilter &> /dev/null || :
fi
%changelog
* Tue Sep 20 2016 Stuart Gathman <stuart@gathman.org> 1.0.1-1
- Support python3
* Sat Mar 1 2014 Stuart Gathman <stuart@gathman.org> 1.0-2
- Remove start.sh to track EPEL repository, suggest daemonize as replacement
- Selinux subpackage should not care about pymilter version
* Wed Jun 26 2013 Stuart Gathman <stuart@gathman.org> 1.0-1
- Allow ACCEPT as untrapped exception policy
- Optional dir for getaddrset and getaddrdict in Milter.config
- Show registered milter name in untrapped exception message.
- Include selinux subpackage
- Provide Milter.greylist export and Milter.greylist import to migrate data
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1
- Add Milter.test module for unit testing milters.
- Fix typo that prevented setsymlist from being active.
- Change untrapped exception message to:
- "pymilter: untrapped exception in milter app"
* Thu Apr 12 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
- Remove redundant table in miltermodule
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
- Raise ValueError on unescaped '%' passed to setreply
- Grace time at end of Greylist window
* Fri Aug 19 2011 Stuart Gathman <stuart@bmsi.com> 0.9.5-1
- Print milter.error for invalid callback return type.
(Since stacktrace is empty, the TypeError exception is confusing.)
- Fix milter-template.py
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
* Tue Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
- Handle IP6 in Milter.utils.iniplist()
- python-2.6
* Thu Jul 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.3-1
- Handle source route in Milter.utils.parse_addr()
- Fix default arg in chgfrom.
- Disable negotiate callback for libmilter < 8.14.3 (1,0,1)
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-3
- Change result of @noreply callbacks to NOREPLY when so negotiated.
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-2
- Cache callback negotiation
* Thu May 28 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-1
- Add new callback support: data,negotiate,unknown
- Auto-negotiate protocol steps
* Thu Feb 05 2009 Stuart Gathman <stuart@bmsi.com> 0.9.1-1
- Fix missing address of optional param to addrcpt
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-4
- Stop using INSTALLED_FILES to make Fedora happy
- Remove config flag from start.sh glue
- Own /var/log/milter
- Use _localstatedir
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-2
- Changes to meet Fedora standards
* Mon Nov 24 2008 Stuart Gathman <stuart@bmsi.com> 0.9.0-1
- Split pymilter into its own CVS module
- Support chgfrom and addrcpt_par
- Support NS records in Milter.dns
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
- /var/run/milter directory must be owned by mail
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
- improved parsing into email and fullname (still 2 self test failures)
- implement no-DSN CBV, reduce full DSNs
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
- Use ifarch hack to build milter and milter-spf packages as noarch
- Remove spf dependency from dsn.py, add dns.py
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
- move AddrCache, parse_addr, iniplist to Milter package
- move parse_header to Milter.utils
- fix plock for missing source and can't change owner/group
- split out pymilter and pymilter-spf packages
- move milter apps to /usr/lib/pymilter
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
- SPF moved to pyspf RPM
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
- Support CBV timeout
+146 -406
View File
@@ -1,211 +1,34 @@
# This spec file contains 2 noarch packages in addition to the pymilter
# module. To compile all three on 32-bit Intel, use:
# rpmbuild -ba --target=i386,noarch pymilter.spec
%define __python python2.4
%define version 0.8.11
%define release 1%{?dist}.py24
# what version of RH are we building for?
%define redhat7 0
# Options for Redhat version 6.x:
# rpm -ba|--rebuild --define "rh7 1"
%{?rh7:%define redhat7 1}
# some systems dont have initrddir defined
%{?_initrddir:%define _initrddir /etc/rc.d/init.d}
%if %{redhat7}
# Redhat 7.x and earlier (multiple ps lines per thread)
%define sysvinit milter.rc7
%else
%define sysvinit milter.rc
%endif
# RH9, other systems (single ps line per process)
%ifos aix4.1
%define libdir /var/log/milter
%define __python python2
%if 0%{?rhel} == 6
%define pythonbase python
%else
%define pythonbase python2
%endif
%define libdir %{_libdir}/pymilter
%endif
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
%ifarch noarch
Name: milter
Group: Applications/System
Summary: BMS spam and reputation milter
Version: %{version}
Release: %{release}
Source: pymilter-%{version}.tar.gz
#Patch: %{name}-%{version}.patch
License: GPL
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot
Vendor: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html
Requires: %{__python} >= 2.4, pyspf >= 2.0.4, pymilter
%ifos Linux
Requires: chkconfig
%endif
%description -n milter
A complex but effective spam filtering, SPF checking, greylisting,
and reputation tracking mail application. It uses pydspam if installed for
bayesian filtering.
%package spf
Group: Applications/System
Summary: BMS spam and reputation milter
Requires: pyspf >= 2.0.4, pymilter
Obsoletes: pymilter-spf < 0.8.10
%description spf
A simple mail filter to add Received-SPF headers and reject forged mail.
Rejection policy is configured via sendmail access file and can be
tailored by domain.
%prep
%setup -q -n pymilter-%{version}
#patch -p0 -b .bms
%install
rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/var/log/milter
mkdir -p $RPM_BUILD_ROOT/etc/mail
mkdir $RPM_BUILD_ROOT/var/log/milter/save
mkdir -p $RPM_BUILD_ROOT%{libdir}
cp *.txt $RPM_BUILD_ROOT/var/log/milter
cp -p bms.py spfmilter.py ban2zone.py $RPM_BUILD_ROOT%{libdir}
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
cp spfmilter.cfg $RPM_BUILD_ROOT/etc/mail
# logfile rotation
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF'
/var/log/milter/milter.log {
copytruncate
compress
}
/var/log/milter/banned_ips {
rotate 7
daily
copytruncate
}
EOF
# purge saved defanged message copies
mkdir -p $RPM_BUILD_ROOT/etc/cron.daily
%ifos aix4.1
R=
%else
R='-r'
%endif
cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF'
#!/bin/sh
find /var/log/milter/save -mtime +7 | xargs $R rm
# work around memory leak
/etc/init.d/milter condrestart
EOF
chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
%ifnos aix4.1
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
cp spfmilter.rc $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
/^python=/
c
python="%{__python}"
.
w
q
EOF
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter <<'EOF'
/^python=/
c
python="%{__python}"
.
w
q
EOF
%endif # aix4.1
mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
%ifos aix4.1
%post
mkssys -s milter -p %{libdir}/start.sh -u 25 -S -n 15 -f 9 -G mail || :
%preun
if [ $1 = 0 ]; then
rmssys -s milter || :
fi
%else # not aix4.1
%post -n milter
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
/sbin/chkconfig --add milter
%preun -n milter
if [ $1 = 0 ]; then
/sbin/chkconfig --del milter
fi
%post spf
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
/sbin/chkconfig --add spfmilter
%preun spf
if [ $1 = 0 ]; then
/sbin/chkconfig --del spfmilter
fi
%endif # aix4.1
%files
%defattr(-,root,root)
/etc/logrotate.d/milter
/etc/cron.daily/milter
%ifos aix4.1
%defattr(-,smmsp,mail)
%else
/etc/rc.d/init.d/milter
%defattr(-,mail,mail)
%endif
%dir /var/log/milter
%dir /var/log/milter/save
%config %{libdir}/bms.py
%config %{libdir}/ban2zone.py
%config(noreplace) /var/log/milter/strike3.txt
%config(noreplace) /var/log/milter/softfail.txt
%config(noreplace) /var/log/milter/fail.txt
%config(noreplace) /var/log/milter/neutral.txt
%config(noreplace) /var/log/milter/quarantine.txt
%config(noreplace) /var/log/milter/permerror.txt
%config(noreplace) /var/log/milter/temperror.txt
%config(noreplace) /etc/mail/pymilter.cfg
/usr/share/sendmail-cf/hack/rhsbl.m4
%files spf
%defattr(-,root,root)
%dir /var/log/milter
%{libdir}/spfmilter.py*
%config(noreplace) /etc/mail/spfmilter.cfg
/etc/rc.d/init.d/spfmilter
%else # not noarch
%define name pymilter
Summary: Python interface to sendmail milter API
Name: %{name}
Version: %{version}
Release: %{release}
Source: %{name}-%{version}.tar.gz
#Patch: %{name}-%{version}.patch
License: GPL
Name: %{pythonbase}-pymilter
Version: 1.0.1
Release: 1%{dist}
Source: https://github.com/sdgathman/pymilter/archive/pymilter-%{version}.tar.gz
Source1: pymilter.te
# Patch miltermodule to python3
# FIXME: replace with reverse patch at some point (make py3 the default)
Patch: milter.patch
License: GPLv2+
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot
Vendor: Stuart D. Gathman <stuart@bmsi.com>
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html
Requires: %{__python} >= 2.4, sendmail >= 8.13
BuildRequires: %{__python}-devel >= 2.4, sendmail-devel >= 8.13
# python-2.6.4 gets RuntimeError: not holding the import lock
Requires: %{pythonbase} >= 2.6.5, sendmail-milter >= 8.13
%if 0%{?fedora} >= 23
# Need python2.6 specific pydns, not the version for system python
Recommends: %{pythonbase}-pydns
%endif
# Needed for callbacks, not a core function but highly useful for milters
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13
%description
This is a python extension module to enable python scripts to
@@ -213,239 +36,156 @@ attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV.
%package selinux
Summary: SELinux policy module for pymilter
Group: System Environment/Base
Requires: policycoreutils, selinux-policy, %{name}
BuildRequires: policycoreutils, checkpolicy
%if 0%{?epel} >= 6
BuildRequires: policycoreutils-python
%else
BuildRequires: policycoreutils-python-utils
%endif
%description selinux
SELinux policy module for using pymilter with sendmail with selinux enforcing
%prep
%setup -q
#patch -p0 -b .bms
%setup -q -n pymilter-%{version}
cp %{SOURCE1} pymilter.te
%build
%if %{redhat7}
LDFLAGS="-s"
%else # Redhat builds debug packages after 7.3
LDFLAGS="-g"
%endif
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{__python} setup.py build
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
checkmodule -m -M -o pymilter.mod pymilter.te
semodule_package -o pymilter.pp -m pymilter.mod
%install
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
mkdir -p $RPM_BUILD_ROOT/var/run/milter
%{__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}
%ifos aix4.1
cat >$RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
#!/bin/sh
cd /var/log/milter
exec /usr/local/bin/python bms.py >>milter.log 2>&1
EOF
%else # not aix4.1
cp start.sh $RPM_BUILD_ROOT%{libdir}
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^python=/
c
python="%{__python}"
.
w
q
EOF
%endif
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
%if !%{redhat7}
#grep '.pyc$' INSTALLED_FILES | sed -e 's/c$/o/' >>INSTALLED_FILES
%endif
# start.sh is used by spfmilter and milter, and could be used by
# other milters running on redhat
%files -f INSTALLED_FILES
%defattr(-,root,root)
%doc README HOWTO ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%config %{libdir}/start.sh
%dir %attr(0755,mail,mail) /var/run/milter
# install selinux modules
mkdir -p %{buildroot}%{_datadir}/selinux/targeted
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%endif # noarch
%files
%defattr(-,root,root,-)
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%{python_sitearch}/*
%{libdir}
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%files selinux
%doc pymilter.te
%{_datadir}/selinux/targeted/*
%clean
rm -rf $RPM_BUILD_ROOT
%post selinux
/usr/sbin/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
&>/dev/null || :
%postun selinux
if [ $1 -eq 0 ] ; then
/usr/sbin/semodule -s targeted -r pymilter &> /dev/null || :
fi
%changelog
* Tue Sep 20 2016 Stuart Gathman <stuart@gathman.org> 1.0.1-1
- Support python3
* Sat Mar 1 2014 Stuart Gathman <stuart@gathman.org> 1.0-2
- Remove start.sh to track EPEL repository, suggest daemonize as replacement
- Selinux subpackage should not care about pymilter version
* Wed Jun 26 2013 Stuart Gathman <stuart@gathman.org> 1.0-1
- Allow ACCEPT as untrapped exception policy
- Optional dir for getaddrset and getaddrdict in Milter.config
- Show registered milter name in untrapped exception message.
- Include selinux subpackage
- Provide Milter.greylist export and Milter.greylist import to migrate data
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1
- Add Milter.test module for unit testing milters.
- Fix typo that prevented setsymlist from being active.
- Change untrapped exception message to:
- "pymilter: untrapped exception in milter app"
* Thu Apr 12 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
- Remove redundant table in miltermodule
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
- Raise ValueError on unescaped '%' passed to setreply
- Grace time at end of Greylist window
* Fri Aug 19 2011 Stuart Gathman <stuart@bmsi.com> 0.9.5-1
- Print milter.error for invalid callback return type.
(Since stacktrace is empty, the TypeError exception is confusing.)
- Fix milter-template.py
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
* Tue Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
- Handle IP6 in Milter.utils.iniplist()
- python-2.6
* Thu Jul 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.3-1
- Handle source route in Milter.utils.parse_addr()
- Fix default arg in chgfrom.
- Disable negotiate callback for libmilter < 8.14.3 (1,0,1)
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-3
- Change result of @noreply callbacks to NOREPLY when so negotiated.
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-2
- Cache callback negotiation
* Thu May 28 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-1
- Add new callback support: data,negotiate,unknown
- Auto-negotiate protocol steps
* Thu Feb 05 2009 Stuart Gathman <stuart@bmsi.com> 0.9.1-1
- Fix missing address of optional param to addrcpt
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-4
- Stop using INSTALLED_FILES to make Fedora happy
- Remove config flag from start.sh glue
- Own /var/log/milter
- Use _localstatedir
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-2
- Changes to meet Fedora standards
* Mon Nov 24 2008 Stuart Gathman <stuart@bmsi.com> 0.9.0-1
- Split pymilter into its own CVS module
- Support chgfrom and addrcpt_par
- Support NS records in Milter.dns
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
- /var/run/milter directory must be owned by mail
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
- log rcpt for SRS rejections
- improved parsing into email and fullname (still 2 self test failures)
- implement no-DSN CBV, reduce full DSNs
- check for porn words in MAIL FROM fullname
- ban IP for too many bad MAIL FROMs or RCPT TOs
- temperror policy in access
- no CBV for whitelisted MAIL FROM except permerror, softfail
- Allow explicitly whitelisted email from banned_users.
- configure gossip TTL
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
- Use ifarch hack to build milter and milter-spf packages as noarch
- Remove spf dependency from dsn.py, add dns.py
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
- move AddrCache, parse_addr, iniplist to Milter package
- move parse_header to Milter.utils
- fix plock for missing source and can't change owner/group
- add sample spfmilter.py milter
- private_relay config option
- persist delayed DSN blacklisting
- handle gossip server restart without disabling gossip
- split out pymilter and pymilter-spf packages
- move milter apps to /usr/lib/pymilter
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
- More lame bounce heuristics
- SPF moved to pyspf RPM
- wiretap archive option
- Do plain CBV if missing template
- SMTP AUTH policy in access
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
- Support CBV timeout
- Support fail template, headers in templates
- Create GOSSiP record only when connection will procede to DATA.
- More SPF lax heuristics
- Don't require SPF pass for white/black listing mail from trusted relay.
- Support localpart wildcard for white and black lists.
* Thu Feb 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-1
- Delay reject of unsigned RCPT for postmaster and abuse only
- Fix dsn reporting of hard permerror
- Resolve FIXME for wrap_close in miltermodule.c
- Add Message-ID to DSNs
- Use signed Message-ID in delayed reject to blacklist senders
- Auto-train via blacklist and auto-whitelist
- Don't check userlist for signed MFROM
- Accept but skip DSPAM and training for whitelisted senders without SPF PASS
- Report GC stats
- Support CIDR matching for IP lists
- Support pysrs sign feature
- Support localpart specific SPF policy in access file
* Thu Dec 29 2005 Stuart Gathman <stuart@bmsi.com> 0.8.5-1
- Simple trusted_forwarder implementation.
- Fix access_file neutral policy
- Move Received-SPF header to beginning of headers
- Supply keyword info for all results in Received-SPF header.
- Move guessed SPF result to separate header
- Activate smfi_insheader only when SMFIR_INSHEADER defined
- Handle NULL MX in spf.py
- in-process GOSSiP server support (to be extended later)
- Expire CBV cache and renew auto-whitelist entries
* Fri Oct 21 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-2
- Don't supply sender when MFROM is subdomain of header from/sender.
- Don't send quarantine DSN for DSNs
- Skip dspam for replies/DSNs to signed MFROM
* Thu Oct 20 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-1
- Fix SPF policy via sendmail access map (case insensitive keys).
- Auto whitelist senders, train screener on whitelisted messages
- Optional idx parameter to addheader to invoke smfi_insheader
- Activate progress when SMFIR_PROGRESS defined
* Wed Oct 12 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-1
- Keep screened honeypot mail, but optionally discard honeypot only mail.
- spf_accept_fail option for braindead SPF senders (treats fail like softfail)
- Consider SMTP AUTH connections internal.
- Send DSN for SPF errors corrected by extended processing.
- Send DSN before SCREENED mail is quarantined
- Option to set SPF policy via sendmail access map.
- Option to supply Sender header from MAIL FROM when missing.
- Use logging package to keep log lines atomic.
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-4
- Limit each CNAME chain independently like PTR and MX
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-3
- Limit CNAME lookups (regression)
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-2
- Handle corrupt ZIP attachments
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.2-1
- Strict processing limits per SPF RFC
- Fixed several parsing bugs under RFC
- Support official IANA SPF record (type99)
- Honeypot support (requires pydspam-1.1.9)
- Extended SPF processing results beyond strict RFC limits
- Support original SES for local bounce protection (requires pysrs-0.30.10)
- Callback exception processing option in milter module
* Thu Jun 16 2005 Stuart Gathman <stuart@bmsi.com> 0.8.1-1
- Fix zip in zip loop in mime.py
- Fix HeaderParseError in bms.py header callback
- Check internal_domains for outgoing mail
- Fix inconsistent results from send_dsn
* Mon Jun 06 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-3
- properly log pydspam exceptions
* Sat Jun 04 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-2
- Include default softfail, strike3 templates
* Wed May 25 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-1
- Move Milter module to subpackage.
- DSN support for Three strikes rule and SPF SOFTFAIL
- Move /*mime*/ and dynip to Milter subpackage
- Fix SPF unknown mechanism list not cleared
- Make banned extensions configurable.
- Option to scan zipfiles for bad extensions.
* Tue Feb 08 2005 Stuart Gathman <stuart@bmsi.com> 0.7.3-1.EL3
- Support EL3 and Python2.4 (some scanning/defang support broken)
* Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
- Fix various SPF bugs
- Recognize dynamic PTR names, and don't count them as authentication.
- Three strikes and yer out rule.
- Block softfail by default unless valid PTR or HELO
- Return unknown for null mechanism
- Return unknown for invalid ip address in mechanism
- Try best guess on HELO also
- Expand setreply for common errors
- make rhsbl.m4 hack available for sendmail.mc
* Sun Aug 22 2004 Stuart Gathman <stuart@bmsi.com> 0.7.1-1
- Handle modifying mislabeled multipart messages without an exception
- Support setbacklog, setmlreply
- allow multi-recipient CBV
- return TEMPFAIL for SPF softfail
* Fri Jul 23 2004 Stuart Gathman <stuart@bmsi.com> 0.7.0-1
- SPF check hello name
- Move pythonsock to /var/run/milter
- Move milter.cfg to /etc/mail/pymilter.cfg
- Check M$ style XML CID records by converting to SPF
- Recognize, but never match ip6 until we properly support it.
- Option to reject when no PTR and no SPF
* Fri Apr 09 2004 Stuart Gathman <stuart@bmsi.com> 0.6.9-1
- Validate spf.py against test suite, and add Received-SPF support to spf.py
- Support best_guess for SPF
- Reject numeric hello names
- Preserve case of local part in sender
- Make libmilter timeout a config option
- Fix setup.py to work with python < 2.2.3
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-3
- Reject invalid SRS immediately for benefit of callback verifiers
- Fix include bug in spf.py
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-2
- Bug in check_header
* Mon Apr 05 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-1
- Don't report spoofed unless rcpt looks like SRS
- Check for bounce with multiple rcpts
- Make dspam see Received-SPF headers
- Make sysv init work with RH9
* Thu Mar 25 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-3
- Forgot to make spf_reject_neutral global in bms.py
* Wed Mar 24 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-2
- Defang message/rfc822 content_type with boundary
- Support SPF delegation
- Reject neutral SPF result for selected domains
* Tue Mar 23 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-1
- SRS forgery check. Detect thread resource starvation.
- Properly remove local socket with explicit type.
- Decode obfuscated subject headers.
* Wed Mar 11 2004 Stuart Gathman <stuart@bmsi.com> 0.6.6-2
- init script bug with python2.3
* Wed Mar 10 2004 Stuart Gathman <stuart@bmsi.com> 0.6.6-1
- SPF checking, hello blacklist
* Mon Mar 08 2004 Stuart Gathman <stuart@bmsi.com> 0.6.5-2
- memory leak in envfrom and envrcpt
* Mon Mar 01 2004 Stuart Gathman <stuart@bmsi.com> 0.6.5-1
- progress notification
- memory leak in connect
- trusted relay
* Thu Feb 19 2004 Stuart Gathman <stuart@bmsi.com> 0.6.4-2
- smart alias wildcard patch, compile for sendmail-8.12
* Thu Dec 04 2003 Stuart Gathman <stuart@bmsi.com> 0.6.4-1
- many fixes for dspam support
* Wed Oct 22 2003 Stuart Gathman <stuart@bmsi.com> 0.6.3
- dspam SCREEN feature
- streamline dspam false positive handling
* Mon Sep 01 2003 Stuart Gathman <stuart@bmsi.com> 0.6.1
- Full dspam support added
* Mon Aug 26 2003 Stuart Gathman <stuart@bmsi.com>
- Use New email module
* Fri Jun 27 2003 Stuart Gathman <stuart@bmsi.com>
- Add dspam module
+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 };
-29
View File
@@ -1,29 +0,0 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: DELIVERY STATUS (POSSIBLE SPAM)
Auto-Submitted: auto-generated (content analysis)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
A statistical analysis of your message has classified it as junk mail,
and it has been quarantined. Eventually, the recipients will review
their quarantined mail and may notice your message. If your message is
important, please contact them via other means. You may also try sending
them a simple plain text message.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
-38
View File
@@ -1,38 +0,0 @@
# Analyze milter log to find abusers
fp = open('/var/log/milter/milter.log','r')
subdict = {}
ipdict = {}
spamcnt = {}
for line in fp:
a = line.split(None,4)
if len(a) < 4: continue
dt,tm,id,op = a[:4]
if op == 'Subject:':
if len(a) > 4: subdict[id] = a[4].rstrip()
elif op == 'connect':
ipdict[id] = a[4].rstrip()
elif op in ('eom','dspam'):
if id in subdict: del subdict[id]
if id in ipdict: del ipdict[id]
elif op in ('REJECT:','DSPAM:','SPAM:','abort'):
if id in subdict:
if id in ipdict:
ip = ipdict[id]
del ipdict[id]
f,host,raw = ip.split(None,2)
if host in spamcnt:
spamcnt[host] += 1
else:
spamcnt[host] = 1
else: ip = ''
print dt,tm,op,a[4].rstrip(),subdict[id]
del subdict[id]
else:
print line.rstrip()
print len(subdict),'leftover entries'
spamlist = filter(lambda x: x[1] > 1,spamcnt.items())
spamlist.sort(lambda x,y: x[1] - y[1])
for ip,cnt in spamlist:
print cnt,ip
-138
View File
@@ -1,138 +0,0 @@
# Analyze milter log to find abusers
import traceback
import sys
def parse_addr(a):
beg = a.find('<')
end = a.find('>')
if beg >= 0:
if end > beg: return a[beg+1:end]
return a
class Connection(object):
def __init__(self,dt,tm,id,ip=None,conn=None):
self.dt = dt
self.tm = tm
self.id = id
if ip:
_,self.host,self.ip = ip.split(None,2)
elif conn:
self.ip = conn.ip
self.host = conn.host
self.helo = conn.helo
self.subject = None
self.rcpt = []
self.mfrom = None
self.helo = None
self.innoc = []
self.whitelist = False
def connections(fp):
conndict = {}
termdict = {}
for line in fp:
if line.startswith('{'): continue
a = line.split(None,4)
if len(a) < 4: continue
dt,tm,id,op = a[:4]
if (id,op) == ('bms','milter'):
# FIXME: optionally yield all partial connections in conndict
conndict = {}
termdict = {}
continue
if id[0] == '[' and id[-1] == ']':
try:
key = int(id[1:-1])
except:
print >>sys.stderr,'bad id:',line.rstrip()
continue
else: continue
if op == 'connect':
ip = a[4].rstrip()
conn = Connection(dt,tm,id,ip=ip)
conndict[key] = conn
elif op in (
'DISCARD:','TAG:','CBV:','Large','No',
'NOTE:','From:','Sender:','TRAIN:'):
continue
else:
op = op.lower()
try:
conn = conndict[key]
except KeyError:
try:
conn = termdict[key]
del termdict[key]
conndict[key] = conn
except KeyError:
print >>sys.stderr,'key error:',line.rstrip()
continue
try:
if op == 'subject:':
if len(a) > 4:
conn.subject = a[4].rstrip()
elif op == 'innoc:':
conn.innoc.append(a[4].rstrip())
elif op == 'whitelist':
conn.whitelist = True
elif op == 'x-mailer:':
if len(a) > 4:
conn.mailer = a[4].rstrip()
elif op == 'x-guessed-spf:':
conn.spfguess = a[4]
elif op == 'received-spf:':
conn.spfres,conn.spfmsg = a[4].rstrip().split(None,1)
elif op == 'received:':
conn.received = a[4].rstrip()
elif op == 'temp':
_,conn.tempfile = a[4].rstrip().split(None,1)
elif op == 'srs':
_,conn.srsrcpt = a[4].rstrip().split(None,1)
elif op == 'mail':
_,conn.mfrom = a[4].rstrip().split(None,1)
elif op == 'rcpt':
_,rcpt = a[4].rstrip().split(None,1)
conn.rcpt.append(rcpt)
elif op == 'hello':
_,conn.helo = a[4].rstrip().split(None,1)
elif op in ('eom','dspam','abort'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('reject:','dspam:','tempfail:','reject','fail:','honeypot:'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
conn.resmsg = a[4].rstrip()
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('fp:','spam:'):
del conndict[key]
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
else:
print >>sys.stderr,'unknown op:',line.rstrip()
except Exception:
print >>sys.stderr,'error:',line.rstrip()
traceback.print_exc()
if __name__ == '__main__':
import gzip
for fn in sys.argv[1:]:
if fn.endswith('.gz'):
fp = gzip.open(fn)
else:
fp = open(fn)
for conn in connections(fp):
if conn.rcpt and conn.mfrom:
for r in conn.rcpt:
if r.lower().find('iancarter') > 0: break
else:
if conn.mfrom.lower().find('iancarter') < 0: continue
print >>sys.stderr,conn.result,conn.dt,conn.tm,conn.id,conn.subject,parse_addr(conn.mfrom),
for a in conn.rcpt:
print parse_addr(a),
print
-44
View File
@@ -1,44 +0,0 @@
divert(-1)
#
# Copyright (c) 2002 Derek J. Balling
# All rights reserved.
#
# Permission to use granted for all purposes. If modifications are made
# they are requested to be sent to <dredd@megacity.org> for inclusion in future
# versions
#
# Allows (hopefully) for checking of access.db whitelisting now. This ONLY
# works on sendmail-8.12.x ... use on any other version may require tinkering
# by you the downloader.
#
# Incorporates many changes by Sergey S. Mokryshev <mokr@mokr.net>
#
#
divert(0)
ifdef(`_RHSBL_R_',`dnl',`dnl
VERSIONID(`$Id$')
define(`_RHSBL_R_',`')
ifdef(`_DNSBL_R_',`dnl',`dnl
LOCAL_CONFIG
# map for DNS based blacklist lookups based on the sender RHS
Kdnsbl host -T<TMP>')')
divert(-1)
define(`_RHSBL_SRV_', `_ARG_')dnl
define(`_RHSBL_MSG_', `ifelse(len(X`'_ARG2_),`1',`"550 Mail from " $`'&{RHS} " refused by blackhole site '_RHSBL_SRV_`"',`_ARG2_')')dnl
define(`_RHSBL_MSG_TMP_', `ifelse(_ARG3_,`t',`"451 Temporary lookup failure of " $`'&{RHS} " at '_RHSBL_SRV_`"',`_ARG3_')')dnl
MAILER_DEFINITIONS
SLocal_check_mail
# DNS based RHS spam list blackholes.bmsi.com
R$* $: <?> $>CanonAddr $1
R<?> $*<@$+.> $: <?> $1<@$2.> $| $>SearchList <+ rhs> $| <F:$1@$2> <D:$2> <>
R<?> $* $| <$={Accept}> $: OKSOFAR
R<?> $*<@$+.> $| $* $: <?> $(dnsbl $2._RHSBL_SRV_. $: OK $) $(macro {RHS} $@ $2 $)
R<?> OK $: OKSOFAR
R<?> $*<@$*> $: OKSOFAR
ifelse(len(X`'_ARG3_),`1',
`R<?>$+<TMP> $: TMPOK',
`R<?>$+<TMP> $#error $@ 4.7.1 $: _RHSBL_MSG_TMP_')
R<?>$+ $#error $@ 5.7.1 $: _RHSBL_MSG_
+32 -31
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
@@ -35,9 +37,11 @@ class sampleMilter(Milter.Milter):
# 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.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
@@ -58,23 +62,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:
@@ -84,7 +88,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
@@ -93,12 +97,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()
@@ -121,7 +125,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
@@ -136,19 +140,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):
@@ -169,13 +170,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.4
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
+14 -13
View File
@@ -2,21 +2,22 @@ import os
import sys
from distutils.core import setup, Extension
# FIXME: on some versions of sendmail, smutil is renamed to sm
# on slackware and debian, leave it out entirely. It depends
# on how libmilter was built by the sendmail package.
libs = ["milter", "smutil"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian
if sys.version < '2.6.5':
sys.exit('ERROR: Sorry, python 2.6.5 is required for this module.')
# patch distutils if it can't cope with the "classifiers" or
# "download_url" keywords
if sys.version < '2.2.3':
from distutils.dist import DistributionMetadata
DistributionMetadata.classifiers = None
DistributionMetadata.download_url = None
# FIXME: on some versions of sendmail, smutil is renamed to sm.
# On slackware and debian, leave it out entirely. It depends
# on how libmilter was built by the sendmail package.
#libs = ["milter", "smutil"]
libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian
modules = ["mime"]
if sys.version >= '3':
modules.append("sgmllib")
print("modules=",modules)
# NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "pymilter", version = '0.8.11',
setup(name = "pymilter", version = '1.0.1',
description="Python interface to sendmail milter API",
long_description="""\
This is a python extension module to enable python scripts to
@@ -30,7 +31,7 @@ sending DSNs or doing CBVs.
maintainer_email="stuart@bmsi.com",
license="GPL",
url="http://www.bmsi.com/python/milter.html",
py_modules=["mime"],
py_modules=modules,
packages = ['Milter'],
ext_modules=[
Extension("milter", ["miltermodule.c"],
+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()
-28
View File
@@ -1,28 +0,0 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: SPF %(result)s (POSSIBLE FORGERY)
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy indicated that the above email was likely forged and that
feedback was desired for debugging. If you are sending from a foreign ISP,
then you may need to follow your home ISPs instructions for configuring
your outgoing mail server.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
-20
View File
@@ -1,20 +0,0 @@
[milter]
# The socket used to communicate with sendmail
socketname = /var/run/milter/spfmiltersock
# Name of the milter given to sendmail
name = pyspffilter
# Trusted relays such as secondary MXes that should not have SPF checked.
;trusted_relay =
# Internal networks that should not have SPF checked.
internal_connect = 127.0.0.1,192.168.0.0/16,10.0.0.0/8
# See http://www.openspf.com for more info on SPF.
[spf]
# Use sendmail access map or similar format for detailed spf policy.
# SPF entries in the access map will override defaults.
access_file = /etc/mail/access.db
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
-253
View File
@@ -1,253 +0,0 @@
# A simple SPF milter.
# You must install pyspf for this to work.
# http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2007 Business Management Systems, Inc.
# This code is under GPL. See COPYING for details.
import sys
import Milter
import spf
import syslog
import anydbm
from Milter.config import MilterConfigParser
from Milter.utils import iniplist,parse_addr
syslog.openlog('spfmilter',0,syslog.LOG_MAIL)
class Config(object):
"Hold configuration options."
pass
def read_config(list):
"Return new config object."
cp = MilterConfigParser()
cp.read(list)
if cp.has_option('milter','datadir'):
os.chdir(cp.get('milter','datadir'))
conf = Config()
conf.socketname = cp.getdefault('milter','socketname', '/tmp/spfmiltersock')
conf.miltername = cp.getdefault('milter','name','pyspffilter')
conf.trusted_relay = cp.getlist('milter','trusted_relay')
conf.internal_connect = cp.getlist('milter','internal_connect')
conf.trusted_forwarder = cp.getlist('spf','trusted_relay')
conf.access_file = cp.getdefault('spf','access_file',None)
return conf
class SPFPolicy(object):
"Get SPF policy by result from sendmail style access file."
def __init__(self,sender,access_file=None):
self.sender = sender
self.domain = sender.split('@')[-1].lower()
if access_file:
try: acf = anydbm.open(access_file,'r')
except: acf = None
else: acf = None
self.acf = acf
def getPolicy(self,pfx):
acf = self.acf
if not acf: return None
try:
return acf[pfx + self.sender]
except KeyError:
try:
return acf[pfx + self.domain]
except KeyError:
try:
return acf[pfx]
except KeyError:
return None
class spfMilter(Milter.Milter):
"Milter to check SPF. Each connection gets its own instance."
def log(self,*msg):
syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg])))
def __init__(self):
self.mailfrom = None
self.id = Milter.uniqueID()
# we don't want config used to change during a connection
self.conf = config
# addheader can only be called from eom(). This accumulates added headers
# which can then be applied by alter_headers()
def add_header(self,name,val,idx=-1):
self.new_headers.append((name,val,idx))
self.log('%s: %s' % (name,val))
def connect(self,hostname,unused,hostaddr):
self.internal_connection = False
self.trusted_relay = False
self.hello_name = None
# sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j').strip()
if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0]
if iniplist(ipaddr,self.conf.internal_connect):
self.internal_connection = True
if iniplist(ipaddr,self.conf.trusted_relay):
self.trusted_relay = True
else: ipaddr = ''
self.connectip = ipaddr
if self.internal_connection:
connecttype = 'INTERNAL'
else:
connecttype = 'EXTERNAL'
if self.trusted_relay:
connecttype += ' TRUSTED'
self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype))
return Milter.CONTINUE
def hello(self,hostname):
self.hello_name = hostname
self.log("hello from %s" % hostname)
return Milter.CONTINUE
# multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message.
def envfrom(self,f,*str):
self.log("mail from",f,str)
if not self.hello_name:
self.log('REJECT: missing HELO')
self.setreply('550','5.7.1',"It's polite to say helo first.")
return Milter.REJECT
self.mailfrom = f
self.new_headers = []
t = parse_addr(f)
if len(t) == 2: t[1] = t[1].lower()
self.canon_from = '@'.join(t)
if not (self.internal_connection or self.trusted_relay) and self.connectip:
rc = self.check_spf()
if rc != Milter.CONTINUE: return rc
return Milter.CONTINUE
def envrcpt(self,f,*str):
return Milter.CONTINUE
def header(self,name,hval):
return Milter.CONTINUE
def eoh(self):
return Milter.CONTINUE
def eom(self):
for name,val,idx in self.new_headers:
try:
self.addheader(name,val,idx)
except:
self.addheader(name,val) # older sendmail can't insheader
return Milter.CONTINUE
def close(self):
return Milter.CONTINUE
def check_spf(self):
receiver = self.receiver
for tf in self.conf.trusted_forwarder:
q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False)
res,code,txt = q.check()
if res == 'pass':
self.log("TRUSTED_FORWARDER:",tf)
break
else:
q = spf.query(self.connectip,self.canon_from,self.hello_name,
receiver=receiver,strict=False)
q.set_default_explanation(
'SPF fail: see http://openspf.org/why.html?sender=%s&ip=%s' % (q.s,q.i))
res,code,txt = q.check()
if res not in ('pass','temperror'):
if self.mailfrom != '<>':
# check hello name via spf unless spf pass
h = spf.query(self.connectip,'',self.hello_name,receiver=receiver)
hres,hcode,htxt = h.check()
if hres in ('deny','fail','neutral','softfail'):
self.log('REJECT: hello SPF: %s 550 %s' % (hres,htxt))
self.setreply('550','5.7.1',htxt,
"The hostname given in your MTA's HELO response is not listed",
"as a legitimate MTA in the SPF records for your domain. If you",
"get this bounce, the message was not in fact a forgery, and you",
"should IMMEDIATELY notify your email administrator of the problem."
)
return Milter.REJECT
else:
hres,hcode,htxt = res,code,txt
else: hres = None
p = SPFPolicy(q.s,self.conf.access_file)
if res == 'fail':
policy = p.getPolicy('spf-fail:')
if not policy or policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT
if res == 'softfail':
policy = p.getPolicy('spf-softfail:')
if policy and policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT
elif res == 'permerror':
policy = p.getPolicy('spf-permerror:')
if not policy or policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
# latest SPF draft recommends 5.5.2 instead of 5.7.1
self.setreply(str(code),'5.5.2',txt,
'There is a fatal syntax error in the SPF record for %s' % q.o,
'We cannot accept mail from %s until this is corrected.' % q.o
)
return Milter.REJECT
elif res == 'temperror':
policy = p.getPolicy('spf-temperror:')
if not policy or policy == 'REJECT':
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'4.3.0',txt)
return Milter.TEMPFAIL
elif res == 'neutral' or res == 'none':
policy = p.getPolicy('spf-neutral:')
if policy and policy == 'REJECT':
self.log('REJECT NEUTRAL:',q.s)
self.setreply('550','5.7.1',
"%s requires and SPF PASS to accept mail from %s. [http://openspf.org]"
% (receiver,q.s))
return Milter.REJECT
elif res == 'pass':
policy = p.getPolicy('spf-pass:')
if policy and policy == 'REJECT':
self.log('REJECT PASS:',q.s)
self.setreply('550','5.7.1',
"%s has been blacklisted by %s." % (q.s,receiver))
return Milter.REJECT
self.add_header('Received-SPF',q.get_header(res,receiver),0)
if hres and q.h != q.o:
self.add_header('X-Hello-SPF',hres,0)
return Milter.CONTINUE
if __name__ == "__main__":
Milter.factory = spfMilter
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
global config
config = read_config(['spfmilter.cfg','/etc/mail/spfmilter.cfg'])
miltername = config.miltername
socketname = config.socketname
print """To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=%s
X%s, S=local:%s
See the sendmail README for libmilter.
sample spfmilter startup""" % (miltername,miltername,socketname)
sys.stdout.flush()
Milter.runmilter("pyspffilter",socketname,240)
print "sample spfmilter shutdown"
-85
View File
@@ -1,85 +0,0 @@
#!/bin/bash
#
# spfmilter This shell script takes care of starting and stopping spfmilter.
#
# chkconfig: 2345 80 30
# description: a process that checks SPF for messages sent through sendmail.
# processname: spfmilter
# config: /etc/mail/spfmilter.cfg
# pidfile: /var/run/milter/spfmilter.pid
python="python2.4"
pidof() {
set - ""
if set - `ps -e -o pid,cmd | grep "${python} spfmilter.py"` &&
[ "$2" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
[ -x /usr/lib/pymilter/start.sh ] || exit 0
RETVAL=0
prog="spfmilter"
start() {
# Start daemons.
echo -n "Starting $prog: "
if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /usr/lib/pymilter/start.sh spfmilter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/spfmilter
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc milter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/spfmilter
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/spfmilter ]; then
stop
start
RETVAL=$?
fi
;;
status)
status spfmilter
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
+10 -5
View File
@@ -1,14 +1,19 @@
#!/bin/sh
appname="$1"
script="${2:-${appname}}"
datadir=/var/log/milter
datadir="/var/lib/milter"
logdir="/var/log/milter"
piddir="/var/run/milter"
libdir="/usr/lib/pymilter"
python="python2.4"
exec >>${datadir}/${appname}.log 2>&1
exec >>${logdir}/${appname}.log 2>&1
if test -s ${datadir}/${script}.py; then
cd ${datadir} # use version in log dir if it exists for debugging
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 /usr/lib/pymilter
cd ${libdir}
fi
${python} ${script}.py &
echo $! >/var/run/milter/${appname}.pid
echo $! >${piddir}/${appname}.pid
-69
View File
@@ -1,69 +0,0 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: Critical mail server configuration error
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Someone at IP address %(connectip)s sent an email claiming
to be from %(sender)s.
If that wasn't you, then your domain, %(sender_domain)s,
was forged - i.e. used without your knowlege or authorization by
someone attempting to steal your mail identity. This is a very
serious problem, and you need to provide authentication for your
SMTP (email) servers to prevent criminals from forging your
domain. The simplest step is usually to publish an SPF record
with your Sender Policy.
For more information, see: http://openspf.org
I hate to annoy you with a DSN (Delivery Status
Notification) from a possibly forged email, but since you
have not published a sender policy, there is no other way
of bringing this to your attention.
If it *was* you that sent the email, then your email domain
or configuration is in error. If you don't know anything
about mail servers, then pass this on to your SMTP (mail)
server administrator. We have accepted the email anyway, in
case it is important, but we couldn't find anything about
the mail submitter at %(connectip)s to distinguish it from a
zombie (compromised/infected computer - usually a Windows
PC). There was no PTR record for its IP address (PTR names
that contain the IP address don't count). RFC2821 requires
that your hello name be a FQN (Fully Qualified domain Name,
i.e. at least one dot) that resolves to the IP address of
the mail sender. In addition, just like for PTR, we don't
accept a helo name that contains the IP, since this doesn't
help to identify you. The hello name you used,
%(heloname)s, was invalid.
Furthermore, there was no SPF record for the sending domain
%(sender_domain)s. We even tried to find its IP in any A or
MX records for your domain, but that failed also. We really
should reject mail from anonymous mail clients, but in case
it is important, we are accepting it anyway.
We are sending you this message to alert you to the fact that
Either - Someone is forging your domain.
Or - You have problems with your email configuration.
Or - Possibly both.
If you need further assistance, please do not hesitate to
contact me again.
Kind regards,
postmaster@%(receiver)s
-33
View File
@@ -1,33 +0,0 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: Critical DNS configuration error
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your DNS server is not responding to TXT queries. In other words,
it is BROKEN. You need to get somebody to fix it ASAP. We
are attempting to do TXT queries to see if you have an SPF record.
See http://openspf.org
We are sending you this message to alert you to the fact that
you have problems with your DNS.
If you need further assistance, please do not hesitate to
contact me again.
Kind regards,
postmaster@%(receiver)s
+2 -2
View File
@@ -1,16 +1,16 @@
import unittest
import testbms
import testmime
import testsample
import testutils
import testgrey
import os
def suite():
s = unittest.TestSuite()
s.addTest(testbms.suite())
s.addTest(testmime.suite())
s.addTest(testsample.suite())
s.addTest(testutils.suite())
s.addTest(testgrey.suite())
return s
if __name__ == '__main__':
-710
View File
@@ -1,710 +0,0 @@
From stuart@bmsi.com Wed May 1 14:37:14 2002
Return-Path: <stuart@bmsi.com>
Received: from bmsi.com (IDENT:stuart@localhost [127.0.0.1])
by gathman.bmsi.com (8.11.6/8.11.6) with ESMTP id g41IbCF01796
for <stuart@gathman.bmsi.com>; Wed, 1 May 2002 14:37:13 -0400
Sender: stuart@gathman.bmsi.com
Message-ID: <3CD035D7.18ADF27F@bmsi.com>
Date: Wed, 01 May 2002 14:37:11 -0400
From: "Stuart D. Gathman" <stuart@bmsi.com>
Organization: Business Management Systems, Inc.
X-Mailer: Mozilla 4.78 [en] (X11; U; Linux 2.4.9-21 i586)
X-Accept-Language: en
MIME-Version: 1.0
To: stuart@gathman.bmsi.com
Subject: Amazon.com--Earth's Biggest Selection
Content-Type: multipart/mixed;
boundary="------------59A46341C90BA737DD47867B"
This is a multi-part message in MIME format.
--------------59A46341C90BA737DD47867B
Content-Type: multipart/alternative;
boundary="------------0B098FB91956AC123C61B151"
--------------0B098FB91956AC123C61B151
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065
--
Stuart D. Gathman
Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flamis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.
--------------0B098FB91956AC123C61B151
Content-Type: text/html; charset=us-ascii
Content-Transfer-Encoding: 7bit
<!doctype html public "-//w3c//dtd html 4.0 transitional//en">
<html>
<A HREF="http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065">http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065</A>
<pre>--&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Stuart D. Gathman&nbsp;<stuart@bmsi.com>
Business Management Systems Inc.&nbsp; Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flamis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.</pre>
&nbsp;</html>
--------------0B098FB91956AC123C61B151--
--------------59A46341C90BA737DD47867B
Content-Type: text/html; charset=us-ascii;
name="103-3111065-2579065"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="103-3111065-2579065"
Content-Base: "http://www.amazon.com/exec/obidos/subs
t/home/redirect.html/103-3111065-25
79065"
Content-Location: "http://www.amazon.com/exec/obidos/subs
t/home/redirect.html/103-3111065-25
79065"
<html>
<head>
<title>
Amazon.com--Earth's Biggest Selection
</title>
<meta name="keywords" content="amazon.com,amazon books,amazon,amazon.com books,amazon music,amazon.com music,amazon video,amazon.com video,auctions,amazon auctions,amazon.com auctions,electronics,consumer electronics,gifts,amazon gifts,amazon.com gifts,cards,e-cards,e-mail cards,greeting cards,amazon cards,amazon.com cards,toys,amazon toys,amazon.com toys,games,amazon games,amazon.com games,toys & games,toys and games">
<style type="text/css"><!-- .serif { font-family: times,serif; font-size: medium; }
.sans { font-family: verdana,arial,helvetica,sans-serif; font-size: medium; }
.small { font-family: verdana,arial,helvetica,sans-serif; font-size: small; }
.h1 { font-family: verdana,arial,helvetica,sans-serif; color: #CC6600; font-size: medium; }
.h3color { font-family: verdana,arial,helvetica,sans-serif; color: #CC6600; font-size: small; }
.tiny { font-family: verdana,arial,helvetica,sans-serif; font-size: x-small; }
.listprice { font-family: arial,verdana,helvetica,sans-serif; text-decoration: line-through; font-size: small; }
.price { font-family: verdana,arial,helvetica,sans-serif; color: #990000; font-size: small; }
--></style>
</head>
<body bgcolor="#FFFFFF" link="#003399" alink="#FF9933" vlink="#996633" text="#000000" onLoad="document.searchform.elements[1].focus()">
<a name="top"></a>
<map name="right_top_nav_map">
<area shape="rect" href=/exec/obidos/shopping-basket/ref=top_nav_sb_gateway/103-3111065-2579065 coords="0,0,80,21">
<area shape="rect" href=/exec/obidos/wishlist/ref=cm_wl_topnav_gateway/103-3111065-2579065 coords="85,0,151,21">
<area shape="rect" href=/exec/obidos/account-access-login/ref=top_nav_ya_gateway/103-3111065-2579065 coords="155,0,256,21">
<area shape="rect" href=/exec/obidos/tg/browse/-/508510/ref=top_nav_hp_gateway/103-3111065-2579065 coords="260,0,299,21">
</map>
<map name="gateway_nav_map">
<area shape=rect coords="0,0,124,28" href=/exec/obidos/tg/stores/static/-/gateway/international-gateway/ref=gw_subnav_in/103-3111065-2579065>
<area shape=rect coords="125,0,228,28" href=/exec/obidos/tg/new-for-you/top-sellers/-/main/ref=gw_subnav_ts/103-3111065-2579065>
<area shape=rect coords="229,0,332,28" href=/exec/obidos/tg/browse/-/700060/ref=gw_subnav_target/103-3111065-2579065>
<area shape=rect coords="333,0,450,28" href=/exec/obidos/tg/browse/-/909656/ref=stuffandsubnav_td1_/103-3111065-2579065>
<area shape=rect coords="451,0,580,28" href=/exec/obidos/subst/misc/sell-your-stuff.html/ref=subnav_sys_/103-3111065-2579065>
</map>
<table border=0 width=100% cellspacing=0 cellpadding=0>
<tr><td width=100%>
<center>
<table width=100% border=0 cellspacing=0 cellpadding=0 vspace=0>
<tr>
<td width=25% rowspan=2>&nbsp;</td>
<td align=left valign=bottom><a href=/exec/obidos/subst/home/redirect.html/ref=nh_gateway/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/associates/navbar2000/logo-no-border(1).gif" width=148 height=43 alt="" border=0></a></td>
<td width=10%>&nbsp;</td>
<td align=right>
<img src="http://g-images.amazon.com/images/G/01/nav/personalized/cartwish/right-topnav-default-2.gif" width=300 height=22 alt="" USEMAP=#right_top_nav_map border=0></td>
<td align=right rowspan=2 width=25%>
&nbsp;
</td>
</tr>
<tr valign=bottom>
<td colspan=3 align=center>
<table align=center border=0 cellpadding=0 cellspacing=0><tr valign=bottom>
<td><a href=/exec/obidos/subst/home/home.html/ref%3Dtab%5Fgw%5Fgw%5F1/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/welcome-on-whole.gif" width=60 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/stores/your/store-home/-/0/ref%3Dtab%5Fgw%5Ffr%5F2/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/yourstore-off-sliced._ZCSTUART%27S,0,2,0,0,verdenab,7,90,90,80_.gif" width=81 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/283155/ref%3Dtab%5Fgw%5Fb%5F3/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/books-off-sliced.gif" width=39 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/172282/ref%3Dtab%5Fgw%5Fe%5F4/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/electronics-off-sliced.gif" width=74 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/130/ref%3Dtab%5Fgw%5Fd%5F5/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/dvd-off-sliced.gif" width=35 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/171280/ref%3Dtab%5Fgw%5Ft%5F6/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/toys-off-sliced.gif" width=47 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/468642/ref%3Dtab%5Fgw%5Fvg%5F7/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/videogames-off-sliced.gif" width=73 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/600460/ref%3Dtab%5Fgw%5F%5F8/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/corporate-off-sliced.gif" width=70 height=26 border=0></a></td>
<td><a href=/exec/obidos/subst/home/all-stores.html/ref%3Dtab_gw_storesdirectory/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/see-more-off-sliced.gif" width=70 height=26 border=0></a></td>
</tr></table>
</td>
</tr>
</table>
</center>
</td></tr>
<tr align=center bgcolor=#006699>
<td><img src="http://g-images.amazon.com/images/G/01/nav/amazon/gateway/blue/gateway-subnav-default.gif" width=580 height=28 width=580 height=28 alt="" USEMAP="#gateway_nav_map" border=0></td>
</tr>
<tr>
<td bgcolor=#ffffdd align=center class=small>
<font face=verdana,arial,helvetica size=-1>
<font color="#CC6600"><B>Hello, Stuart D. Gathman.</B></font>
We have <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/subst%2Frecs%2Finstant-recs-home.html%2Fref%3Dpd_ir_gw_r/ref=ilm_stripe_272005/103-3111065-2579065&message=272005,m1,26">recommendations</A> for you.
</font><font face=verdana,arial,helvetica size=-2>
(If you're not Stuart D. Gathman, <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/flex-sign-in%2Fref%3Dpd_ir_gw_r%2F%3Fopt%3Doa%26page%3Drecs%2Fsign-in-secure.html%26response%3Dtg%2Frecs%2Frecs-post-login-dispatch%2F-%2Frecs%2Fpd_rw_gw_r/ref=ilm_stripe_272005/103-3111065-2579065&message=272005,m1,26">click here</A>.)
</font>
</td>
</tr>
</table>
<br>
<table width=100% cellpadding=0 cellspacing=0 border=0>
<tr valign=top>
<td width=174>
<TABLE border=0 cellspacing=0 cellpadding=0><TR valign=bottom align=center>
<td><img src="http://g-images.amazon.com/images/G/01/v9/search-browse/search-gateway.gif" width=171 height=19 border=0 alt="Search Amazon.com"></td>
</TR> <TR valign=top align=center><TD> <TABLE border=0 width= 171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#FFCC66 valign=top width=100%>
<form method="post" action="/exec/obidos/search-handle-form/103-3111065-2579065" name="searchform">
<select name=index>
<option value=blended selected>All Products
<option value=books>Books
<option value=music>Popular Music
<option value=music-dd>Music Downloads
<option value=classical>Classical Music
<option value="dvd">DVD
<option value="vhs">VHS
<option value=theatrical>Movie Showtimes
<option value=toys>Toys
<option value=baby>Baby
<option value=pc-hardware>Computers
<option value=videogames>Video Games
<option value=electronics>Electronics
<option value=photo>Camera &amp; Photo
<option value=software>Software
<option value=tools>Tools &amp; Hardware
<option value=magazines>Magazines
<option value=garden>Outdoor Living
<option value=kitchen>Kitchen
<option value=travel>Travel
<option value=wireless-phones>Cell Phones & Service
<option value=outlet>Outlet
<option value=auction-redirect>Auctions
<option value=fixed-price-redirect>zShops
</select>
<input type="text" name="field-keywords" size="15">
<input type="image" height="21" width="21" border=0 value="Go" name="Go" src="http://g-images.amazon.com/images/G/01/v9/search-browse/go-button-gateway.gif" align=absmiddle>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> </TD> </form>
</TR> </TABLE> <br clear=left>
<TABLE border=0 cellspacing=0 cellpadding=0>
<TR valign=bottom align=center>
<td><img src="http://g-images.amazon.com/images/G/01/v9/search-browse/browse-gateway.gif" width=171 height=19 border=0 alt="Browse Amazon.com"></td>
</TR> <TR valign=top align=center>
<TD> <TABLE border=0 width= 171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#ffffff valign=top width=100%>
<table cellpadding=3 cellspacing=0>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/283155/ref=gw_br_bo/103-3111065-2579065">Books</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/172282/ref=gw_br_el/103-3111065-2579065">Electronics</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/540744/ref=gw_br_ba/103-3111065-2579065">Baby &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/540744/ref=gw_br_ba/103-3111065-2579065">Baby Registry</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/5174/ref=gw_br_mu/103-3111065-2579065">Music</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/redirect-to-partner/ref=gw_br_dscm/103-3111065-2579065?name=dscm&aid=2&aparam=tb5270_bhp&trx=8056">Health & Beauty</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/130/ref=gw_br_dvd/103-3111065-2579065">DVD</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/229534/ref=gw_br_sw/103-3111065-2579065">Software</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/284507/ref=gw_br_ki/103-3111065-2579065">Kitchen &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/284507/ref=gw_br_ki/103-3111065-2579065">Housewares</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/228013/ref=gw_br_hi/103-3111065-2579065">Tools &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/228013/ref=gw_br_hi/103-3111065-2579065">Hardware</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/541966/ref=gw_br_pc/103-3111065-2579065">Computers</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/502394/ref=gw_br_p/103-3111065-2579065">Camera & Photo</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/562436/ref=gw_br_th/103-3111065-2579065">Movie Showtimes</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/468642/ref=gw_br_cvg/103-3111065-2579065">Computer &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/468642/ref=gw_br_cvg/103-3111065-2579065">Video Games</a></b></td>
</tr> <tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/171280/ref=gw_br_tg/103-3111065-2579065">Toys &amp; Games</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/301185/ref=gw_br_wi/103-3111065-2579065">Cell Phones</a><br>&nbsp;&nbsp;&nbsp;<a href="/exec/obidos/tg/browse/-/301185/ref=gw_br_wi/103-3111065-2579065">& Service</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/404272/ref=gw_br_vi/103-3111065-2579065">Video</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/599858/ref=gw_br_zi/103-3111065-2579065">Magazine</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/599858/ref=gw_br_zi/103-3111065-2579065">Subscriptions</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/286168/ref=gw_br_lp/103-3111065-2579065">Outdoor Living</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/605012/ref=gw_br_tr/103-3111065-2579065">Travel</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/acn-redirect-to-partner/ref=gw_br_cars/103-3111065-2579065?partner-name=carsdirect&partner-url=home%3Fpartner%3Damzn%26customerid%3Dbrowse">Cars</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/229220/ref=gw_br_gi/103-3111065-2579065">Gifts &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/229220/ref=gw_br_gi/103-3111065-2579065">Gift Certificates</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="http://s1.amazon.com/exec/varzea/subst/home/home.html/ref=gw_br_au/103-3111065-2579065">Auctions</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="http://s1.amazon.com/exec/varzea/subst/home/fixed.html/ref=gw_br_zs/103-3111065-2579065">zShops</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/517808/ref=gw_br_ou/103-3111065-2579065">Outlet</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/600460/ref=gw_br_cb/103-3111065-2579065">Corporate</a> <br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/600460/ref=gw_br_cb/103-3111065-2579065">Accounts</a></b></td>
</tr>
<tr>
<td class=small>
<a href="/exec/obidos/flex-sign-in/ref=pd_fr_gw_fav_edt/103-3111065-2579065?page=personalization/favorites/favorites-sign-in-secure.html&response=favorites-edit/personalization/favorites/edit-areas.html&pass_through=product-group-id.gateway.hp&method=GET">
<img src="http://g-images.amazon.com/images/G/01/buttons/edit-favorites.gif" width=69 height=15 border=0 valign=top vspace=2></a><br>
</td>
</tr>
<tr>
<td class=small><b>Browse Partners</b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<a href="/exec/obidos/tg/browse/-/700060/ref=gw_tarb_/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/target/target-logo-sm.gif" width=71 height=17 border=0 alt=Target></a></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<a href="/exec/obidos/tg/browse/-/171280/ref=gw_trub_/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/toys/navigation/tru-logo.gif" width=117 height=14 border=0 alt=Toysrus.com></a></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<a href="/exec/obidos/tg/browse/-/540744/ref=gw_brub_/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/toys/navigation/bru-logo.gif" width=136 height=15 border=0 alt=Babiesrus.com></a></td>
</tr>
</table>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> </TD>
</TR>
</TABLE> <br>
<TABLE border=0 width=171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#ffffff valign=top width=100%>
<font face=verdana,arial,helvetica color=#000000 size=-1><b>Special Features</b></font><br>
<font face=verdana,arial,helvetica size=-1>
<ul><li> <A href="/exec/obidos/subst/alerts/signup.html/ref=gw_hp_ls_1_1/103-3111065-2579065">Alerts</A><li> <A href="/exec/obidos/subst/misc/anywhere/anywhere.html/ref=gw_hp_ls_1_2/103-3111065-2579065">Amazon.com
Anywhere</A><li> <A href="/exec/obidos/subst/misc/amazon-credit/marketing-page.html/ref=gw_hp_ls_1_3/103-3111065-2579065">Amazon Credit Account</A><li> <A href="/exec/obidos/subst/delivers/delivers-signup-combo.html/ref=gw_hp_ls_1_4/103-3111065-2579065">Delivers</A><li><A href="/exec/obidos/tg/browse/-/225840/ref=gw_hp_ls_1_5/103-3111065-2579065">Free e-Cards</A><li><A href="/exec/obidos/subst/community/community-home.html/ref=gw_hp_ls_1_6/103-3111065-2579065">Friends &amp; Favorites</A><li> <A href="/exec/obidos/subst/gifts/gift-services/gift-certificates.html/ref=gw_hp_ls_1_7/103-3111065-2579065">Gift
Certificates</A><li> <A href="http://auctions.amazon.com/exec/varzea/subst/fx/home.html/ref=gw_hp_ls_1_8/103-3111065-2579065">Honor
System</A><li> <A href="/exec/obidos/subst/community/community.html/ref=gw_hp_ls_1_9/103-3111065-2579065">Purchase
Circles</A><li>
<A href="/exec/obidos/tg/browse/-/885446/ref=gw_hp_ls_1_10/103-3111065-2579065">Wedding
Registry</A></ul>
</font>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> <br>
<TABLE border=0 width=171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#ffffff valign=top width=100%>
<font face=verdana,arial,helvetica color=#000000 size=-1><b>Associates</b></font><br>
<font face=verdana,arial,helvetica size=-1>
Sell books, music, videos, and more from your
Web site. <A href="/exec/obidos/subst/associates/join/associates.html/ref=gw_hp_ls_2_1/103-3111065-2579065">Start earning
today</A>!<BR>
</font>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> <br>
<p>
<br clear=all>
</td>
<td>&nbsp;</td>
<td>
<center>
</center>
<br clear=all><p>
<A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/tg/browse/-/283155/ref=ilm_rc_285799/103-3111065-2579065&message=285799,m1,27">
<center><img src="http://g-images.amazon.com/images/G/01/books/homepage-pricing/books-home-pricing-iii.gif" width=257 height=99 border=0></center>
</A>
<br clear=all><br>
<A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/tg/browse/-/753570/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28"><img src="http://g-images.amazon.com/images/G/01/icons/thumbnails/b00003cwt6_thumb.gif" width=41 height=60 border=0 valign=top align=left></A>
Pre-order the Oscar&#174;-winning blockbuster <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/tg/browse/-/753570/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28"><I>The Lord of the Rings: The Fellowship of the Ring</I></A>, arriving on <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/ASIN/B00003CWT6/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28">DVD</A> and <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/ASIN/B000065U6Q/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28">video</A> August 6.
<br clear=all><br>
<b class=small><A href="/exec/obidos/tg/browse/-/229220/ref=gw_hp_cs_1_1/103-3111065-2579065">In Gifts</A></b><br>
<A href="/exec/obidos/tg/browse/-/229220/ref=gw_hp_cs_2_1/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/marketing/mothers_day/md_sd_roto.jpg" width=100 height=95 border=0 align=left hspace=4></A>
<b><font face=verdana,arial,helvetica color=#CC6600>Mother's Day Is May 12</font></b><br>
We've made it fun and easy to buy the perfect
present for Mom. Shop by <A href="/exec/obidos/tg/stores/recs/gift-wizard-refine/-/holiday/ref=gw_hp_cs_2_2/103-3111065-2579065">recipient</A>
or <A href="/exec/obidos/tg/stores/recs/gift-wizard/-/price/ref=gw_hp_cs_2_3/103-3111065-2579065">price</A>,
browse <A href="/exec/obidos/tg/stores/recs/gift-wizard/-/topsellers/ref=gw_hp_cs_2_4/103-3111065-2579065">top
sellers</A>, or order <A href="http://www.amazon.com/exec/obidos/redirect-to-external-url/103-3111065-2579065?path=http%3A//www.proflowers.com/freechocolate/freechocolate.cfm%3FREF%3DFCHAmazonGatewayExp042702">flowers</A>.
Visit <A href="/exec/obidos/tg/browse/-/229220/ref=gw_hp_cs_2_5/103-3111065-2579065">Gifts</A> for
these and more great ideas for expressing your love and
appreciation.<BR>
&nbsp;<br clear=left>
<br clear=all>
<a href=/exec/obidos/instant-recs/recs/instant-recs-home.html/ref=pd_gw_qpt_h/103-3111065-2579065><b class=small>Your Recommendations</b></a>
<br> <b class=h1>
<i>War in Heaven</i>
</b>
</b><br>
<a href=/exec/obidos/ASIN/0802812198/ref=pd_gw_qpt_1/103-3111065-2579065><img src="http://images.amazon.com/images/P/0802812198.01.__PE20_PIm.arrow,TopLeft,-2,-19_SCTZZZZZZZ_.jpg" width=76 height=116 vspace=3 hspace=7 align=left border=0></a>
<b>Amazon.com</b><br>
"The telephone was ringing wildly," begins Charles Williams's novel <I>War in Heaven</I>, "but without result, since there was no-one in the room but the corpse." From this abrupt--and darkly humorous--start, Williams takes us on a 20th-century version of the Grail quest, with an Archdeacon, a Duke, and an...
<a href=/exec/obidos/ASIN/0802812198/ref=pd_gw_qpt_1/103-3111065-2579065>
<font size=-1>Read more</font></a>
<span class=tiny>
&#124;
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/0802812198/gw/1/pc/3/none/ref=pd_gw_qpt_1/103-3111065-2579065>Why was I recommended this?</a>&#041;
</span>
<br clear=all>
<br><b class=small>More Recommendations</b><br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/0471070408/ref=pd_gw_qpt_2/103-3111065-2579065><i>Reliable Linux</i></a> by Iain Campbell
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/0471070408/gw/1/pc/3/none/ref=pd_gw_qpt_2/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/1565926102/ref=pd_gw_qpt_3/103-3111065-2579065><i>Programming PHP</i></a> by Rasmus Lerdorf, et al
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/1565926102/gw/1/pc/3/none/ref=pd_gw_qpt_3/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/0802812201/ref=pd_gw_qpt_4/103-3111065-2579065><i>Descent into Hell</i></a> by Charles W. Williams
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/0802812201/gw/1/pc/3/none/ref=pd_gw_qpt_4/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/059600186X/ref=pd_gw_qpt_5/103-3111065-2579065><i>Network Troubleshooting Tools (O'Reilly System Administration)</i></a> by Joseph D. Sloan
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/059600186X/gw/1/pc/3/none/ref=pd_gw_qpt_5/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<p>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/tg/stores/your/favorites/-/music/ref=pd_fr_gw_nr_h/103-3111065-2579065><b>Your Music Store</b></a></font><br>
<font face=verdana,arial,helvetica color=#CC6600><b>
Isaac Freeman, et al&#44;
<i>Beautiful Stars</i>
</b></font>
<br>
<a href=/exec/obidos/ASIN/B000063TQV/ref=pd_fr_qw_nr_1/103-3111065-2579065><img src="http://images.amazon.com/images/P/B000063TQV.01.26TLZZZZ.jpg" width=73 height=71 vspace=3 hspace=7 align=left border=0></a>
Great African American gospel music has an indisputable power, rooted in the audible faith of its performers and the beauty of their voices. As the bass singer of the <a href="/exec/obidos/tg/stores/artist/glance/-/73920/103-3111065-2579065">Fairfield Four</a>, an a cappella group that started more than a half century ago,...
<a href=/exec/obidos/ASIN/B000063TQV/ref=pd_fr_qw_nr_1/103-3111065-2579065><font size=-1>Read more</font></a>
<br>
<br clear=left>
<br>
<table border=0 cellpadding=2 cellspacing=0><tr><td colspan=2>
<p><b class="small">More Stores:</b>
</td></tr>
<tr valign=top><td width=1%><a href=/exec/obidos/tg/stores/your/favorites/-/electronics/ref=pd_fr_qw_nr_2/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-electronics-icon.gif" width=18 height=18 alt=Icon border=0 align=absmiddle></a></td><td><b class="small"><a href=/exec/obidos/tg/stores/your/favorites/-/electronics/ref=pd_fr_gw_nr_2_p/103-3111065-2579065>Your Electronics Store</a>:</b> <a href=/exec/obidos/ASIN/B000063574/ref=pd_fr_gw_nr_2/103-3111065-2579065>iRiver SlimX iMP-350 CD/MP3 Player with 8 minutes ASP and Upgradeable Firmware</a>
by iRiver
</td></tr>
<tr valign=top><td width=1%><a href=/exec/obidos/tg/stores/your/favorites/-/video/ref=pd_fr_qw_nr_3/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-video-icon.gif" width=18 height=18 alt=Icon border=0 align=absmiddle></a></td><td><b class="small"><a href=/exec/obidos/tg/stores/your/favorites/-/video/ref=pd_fr_gw_nr_3_p/103-3111065-2579065>Your Video Store</a>:</b> <a href=/exec/obidos/ASIN/B000062XNA/ref=pd_fr_gw_nr_3/103-3111065-2579065><i>Ocean's Eleven</i></a>
<b>VHS</b> ~ George Clooney
</td></tr>
</table>
<p>
<b><font face=verdana,arial,helvetica color=#CC6600>Listmania!</font></b><br>
<font face=verdana,arial,helvetica size=-2>
(<a href=/exec/obidos/tg/browse/-/542566/103-3111065-2579065>What is this?</a>)
</font><br>
<table width=100% border=0 cellpadding=5 cellspacing=0>
<tr valign=top>
<td width=50% class=small>
<a href=/exec/obidos/tg/listmania/list-browse/-/2RKS17C9X4D3F/ref=pd_gw_lmq_1/103-3111065-2579065><img src="http://images.amazon.com/images/P/0072127732.01.__PIm.arrow,TopLeft,-2,-19_SCTZZZZZZZ_.jpg" width=76 height=109 border=0 vspace=4 hspace=5></a>
<p>
<font face=verdana,arial,helvetica size=-1>
<a href=/exec/obidos/tg/listmania/list-browse/-/2RKS17C9X4D3F/ref=pd_gw_lmq_1/103-3111065-2579065><b>Best Linux Security books</b></a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A3362WVVMJ3LE9/ref=pd_gw_lmq_n1/103-3111065-2579065>J. Parker</a>, Administrator, hacker.<br>
(7 item list)</font>
</td>
<td width=50% class=small>
<a href=/exec/obidos/tg/listmania/list-browse/-/2B0DIAPG2D3RT/ref=pd_gw_lmq_2/103-3111065-2579065><img src="http://images.amazon.com/images/P/0070419531.01.__PIm.arrow,TopLeft,-2,-19_SCTZZZZZZZ_.jpg" width=76 height=109 border=0 vspace=4 hspace=5></a>
<p>
<font face=verdana,arial,helvetica size=-1>
<a href=/exec/obidos/tg/listmania/list-browse/-/2B0DIAPG2D3RT/ref=pd_gw_lmq_2/103-3111065-2579065><b>Networking</b></a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/AJINE650CAMUQ/ref=pd_gw_lmq_n2/103-3111065-2579065>gakis</a>, Engineer<br>
(13 item list)</font>
</td>
</tr>
<tr>
<td colspan=2 class=small><ul>
<li><a href=/exec/obidos/tg/listmania/list-browse/-/IEF1DNVKZO8B/ref=pd_gw_lmq_3/103-3111065-2579065>My Coder Library</a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A3RK9LZQKL2YIN/ref=pd_gw_lmq_n3/103-3111065-2579065>John Washam</a><br> <li><a href=/exec/obidos/tg/listmania/list-browse/-/LE6A7H4L7VZK/ref=pd_gw_lmq_4/103-3111065-2579065>ALL THE FANTASY YOU'LL EVER NE</a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A3628L43ZVEMP5/ref=pd_gw_lmq_n4/103-3111065-2579065>aramis</a><br> <li><a href=/exec/obidos/tg/listmania/list-browse/-/1MD5H6RUOIMIU/ref=pd_gw_lmq_5/103-3111065-2579065>Mythopoeic Fantasy</a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A7CSNW9E46NR5/ref=pd_gw_lmq_n5/103-3111065-2579065>Vera Nazarian</a><br> </ul></td></tr></table>
<p>
<b class=small><A href="/exec/obidos/tg/browse/-/605012/ref=gw_hp_cb_1_1/103-3111065-2579065">In Travel</A></b><br>
<A href="/exec/obidos/tg/browse/-/605012/ref=gw_hp_cb_2_1/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/travel/promotions/travel-gateway1.gif" width=100 height=95 border=0 align=left hspace=4></A>
<b><font face=verdana,arial,helvetica color=#CC6600>Your Next Vacation Starts
Here</font></b><br>
Save up to 70% on hotels from Vegas to New York
and everywhere in between on <A href="/exec/obidos/acn-redirect-to-partner/103-3111065-2579065?partner-name=expedia&partner-url=pubspec/scripts/eap.asp%3FEAPID%3D11420-1%26GOTO%3DDAILY%26Page%3D/deals/hoteldeals.asp%3Frfrr%3D-2980">Expedia.com</A>.
Book a flight during Hotwire's <A href="/exec/obidos/acn-redirect-to-partner/103-3111065-2579065?partner-name=hotwire&partner-url=index.jsp%3Fsid%3D39151%26bid%3DB627">major-airline Spring Sale</A> through May 2 and fly the
big-name airlines at no-name airline prices. <A href="/exec/obidos/acn-redirect-to-partner/103-3111065-2579065?partner-name=thevacationstore&partner-url=cruises/show_cruise.asp%3Fd%3D%26i%3D743065%26c%3D24%26v%3D110">The
Vacation Store</A> is offering seven-day Holland America
Caribbean cruises from just $599. <BR>
&nbsp;<br clear=left>
<td width=174>
<table width=100% cellpadding=3 cellspacing=0 border=0>
<tr>
<td>
<a href=/exec/obidos/subst/xs/hotpicks.html/ref=xs_ie_13_gw/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/marketing/cross-shop/web-labs/lp_gate_roto_t._ZCStuart%5c,,3,5,300,300,verdenab,14,204,0,0_SCLZZZZZZZ_.gif" width=174 height=34 border=0></a><br>
<a href=/exec/obidos/subst/xs/hotpicks.html/ref=xs_ie_13_gw/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/marketing/cross-shop/web-labs/lp_gate_roto_m.gif" width=174 height=200 border=0></a><br>
<a href=/exec/obidos/subst/xs/hotpicks.html/ref=xs_ie_13_gw/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/marketing/cross-shop/web-labs/lp_gate_roto_b.gif" width=174 height=231 border=0></a><br>
<a href=/exec/obidos/tg/new-for-you/new-for-you/-/main/ref=pd_nfy_gw_n/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/banners/n4u/n4u-header-recognized-01.gif" width=174 height=41 hspace=0 vspace=0 align=right border=0 alt="New For You"></a><br clear=all>
<table border=0 bgcolor=#708090 cellpadding=1 cellspacing=0 width=174 align=right valign=top vspace=0 hspace=0><tr><td>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff>
<tr><td bgcolor=#ffffff align=middle>
<span class=small><font color=#CC6600><b>Stuart,</b></font> check out what's<b> <a href=/exec/obidos/tg/new-for-you/new-for-you/-/main/ref=pd_nfy_gw_n/103-3111065-2579065>New for You</a></b>:<br></span>
</td></tr>
<tr><td bgcolor=#ffffff align=middle>
<span class=tiny>(If you're not Stuart D. Gathman, <a href=/exec/obidos/flex-sign-in/ref=pd_nfy_gw_n/103-3111065-2579065?opt=o&page=misc/login/flex-sign-in-secure.html&response=tg/new-for-you/new-for-you/-/main>click here</a>.)</span>
<br><br>
</td></tr>
<tr bgcolor=#eeeecc><td>
<a href=/exec/obidos/tg/new-for-you/inbox/inbox/-/main/ref=pd_nfy_gw_ibx/103-3111065-2579065><b class=small>Your Message Center</b></a>
</td></tr>
<tr bgcolor=#ffffee><td>
<table><tr bgcolor=#ffffee>
<td valign=top><a href=/exec/obidos/tg/new-for-you/inbox/inbox/-/main/ref=pd_nfy_gw_ibx/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/exclamation-clear.gif" width=20 height=20 border=0 alt=!></a></td>
<td class=small> You have <a href=/exec/obidos/tg/new-for-you/inbox/inbox/-/main/ref=pd_nfy_gw_ibx/103-3111065-2579065>5 new messages</a>.
<br><br>
</td>
</tr></table>
</td></tr>
<tr bgcolor=#eeeecc><td>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/shopping-basket/ref=pd_nfy_gw_sc/103-3111065-2579065><b>Your Shopping Cart</b></a></font>
</td></tr>
<tr><td>
<table><tr>
<td valign=top><a href=/exec/obidos/shopping-basket/ref=pd_nfy_gw_sc/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/shopping-cart-small.gif" width=25 height=25 border=0 alt="Shopping Cart" align=left></a></td>
<td valign=top><font face=verdana,arial,helvetica size=-1>You have 0 items in <a href=/exec/obidos/shopping-basket/ref=pd_nfy_gw_sc/103-3111065-2579065>your Shopping Cart</a>.</font><br><br></td>
</tr></table>
</td></tr></table>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff vspace=0>
<tr bgcolor=#eeeecc><td class=small>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/main/ref=pd_nfy_gw_n/103-3111065-2579065><b>Your New Releases</b></a>
</td></tr></table>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff vspace=0>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/37/ref=pd_nfy_gw_n1/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-music-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/37/ref=pd_nfy_gw_n1/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Pop</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/173429/ref=pd_nfy_gw_n2/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-music-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/173429/ref=pd_nfy_gw_n2/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Christian & Gospel</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/books/5/ref=pd_nfy_gw_n3/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/books/5/ref=pd_nfy_gw_n3/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Computers & Internet</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/kitchen/289814/ref=pd_nfy_gw_n4/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/icon-kitchen-blue.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/kitchen/289814/ref=pd_nfy_gw_n4/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Cookware</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/video/141/ref=pd_nfy_gw_n5/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-vhs-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/video/141/ref=pd_nfy_gw_n5/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Action & Adventure</font></a>
</td></tr>
</td></tr>
<tr><td colspan=2 align=left> <img src="http://g-images.amazon.com/images/G/01/icons/orange-arrow.gif" width=10 height=9 border=0> <a href=/exec/obidos/tg/new-for-you/new-releases/-/main/ref=pd_nfy_gw_n/103-3111065-2579065><font face=verdana,arial,helvetica size=-1><b>More New Releases</b></font></a><p>
</td></tr></table>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff>
<tr bgcolor=#eeeecc><td class=small>
<a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/books/ref=pd_gw_msgr/103-3111065-2579065><b>Movers &amp; Shakers</b></a>
</td></tr></table>
<table border=0 cellpadding=2 cellspacing=0 width=100% bgcolor=#ffffff vspace=0>
<tr><td valign=top align=center>
<img src="http://g-images.amazon.com/images/G/01/icons/uparrow_green2.gif" width=13 height=11 alt="Up">
</td>
<td valign=top>
<font color=#339900 face=verdana,arial,helvetica size=-1><b>974%</b></font> </td></tr>
<tr><td valign=top align=left>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-dvd-icon.gif" width=18 height=18 border=0 alt=Icon >
</td>
<td valign=top>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/dvd/ref=pd_gw_msd2/103-3111065-2579065>Dorothy L. Sayers Mysteries (Strong Poison / Have His Carcass / Gaudy Night)</a>
<font face=verdana,arial,helvetica size=-1>
<b>DVD</b>
<br>~ Dorothy L. Sayers
</font>
</font>
</td></tr>
<tr><td valign=top align=center>
<img src="http://g-images.amazon.com/images/G/01/icons/uparrow_green2.gif" width=13 height=11 alt="Up">
</td>
<td valign=top>
<font color=#339900 face=verdana,arial,helvetica size=-1><b>2,415%</b></font> </td></tr>
<tr><td valign=top align=left>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
</td>
<td valign=top>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/books/ref=pd_gw_msb2/103-3111065-2579065>Artemis Fowl</a>
<br><font face=verdana,arial,helvetica size=-1>by Eoin Colfer</font>
</font>
</td></tr>
<tr><td colspan=2>
<img src="http://g-images.amazon.com/images/G/01/icons/orange-arrow.gif" width=10 height=9 border=0> <font face=verdana,arial,helvetica size=-1><b><a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/books/ref=pd_gw_msgr/103-3111065-2579065>More Movers & Shakers</a></b>
<br>
</td></tr></table>
</td></tr></table>
</td></tr></table>
</td></tr></table>
<br clear="all">
<center>
<form method="post" action="/exec/obidos/search-handle-form/103-3111065-2579065">
<table border=0 width=100% cellpadding=1 cellspacing=0 bgcolor=#999999>
<tr><td>
<table border=0 width=100% bgcolor=#ffffff cellspacing=0 cellpadding=5 class="small">
<tr valign=top><td width=33% class="small">
<b>Where's My Stuff?</b><br>
&#149; Track your <a href="/exec/obidos/flex-sign-in/ref=hy_f_1/103-3111065-2579065?opt=ab&page=help/ya-sign-in-secure.html&response=order-history-filtered&method=POST&ss-order-filter=wheres-my-stuff&return-url=order-history-filtered">recent orders</a>.<br>
&#149; View or change your orders in <a href="/exec/obidos/account-access-login/ref=hy_f_2/103-3111065-2579065">Your Account</a>.
<script language="JavaScript1.1" type="text/javascript">
<!--
var agt=navigator.userAgent.toLowerCase();
var is_major = parseInt(navigator.appVersion);
var is_nav = ((agt.indexOf('mozilla')!=-1) && (agt.indexOf('spoofer')==-1)
&& (agt.indexOf('compatible') == -1) && (agt.indexOf('opera')==-1)
&& (agt.indexOf('webtv')==-1) && (agt.indexOf('hotjava')==-1));
var is_gecko = (agt.indexOf('gecko') != -1);
var is_ie = ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1));
var is_aol = (agt.indexOf("aol") != -1);
var is_opera = (agt.indexOf("opera") != -1);
var is_win = ( (agt.indexOf("win")!=-1) || (agt.indexOf("16bit")!=-1) );
//-->
</script>
<script language="JavaScript1.1" type="text/javascript">
<!--
var OpenedWin;
function openWin (URL, width, height) {
OpenedWin = window.open(URL, "demo_window", "width="+width+",height="+height+",status=no,menubar=no,location=no,toolbar=no,directories=no,scrollbars=no");
if (! is_aol) {
var NewX = (screen.availWidth/2)-(width/2);
var NewY = (screen.availHeight/2)-(height/2);
OpenedWin.moveTo(NewX, NewY);
NewX = null;
NewY = null;
}
}
function launch (URL, width, height) {
if (!URL || !width || !height) {
alert("Error");
} else if (width>screen.availWidth || height>screen.availHeight) {
var message;
message = "Your screen resolution is too low to display the demo.\nClick 'OK' if you wish to continue anyway.\n";
message += '\n Your screen resolution: '+screen.width+' x '+screen.height;
message += ' | Viewable: '+screen.availWidth+' x '+screen.availHeight;
message += '\n Required: '+width+' x '+height;
if (confirm(message)) {
message = "If you can not find the close buttons, use your keyboard:\n";
message += 'Windows: ALT+F4\n';
message += 'Macintosh: CONTROL+W';
alert(message);
openWin(URL, width, height);
}
} else {
openWin(URL, width, height);
}
}
function displayLink(text){
if ( is_major >= 4 && is_win && ( is_nav || is_ie || is_opera || is_gecko ) ) {
document.write(text);
};
}
//-->
</script>
<script language="JavaScript1.1" type="text/javascript">
<!--
displayLink('<br>&#149; See our <b><a href=javascript:launch(\'/exec/obidos/subst/help/demo-wms/display-demo.html/ref=hy_f_demo/103-3111065-2579065\',788,444)>animated demo</a></b>!');
//-->
</script>
</td>
<td width=33% class="small">
<b>Shipping &amp; Returns</b><br>
&#149; See our <a href="/exec/obidos/tg/browse/-/468520/ref=hy_f_3/103-3111065-2579065">shipping rates &amp; policies</a>.<br>
&#149; <a href="/exec/obidos/subst/help/self-service-returns.html/ref=hy_f_4/103-3111065-2579065">Return</a> an item (here's our <a href="/exec/obidos/tg/browse/-/468532/103-3111065-2579065">Returns Policy</a>).
</td>
<td width=33% class="small">
<b>Need Help?</b><br>
&#149; Forgot your password? <a href="/exec/obidos/self-service-forgot-password-get-email/ref=hy_f_6/103-3111065-2579065">Click here</a>.
<br>
&#149; <a href="/exec/obidos/subst/gifts/gift-certificates/gc-redeeming.html/ref=hy_f_7/103-3111065-2579065">Redeem</a> or <a href="/exec/obidos/subst/gifts/gift-services/gift-certificates.html/ref=hy_f_8/103-3111065-2579065">buy</a> a gift certificate.<br>
&#149; <a href="/exec/obidos/tg/browse/-/508510/ref=hy_f_9/103-3111065-2579065">Visit our Help department</a>. <br>
</td></tr>
</table>
</td></tr>
<tr><td>
<table border=0 width=100% bgcolor=#FFCC66 cellspacing=0 cellpadding=5>
<tr><td align=center class="small">
<b>Search&nbsp;</b>
<select name=index>
<option value=blended selected>All Products
<option value=books>Books
<option value=music>Popular Music
<option value=music-dd>Music Downloads
<option value=classical>Classical Music
<option value="dvd">DVD
<option value="vhs">VHS
<option value=theatrical>Movie Showtimes
<option value=toys>Toys
<option value=baby>Baby
<option value=pc-hardware>Computers
<option value=videogames>Video Games
<option value=electronics>Electronics
<option value=photo>Camera &amp; Photo
<option value=software>Software
<option value=tools>Tools &amp; Hardware
<option value=magazines>Magazines
<option value=garden>Outdoor Living
<option value=kitchen>Kitchen
<option value=travel>Travel
<option value=wireless-phones>Cell Phones & Service
<option value=outlet>Outlet
<option value=auction-redirect>Auctions
<option value=fixed-price-redirect>zShops
</select>
<b>&nbsp;&nbsp;for&nbsp;&nbsp;</b>
<input type="text" name="field-keywords" size="15">&nbsp;&nbsp;
<input type=image name="Go" value="Go!" border=0 alt="Go!" src=http://g-images.amazon.com/images/G/01/v9/search-browse/go-button-gateway.gif width=21 height=21 border=0 align=absmiddle > </td></tr></table>
</td></tr>
</table>
</form>
<p align=center>
<b class=h1>Stuart D. Gathman, make </b><font color=#990000><b class=sans>$</b><b class=sans>310.61</b></font><br />
<b class=sans>Sell <a href="/exec/obidos/flex-sign-in/ref=sdp_bbump_gw/103-3111065-2579065?opt=an&page=misc/login/flex-sign-in-secure.html&response=tg/stores/static/-/used/sell-your-collection/1/">your past purchases</a> at Amazon.com today!</b>
</p>
<table width="100%">
<tr>
<td width="50%" valign="top" align="left">
<span class="small"><a href=/exec/obidos/change-style/subst/home/redirect.html/103-3111065-2579065>Text Only</a></span>
</td>
<td width="50%" valign="top" align="right" class="small">
<a href="#top">Top of Page</a>
</td>
</tr>
</table>
<center>
<p>
<a href=/exec/obidos/subst/home/all-stores.html/ref=gw_bt_st/103-3111065-2579065>Directory of All Stores</a><p>
Our International Sites:
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_uk/103-3111065-2579065?path=http%3A//www.amazon.co.uk/exec/obidos/redirect-home%3Ftag%3Dintl-usgt-ukhome-21%26site%3Damazon">United Kingdom</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_de/103-3111065-2579065?path=http%3A//www.amazon.de/exec/obidos/redirect-home%3Ftag%3Dintl-usgt-dehome-21%26site%3Dhome">Germany</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_jp/103-3111065-2579065?path=http%3A//www.amazon.co.jp/exec/obidos/redirect-home%3Ftag%3Dintl-usgatew-jphome-22%26site%3Damazon">Japan</a>
&nbsp;&nbsp|&nbsp;&nbsp;
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_fr/103-3111065-2579065?path=http%3A//www.amazon.fr/exec/obidos/redirect-home%3Fsite%3Damazon%26tag%3Dusfr-gatew-footer-21">France</a>
<p>
<a href=/exec/obidos/tg/browse/-/508510/ref=gw_bt_he/103-3111065-2579065>Help</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/shopping-basket/ref=gw_bt_sc/103-3111065-2579065>Shopping Cart</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/account-access-login/ref=gw_bt_ya/103-3111065-2579065>Your Account</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="http://s1.amazon.com/exec/varzea/ts/announcement-list-zshops/slp/ref=gw_bt_si/103-3111065-2579065">Sell Items</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/flex-sign-in/ref=gw_bt_oc/103-3111065-2579065?opt=a&page=ordering/one-click-address-sign-in-secure.html&response=one-click-main&method=GET&return-url=one-click-main">1-Click Settings</a>
<p>
<a href=/exec/obidos/subst/misc/company-info.html/ref=gw_bt_aa/103-3111065-2579065>About Amazon.com</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/tg/stores/job-listings/-/generic/home/103-3111065-2579065>Join Our Staff</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/subst/associates/join/associates.html/ref=gw_bt_as/103-3111065-2579065">Join Associates</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/subst/partners/direct/direct-application.html/ref=gw_bt_ad/103-3111065-2579065>Join Advantage</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="http://s1.amazon.com/exec/varzea/subst/fx/home.html/ref=gw_bt_hs/103-3111065-2579065">Join Honor System</a>
</center>
<center>
<p>
<div class="tiny" align=center>
<A HREF="/exec/obidos/subst/misc/policy/conditions-of-use.html/103-3111065-2579065">Conditions of Use</A> | <A HREF="/exec/obidos/tg/browse/-/468496/103-3111065-2579065">Privacy Notice</A> &copy; 1996-2002, Amazon.com, Inc. or its affiliates
</div>
</center>
<!-- whfhYn47qD1fv3PW2R8XWAkFcMwteHFKxorD -->
</body>
</html>
--------------59A46341C90BA737DD47867B--
+18587
View File
File diff suppressed because it is too large Load Diff
-323
View File
@@ -1,323 +0,0 @@
import unittest
import doctest
import Milter
import bms
import mime
import rfc822
import StringIO
import email
import sys
#import pdb
class TestMilter(bms.bmsMilter):
def __init__(self):
bms.bmsMilter.__init__(self)
self.logfp = open("test/milter.log","a")
self._delrcpt = [] # record deleted rcpts for testing
self._addrcpt = [] # record added rcpts for testing
def log(self,*msg):
for i in msg: print >>self.logfp, i,
print >>self.logfp
def getsymval(self,name):
if name == 'j': return 'test.milter.org'
return ''
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self.bodyreplaced = True
else:
raise IOError,"replacebody not called from eom()"
# FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a milter
def chgheader(self,field,idx,value):
if not self._body:
raise IOError,"chgheader not called from eom()"
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '':
del self._msg[field]
else:
self._msg[field] = value
self.headerschanged = True
def addheader(self,field,value,idx=-1):
if not self._body:
raise IOError,"addheader not called from eom()"
self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value
self.headerschanged = True
def delrcpt(self,rcpt):
if not self._body:
raise IOError,"delrcpt not called from eom()"
self._delrcpt.append(rcpt)
def addrcpt(self,rcpt):
if not self._body:
raise IOError,"addrcpt not called from eom()"
self._addrcpt.append(rcpt)
def setreply(self,rcode,xcode,msg):
self.reply = (rcode,xcode,msg)
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com"):
self._body = None
self.bodyreplaced = False
self.headerschanged = False
self.reply = None
msg = rfc822.Message(fp)
rc = self.envfrom('<%s>'%sender)
if rc != Milter.CONTINUE: return rc
rc = self.envrcpt('<%s>'%rcpt)
if rc != Milter.CONTINUE: return rc
line = None
for h in msg.headers:
if h[:1].isspace():
line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
if len(s) > 1: val = s[1].strip()
else: val = ''
rc = self.header(s[0],val)
if rc != Milter.CONTINUE: return rc
line = h
if line:
s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
while 1:
buf = fp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = StringIO.StringIO()
rc = self.eom()
if self.bodyreplaced:
body = self._body.getvalue()
else:
msg.rewindbody()
body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body)
return rc
def feedMsg(self,fname,sender="spam@adv.com",rcpt="victim@lamb.com"):
fp = open('test/'+fname,'r')
rc = self.feedFile(fp,sender,rcpt)
fp.close()
return rc
def connect(self,host='localhost'):
self._body = None
self.bodyreplaced = False
rc = bms.bmsMilter.connect(self,host,1,('1.2.3.4',1234))
if rc != Milter.CONTINUE and rc != Milter.ACCEPT:
self.close()
return rc
rc = self.hello('spamrelay')
if rc != Milter.CONTINUE:
self.close()
return rc
class BMSMilterTestCase(unittest.TestCase):
def testDefang(self,fname='virus1'):
milter = TestMilter()
rc = milter.connect('testDefang')
self.assertEqual(rc,Milter.CONTINUE)
rc = milter.feedMsg(fname)
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(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())
fp.seek(0)
msg = mime.message_from_file(fp)
str = msg.get_payload(1).get_payload()
milter.log(str)
milter.close()
# test some spams that crashed our parser
def testParse(self,fname='spam7'):
milter = TestMilter()
milter.connect('testParse')
rc = milter.feedMsg(fname)
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue())
milter.connect('pro-send.com')
rc = milter.feedMsg('spam8')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg('bounce')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg('bounce1')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
milter.close()
def testDefang2(self):
milter = TestMilter()
milter.connect('testDefang2')
rc = milter.feedMsg('samp1')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg("virus3")
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(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())
rc = milter.feedMsg("virus6")
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failUnless(milter.headerschanged,"Message headers not adjusted")
fp = milter._body
open("test/virus6.tstout","w").write(fp.getvalue())
milter.close()
def testDefang3(self):
milter = TestMilter()
milter.connect('testDefang3')
# test script removal on complex HTML attachment
rc = milter.feedMsg('amazon')
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body
open("test/amazon.tstout","w").write(fp.getvalue())
# test defanging Klez virus
rc = milter.feedMsg("virus13")
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body
open("test/virus13.tstout","w").write(fp.getvalue())
# test script removal on quoted-printable HTML attachment
# sgmllib can't handle the <![if cond]> syntax
rc = milter.feedMsg('spam44')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/spam44.tstout","w").write(fp.getvalue())
milter.close()
def testRFC822(self):
milter = TestMilter()
milter.connect('testRFC822')
# test encoded rfc822 attachment
#pdb.set_trace()
rc = milter.feedMsg('test8')
self.assertEqual(rc,Milter.ACCEPT)
# python2.4 doesn't scan encoded message attachments
if sys.hexversion < 0x02040000:
self.failUnless(milter.bodyreplaced,"Message body not replaced")
#self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/test8.tstout","w").write(fp.getvalue())
rc = milter.feedMsg('virus7')
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
#self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/virus7.tstout","w").write(fp.getvalue())
def testSmartAlias(self):
milter = TestMilter()
milter.connect('testSmartAlias')
# test smart alias feature
key = ('foo@example.com','baz@bat.com')
bms.smart_alias[key] = ['ham@eggs.com']
rc = milter.feedMsg('test8',key[0],key[1])
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter._delrcpt == ['<baz@bat.com>'])
self.failUnless(milter._addrcpt == ['<ham@eggs.com>'])
# python2.4 email does not decode message attachments, so script
# is not replaced
if sys.hexversion < 0x02040000:
self.failUnless(milter.bodyreplaced,"Message body not replaced")
def testBadBoundary(self):
milter = TestMilter()
milter.connect('testBadBoundary')
# test rfc822 attachment with invalid boundaries
#pdb.set_trace()
rc = milter.feedMsg('bound')
if sys.hexversion < 0x02040000:
# python2.4 adds invalid boundaries to decects list and makes
# payload a str
self.assertEqual(rc,Milter.REJECT)
self.assertEqual(milter.reply[0],'554')
#self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/bound.tstout","w").write(fp.getvalue())
def testCompoundFilename(self):
milter = TestMilter()
milter.connect('testCompoundFilename')
# test rfc822 attachment with invalid boundaries
#pdb.set_trace()
rc = milter.feedMsg('test1')
self.assertEqual(rc,Milter.ACCEPT)
#self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/test1.tstout","w").write(fp.getvalue())
def testFindsrs(self):
if not bms.srs:
import SRS
bms.srs = SRS.new(secret='test')
sender = bms.srs.forward('foo@bar.com','mail.example.com')
sndr = bms.findsrs(StringIO.StringIO(
"""Received: from [1.16.33.86] (helo=mail.example.com)
by bastion4.mail.zen.co.uk with smtp (Exim 4.50) id 1H3IBC-00013b-O9
for foo@bar.com; Sat, 06 Jan 2007 20:30:17 +0000
X-Mailer: "PyMilter-0.8.5"
<%s> foo
MIME-Version: 1.0
Content-Type: text/plain
To: foo@bar.com
From: postmaster@mail.example.com
""" % sender
))
self.assertEqual(sndr,'foo@bar.com')
# def testReject(self):
# "Test content based spam rejection."
# milter = TestMilter()
# milter.connect('gogo-china.com')
# rc = milter.feedMsg('big5');
# self.failUnless(rc == Milter.REJECT)
# milter.close();
def suite():
s = unittest.makeSuite(BMSMilterTestCase,'test')
s.addTest(doctest.DocTestSuite(bms))
return s
if __name__ == '__main__':
if len(sys.argv) > 1:
for fname in sys.argv[1:]:
milter = TestMilter()
milter.connect('main')
fp = open(fname,'r')
rc = milter.feedFile(fp)
fp = milter._body
sys.stdout.write(fp.getvalue())
else:
#unittest.main()
unittest.TextTestRunner().run(suite())
+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())
+109 -49
View File
@@ -1,4 +1,10 @@
# $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.
#
# Revision 1.3 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
@@ -20,13 +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
from email import Errors
import Milter
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.
@@ -39,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):
@@ -96,58 +118,94 @@ 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
self.testDefang('ziploop',1,'stuart@bmsi.com.zip')
def _chk_name(self,name):
self.filename = name
def _chk_attach(self,msg):
"Filter attachments by content."
# check for bad extensions
mime.check_name(msg,ckname=self._chk_name,scan_zip=True)
# remove scripts from HTML
mime.check_html(msg)
# don't let a tricky virus slip one past us
msg = msg.get_submsg()
if isinstance(msg,email.message.Message):
return mime.check_attachments(msg,self._chk_attach)
return Milter.CONTINUE
def testCheckAttach(self,fname="test1"):
# test1 contains a very long filename
with open('test/'+fname,'rb') as fp:
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True)
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.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]>
@@ -156,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')
@@ -166,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())
+14 -100
View File
@@ -2,98 +2,12 @@ import unittest
import Milter
import sample
import mime
import rfc822
import StringIO
class TestMilter(sample.sampleMilter):
from Milter.test import TestBase
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):
@@ -103,9 +17,9 @@ class BMSMilterTestCase(unittest.TestCase):
self.failUnless(rc == Milter.CONTINUE)
rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failUnless(milter._bodyreplaced,"Message body not replaced")
fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue())
open('test/'+fname+".tstout","wb").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
fp.seek(0)
msg = mime.message_from_file(fp)
@@ -118,9 +32,9 @@ class BMSMilterTestCase(unittest.TestCase):
milter.connect('somehost')
rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
self.failIf(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):
@@ -128,19 +42,19 @@ class BMSMilterTestCase(unittest.TestCase):
milter.connect('somehost')
rc = milter.feedMsg('samp1')
self.failUnless(rc == Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg("virus3")
self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failUnless(milter._bodyreplaced,"Message body not replaced")
fp = milter._body
open("test/virus3.tstout","w").write(fp.getvalue())
open("test/virus3.tstout","wb").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
rc = milter.feedMsg("virus6")
self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failUnless(milter.headerschanged,"Message headers not adjusted")
self.failUnless(milter._bodyreplaced,"Message body not replaced")
self.failUnless(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')
+12 -4
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,7 +13,8 @@ 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)
@@ -31,17 +34,22 @@ class AddrCacheTestCase(unittest.TestCase):
self.failUnless(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)
def testParseHeader(self):
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
h = Milter.utils.parse_header(s)
self.assertEqual(h,b'Last Few Coldplay Album Artworks Available\x00')
def suite():
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__':