Compare commits

..

356 Commits

Author SHA1 Message Date
cvs2svn a776a58240 This commit was manufactured by cvs2svn to create tag 'milter-0_8_18'.
Sprout from master 2013-03-15 23:04:38 UTC Stuart Gathman <stuart@gathman.org> 'Move many configs to datadir'
Delete:
    COPYING
    CREDITS
    ChangeLog
    Doxyfile
    MANIFEST.in
    NEWS
    README
    TODO
    doc/mainpage.py
    doc/milter.py
    makefile
    milter-nomix.py
    milter-template.py
    miltermodule.c
    mime.py
    pymilter.spec
    sample.py
    setup.cfg
    setup.py
    start.sh
    test.py
    test/big5
    test/bounce
    test/bounce1
    test/bound
    test/honey
    test/missingboundary
    test/samp1
    test/spam44
    test/spam7
    test/spam8
    test/test1
    test/test2
    test/test8
    test/virus1
    test/virus13
    test/virus2
    test/virus3
    test/virus4
    test/virus5
    test/virus6
    test/virus7
    test/zip1
    test/zip2
    test/zip3
    test/ziploop
    testgrey.py
    testmime.py
    testsample.py
    testutils.py
2013-03-15 23:04:39 +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
Stuart Gathman bc88a64d9b Release 0.8.11 2008-10-11 15:58:00 +00:00
Stuart Gathman a5078a6eb1 Release 0.8.11 2008-10-11 15:57:59 +00:00
Stuart Gathman 96f5b6e9dc Don't greylist DSNs. 2008-10-11 15:45:46 +00:00
Stuart Gathman 1c4878963b Skip greylisting for good reputation. 2008-10-09 18:44:54 +00:00
Stuart Gathman f8e1c15ccd Text for accepting SPF fail with DSN. 2008-10-09 02:54:13 +00:00
Stuart Gathman c86ad6f68c Pass rpmlint on spec file. 2008-10-09 02:43:16 +00:00
Stuart Gathman 0d1f2b7f4d Don't reset greylist timer on early retries. 2008-10-09 00:55:13 +00:00
Stuart Gathman d4cafcd435 Greylisting 2008-10-08 04:57:28 +00:00
Stuart Gathman d64aad95c1 Delay strike3 REJECT and don't reject if whitelisted.
Recognize vacation messages as autoreplies.
2008-10-02 03:19:00 +00:00
Stuart Gathman f9ed6f7194 Never ban a trusted relay. 2008-09-09 23:24:56 +00:00
Stuart Gathman 93e9644574 Wasn't reading banned_ips 2008-09-09 23:08:16 +00:00
Stuart Gathman d86b9f7312 Whitelists and Blacklists 2008-09-09 23:07:48 +00:00
Stuart Gathman cbf69f596b Top level credit link for mascot image. 2008-08-25 22:54:23 +00:00
Stuart Gathman 5b84d454da API docs on milter.org moved 2008-08-25 22:40:58 +00:00
Stuart Gathman e5bf1aee09 Fix /var/run/milter owner 2008-08-25 22:03:24 +00:00
Stuart Gathman 5df3a80f7b Fix /var/run/milter owner 2008-08-25 22:02:39 +00:00
Stuart Gathman df67ee9147 Report failure to remove milter socket 2008-08-25 22:00:46 +00:00
Stuart Gathman 593384d610 /var/run/milter must be owned by mail 2008-08-25 21:41:18 +00:00
Stuart Gathman 1280f1360e Release 0.8.10 2008-08-25 20:00:51 +00:00
Stuart Gathman 3e1e528abe Release 0.8.10 2008-08-25 19:49:02 +00:00
Stuart Gathman 04ce8f81b9 Release 0.8.10 2008-08-25 18:49:13 +00:00
Stuart Gathman bc390e69b9 Update docs 2008-08-25 18:45:21 +00:00
Stuart Gathman c07ed917ab Handle missing gossip_node so self tests pass. 2008-08-25 18:32:23 +00:00
Stuart Gathman a14d676fb6 Release 0.8.10 2008-08-25 18:18:30 +00:00
Stuart Gathman 600e3dfbfb Update docs for 0.8.10 2008-08-25 18:14:56 +00:00
Stuart Gathman 8cfa03bbc4 Log rcpt for SRS rejections. 2008-08-18 17:47:57 +00:00
Stuart Gathman 28a0e551bd CBV policy sends no DSN. DSN policy sends DSN. 2008-08-06 00:52:38 +00:00
Stuart Gathman be3f463450 Send quarantine DSN to SPF PASS only. 2008-08-05 18:04:06 +00:00
Stuart Gathman a420148b1e Parse ESMTP params 2008-07-29 21:59:29 +00:00
Stuart Gathman f4465ea816 Allow explicitly whitelisted email from banned_users. 2008-05-08 21:35:57 +00:00
Stuart Gathman 1845876665 Configure gossip TTL. 2008-04-10 14:59:35 +00:00
Stuart Gathman cee6bc3bea Release 0.8.10 2008-04-02 18:59:14 +00:00
Stuart Gathman 71403de50e Do not CBV whitelisted addresses. We already know they are good. 2008-04-01 00:13:10 +00:00
Stuart Gathman 017784b5a7 Handle multi-hop source path in parseaddr. 2008-01-10 16:41:04 +00:00
Stuart Gathman 632e7b4248 Handle unquoted fullname when parsing email. 2008-01-09 20:15:49 +00:00
Stuart Gathman 10f4f2613e Packaging tweaks. 2007-11-29 14:35:17 +00:00
Stuart Gathman 69369c3b2a Support temperror policy in access. 2007-11-01 20:09:14 +00:00
Stuart Gathman 5386e08ca5 Send quarantine DSN to SPF pass (official or guessed) only.
Reject blacklisted email too big for dspam.
2007-10-10 18:23:54 +00:00
Stuart Gathman d0fe3b0b84 Check porn keywords in From header field. 2007-10-10 18:07:50 +00:00
Stuart Gathman 670e97cb79 Test on Centos5 2007-09-25 17:07:32 +00:00
Stuart Gathman 6397b7027f Tested on RH7 2007-09-25 16:37:26 +00:00
Stuart Gathman 94ce032559 Update license. 2007-09-25 02:26:29 +00:00
Stuart Gathman 91230381cb Test dns.py 2007-09-25 02:15:35 +00:00
Stuart Gathman 46ed3ddbcb Allow arbitrary object, not just spf.query like, to provide data for create_msg 2007-09-25 01:24:59 +00:00
Stuart Gathman 6048fe6e8c Remove explicit spf dependency. 2007-09-24 20:13:26 +00:00
Stuart Gathman d225384829 Create milter and milter-spf as noarch packages. 2007-09-24 18:00:58 +00:00
Stuart Gathman a84f6aa574 Specify library_dirs for Debian. 2007-09-24 17:44:51 +00:00
Stuart Gathman 344e8f0a0a Report domain on reputation reject. 2007-09-13 14:51:03 +00:00
Stuart Gathman 1fa4b72c84 Delete unparseable timestamps when loading address cache. These have
arisen because of failure to parse MAIL FROM properly.   Will have to
tighten up MAIL FROM parsing to match RFC.
2007-09-03 16:18:45 +00:00
Stuart Gathman 021ea96748 Fixes from test on EL5. 2007-07-25 19:04:44 +00:00
Stuart Gathman a490e79564 Build on EL5 2007-07-25 17:43:34 +00:00
Stuart Gathman 33e8f7c4cc Multi-package build fixes. 2007-07-25 17:30:30 +00:00
Stuart Gathman 6bbb6b3f02 Move milter apps to /usr/lib/pymilter 2007-07-25 17:14:59 +00:00
Stuart Gathman 6577e40bfb Build pymilter as separate package. 2007-07-25 15:32:09 +00:00
Stuart Gathman 04eeeab2e1 Clarify docs. 2007-07-25 15:20:41 +00:00
Stuart Gathman cdfeb2d792 Ban ips on bad mailfrom offenses as well as bad rcpts. 2007-07-02 03:06:10 +00:00
Stuart Gathman 46545cab94 Fix missed comcast dynip. 2007-06-28 20:33:25 +00:00
Stuart Gathman 9a8fdcb120 Ban IPs based on too many invalid recipients in a connection. Requires
configuring check_user.  Tighten HELO best_guess policy.
2007-06-23 20:53:05 +00:00
Stuart Gathman 218f5168bc Do not process valid SRS recipients as delayed_failure. 2007-04-19 16:02:43 +00:00
Stuart Gathman ddbb8ac3ea Ban ips with too many bad rcpts on a connection. 2007-04-15 01:01:13 +00:00
Stuart Gathman a2215124bb Ban ips with too many bad rcpts on a connection. 2007-04-15 00:54:30 +00:00
Stuart Gathman e505d2bb28 Check access_file at startup. Compress rcpt to log. 2007-04-13 17:20:09 +00:00
Stuart Gathman 9f40f265cd Stop querying gossip server twice. 2007-04-05 17:59:07 +00:00
Stuart Gathman 20a875b84d Don't disable gossip for temporary error. 2007-04-02 18:37:25 +00:00
Stuart Gathman 1da5ca54b5 Report bestguess and helo-spf as key-value pairs in Received-SPF
instead of in their own headers.
2007-03-30 18:13:41 +00:00
Stuart Gathman bac593f05d Don't count DSN and unqualified MAIL FROM as internal_domain. 2007-03-29 03:06:10 +00:00
Stuart Gathman dbba488d58 Do not CBV for internal domains. 2007-03-24 00:30:24 +00:00
Stuart Gathman 6936b599fe Get SMTP-Auth policy from access_file. 2007-03-23 22:39:10 +00:00
Stuart Gathman cee38f8149 Properly log From: and Sender: 2007-03-21 04:02:13 +00:00
Stuart Gathman 188e8256f3 Gossip configuration options: client or standalone with optional peers. 2007-03-18 02:32:21 +00:00
Stuart Gathman 4013365a3d New delayed DSN pattern. Retab (expandtab). 2007-03-17 21:22:48 +00:00
Stuart Gathman e571ccc5a5 Fix missing HELO log. 2007-03-13 21:18:28 +00:00
Stuart Gathman f65294b470 Include Received-SPF in permerror DSN. 2007-03-13 18:45:09 +00:00
Stuart Gathman b2d8e838a2 Fix continuing findsrs when srs.reverse fails. 2007-03-03 19:18:57 +00:00
Stuart Gathman f136e973dc Improve delayed failure detection. 2007-03-03 18:46:26 +00:00
Stuart Gathman d289822f42 Handle DNS error sending DSN. 2007-03-03 18:19:40 +00:00
Stuart Gathman 806aa5a6de Updated 2007-03-02 14:18:21 +00:00
Stuart Gathman e84a803cc1 Handle missing HELO. 2007-02-21 22:14:41 +00:00
Stuart Gathman 20612240f3 Use re for auto-reply recognition. 2007-02-07 23:21:26 +00:00
Stuart Gathman c9e6bb68d9 Newbie friendly default for internal_connect 2007-02-07 23:20:28 +00:00
Stuart Gathman 4d69b8fbfe Handle null in header value. 2007-01-26 03:47:23 +00:00
Stuart Gathman 21e3c6f489 Persist blacklisting from delayed DSNs. 2007-01-25 22:47:26 +00:00
Stuart Gathman 83529320ae Add private relay. 2007-01-23 19:46:20 +00:00
Stuart Gathman e5685c6035 Convert tabs to spaces. 2007-01-22 02:46:01 +00:00
Stuart Gathman 4c72135b0e Move parse_header to Milter.utils.
Test case for delayed DSN parsing.
Fix plock when source missing or cannot set owner/group.
2007-01-19 23:31:38 +00:00
Stuart Gathman 393aa6140a Doc update.
Parse From header for delayed failure detection.
Don't check reputation of trusted host.
Track IP reputation only when missing PTR.
2007-01-18 16:48:44 +00:00
Stuart Gathman 2a6a68230b REJECT after data for blacklisted emails - so in case of mistakes, a
legitimate sender will know what happened.
2007-01-16 05:17:29 +00:00
Stuart Gathman 279c831a8e Purge old entries in auto_whitelist and send_dsn logs. 2007-01-11 19:59:40 +00:00
Stuart Gathman c0aa632e16 Negative feedback for bad headers. Purge cache logs on startup. 2007-01-11 04:31:26 +00:00
Stuart Gathman a875ac7834 Documentation updates. 2007-01-10 04:44:25 +00:00
Stuart Gathman 9f8cef5ee2 Get user feedback. 2007-01-08 23:20:54 +00:00
Stuart Gathman 4b0e7b22da Tested on spidey2 2007-01-06 04:32:57 +00:00
Stuart Gathman 40fb05b0e3 Forgot import 2007-01-06 04:25:12 +00:00
Stuart Gathman 8ae7bd4217 Add config file to spfmilter 2007-01-06 04:21:30 +00:00
Stuart Gathman 139e141e1e Make blacklist an AddrCache 2007-01-05 23:33:55 +00:00
Stuart Gathman 8932dc36db Move parse_addr, iniplist, ip4re to Milter.utils 2007-01-05 23:12:13 +00:00
Stuart Gathman bda654b7a0 Added sample spfmilter.py application. 2007-01-05 22:48:48 +00:00
Stuart Gathman 09b671f47b Test AddrCache. 2007-01-05 21:26:03 +00:00
Stuart Gathman 732e7317f1 Move AddrCache to Milter package. 2007-01-05 21:25:40 +00:00
Stuart Gathman 702ec2d4ca Link to pyspf. 2007-01-05 21:24:29 +00:00
Stuart Gathman 7bbff66000 Release 0.8.7 2007-01-04 18:04:37 +00:00
Stuart Gathman 5ad6d321bd Do plain CBV when template missing. 2007-01-04 18:01:11 +00:00
Stuart Gathman d01dc65f39 Use HELO identity if good when MAILFROM is bad. 2006-12-31 03:07:20 +00:00
Stuart Gathman b703031c7e Skip reputation/whitelist/blacklist when rejecting on SPF. Add X-Hello-SPF. 2006-12-30 18:58:53 +00:00
Stuart Gathman 1bc0a4faef Reject on bad_reputation or blacklist and nodspam. Match valid helo like
PTR for guessed SPF pass.
2006-12-28 01:54:32 +00:00
Stuart Gathman 2bea6ad76f Add archive option to wiretap. 2006-12-19 00:59:30 +00:00
Stuart Gathman c9f0c94b92 Reject multiple recipients to DSN.
Auto-disable gossip on DB error.
2006-12-04 18:47:04 +00:00
Stuart Gathman 59bf86e747 Release 0.8.7 2006-11-22 18:32:37 +00:00
Stuart Gathman 8f5513a502 SRS domains were missing srs_reject check when SES was active. 2006-11-22 16:31:22 +00:00
Stuart Gathman 87482d5740 Replace last use of deprecated rfc822 module. 2006-11-22 01:03:28 +00:00
Stuart Gathman b227ca6bb0 Update a use of deprecated rfc822. Recognize report-type=delivery-status 2006-11-21 18:45:49 +00:00
Stuart Gathman dd0125b641 Another lame DSN heuristic. Block PTR cache poisoning attack. 2006-11-04 22:09:39 +00:00
Stuart Gathman a7e98f411e More SPF fixes and tests from pyspf. 2006-10-09 17:59:47 +00:00
Stuart Gathman ea76acdd3d Fix defaults. 2006-10-04 03:46:01 +00:00
Stuart Gathman b92154934b SPF updates from pyspf. 2006-10-04 02:15:57 +00:00
Stuart Gathman 33aeefa19f case_sensitive_localpart option, more delayed bounce heuristics,
optional smart_alias section.
2006-10-01 01:44:06 +00:00
Stuart Gathman 2fe8fa8813 Use latest pyspf verbatim. Will depend on package when pyspf-2.0 is packaged. 2006-10-01 01:42:33 +00:00
Stuart Gathman e0f58cce1f Merge changes from pyspf to pass test suite. 2006-09-08 22:02:57 +00:00
Stuart Gathman 157f33edb8 Permerror for multiple TXT SPF records. 2006-07-31 15:25:39 +00:00
Stuart Gathman 64bf954a17 Remove debug print 2006-07-28 01:21:33 +00:00
Stuart Gathman 357cd1b740 More fixes from pyspf 2006-07-28 01:21:02 +00:00
Stuart Gathman 3a90a35cbc Support CBV timeout 2006-07-26 16:42:26 +00:00
Stuart Gathman 30923ab3a1 Support timeout. 2006-07-26 16:37:35 +00:00
Stuart Gathman d38cf5885e Handle multi-line headers in delayed dsns. 2006-06-21 22:22:00 +00:00
Stuart Gathman 8c4cca8f55 initialize perm_error 2006-06-21 21:13:07 +00:00
Stuart Gathman a20eeda04d More delayed reject token headers.
Don't require HELO pass for CBV.
2006-06-21 21:12:04 +00:00
Stuart Gathman d50215d0ba Include header fields in DSN template. 2006-06-21 21:07:11 +00:00
Stuart Gathman c5b2169509 Remove default templates. Scrub test. 2006-05-24 20:56:35 +00:00
Stuart Gathman 2e42eea306 Release 0.8.6 2006-05-21 04:04:02 +00:00
Stuart Gathman 1c78384da9 Release 0.8.6 2006-05-21 03:56:13 +00:00
Stuart Gathman 053c32e450 Fail dsn 2006-05-21 03:41:44 +00:00
Stuart Gathman b57e365349 Default templates need headers also. 2006-05-21 03:39:59 +00:00
Stuart Gathman 99396a1eee Fail template, move most header fields into template. 2006-05-21 03:30:06 +00:00
Stuart Gathman 528810c31a Create GOSSiP record only when connection will procede to DATA. 2006-05-17 21:28:07 +00:00
Stuart Gathman a9ffc3ae28 a:1.2.3.4 -> ip4:1.2.3.4 'lax' heuristic. 2006-05-12 16:15:20 +00:00
Stuart Gathman eda8680b70 Don't require SPF pass for white/black listing mail from trusted relay.
Support localpart wildcard for white and black lists.
2006-05-12 16:14:48 +00:00
Stuart Gathman afd3e0f042 Check whitelist/blacklist even when not checking SPF (e.g. trusted relay). 2006-04-06 18:14:17 +00:00
Stuart Gathman f42ddbfb53 Fix spec bug 2006-03-25 17:33:36 +00:00
Stuart Gathman 44d76a63d8 0.8.6 release candidate 2006-03-25 17:29:28 +00:00
Stuart Gathman ec4f9fdd99 Import note_error from pyspf. Handle timeout on type99 lookup
specially (sender actually has no SPF record and a braindead DNS server).
2006-03-21 18:48:51 +00:00
Stuart Gathman 6102d641c5 Use re to recognize failure DSNs. 2006-03-10 20:52:49 +00:00
Stuart Gathman d69b805690 Use signed Message-ID in delayed reject to blacklist senders 2006-03-07 20:50:54 +00:00
Stuart Gathman 994bcce7dc Properly report hard PermError (lax mode fails also) by always setting
perm_error attribute with PermError exception.  Improve reporting of
invalid domain PermError.
2006-02-24 02:12:54 +00:00
Stuart Gathman 7f5d8b6b11 Use SRS sign domain list.
Accept but do not use for training whitelisted senders without SPF pass.
Immediate rejection of unsigned bounces.
2006-02-17 05:04:29 +00:00
Stuart Gathman 8d02ab1771 User specific SPF receiver policy. 2006-02-16 02:16:36 +00:00
Stuart Gathman 18759c3698 Remove spf dependency for iniplist 2006-02-12 04:15:01 +00:00
Stuart Gathman 2f533c4591 Use CIDR notation for internal connect list. 2006-02-12 02:12:08 +00:00
Stuart Gathman 04c8b2e1fc Resolve FIXME for wrap_close. 2006-02-12 02:00:42 +00:00
Stuart Gathman 56c1cbd0fd Don't check rcpt user list when signed MFROM. 2006-02-12 01:13:58 +00:00
Stuart Gathman ce51034f69 Use CIDR notation for trusted_forwarder iplist 2006-02-09 20:39:43 +00:00
Stuart Gathman 285d4663c9 put back eom condition 2006-01-30 23:14:48 +00:00
Stuart Gathman 5830e13d00 New milter.log tags 2006-01-12 20:53:51 +00:00
Stuart Gathman 1b685fca76 Accelerate training via whitelist and blacklist. 2006-01-12 20:31:24 +00:00
Stuart Gathman 71e769ef0c New FAQ 2006-01-05 03:17:10 +00:00
Stuart Gathman 63e45eb884 Documentation updates. 2005-12-29 22:46:07 +00:00
Stuart Gathman 28bc84eda0 Release 0.8.5 2005-12-29 19:33:18 +00:00
Stuart Gathman 7f7f2500dc Include report. 2005-12-29 19:23:14 +00:00
Stuart Gathman 4f220b48cf Release 0.8.5 2005-12-29 19:21:37 +00:00
Stuart Gathman a9ca154a92 Handle NULL MX 2005-12-29 19:15:35 +00:00
Stuart Gathman 65672fb26f Update log parser for new ops, etc 2005-12-29 04:50:39 +00:00
Stuart Gathman 155eb4e675 Do not auto-whitelist autoreplys 2005-12-29 04:49:10 +00:00
Stuart Gathman 14d5869019 parse milter.log from bms.py into a sequence of connections 2005-12-28 22:24:34 +00:00
Stuart Gathman 28ca3b2837 Expire and renew AddrCache entries 2005-12-28 20:17:29 +00:00
Stuart Gathman 52b0ac9377 Put guessed result in separate header. 2005-12-23 22:34:46 +00:00
Stuart Gathman 8bc182cb37 Move Received-SPF header to top. 2005-12-23 21:47:07 +00:00
Stuart Gathman fb3c140d4c Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER) 2005-12-23 21:46:36 +00:00
Stuart Gathman 52d23604f7 Always include keyword data in Received-SPF header. 2005-12-23 21:44:15 +00:00
Stuart Gathman 15f8b797bf Select neutral DSN template for best_guess 2005-12-09 16:54:01 +00:00
Stuart Gathman 3b544a4076 improve gossip support.
Initialize srs_domain from srs.srs config property.  Should probably
always block unsigned DSN when signing all.
2005-12-01 22:42:32 +00:00
Stuart Gathman 36a7dce2e5 Fix neutral policy. pobox.com -> openspf.org 2005-12-01 18:59:25 +00:00
Stuart Gathman a418f34491 GOSSiP support, local database only. 2005-11-07 21:22:35 +00:00
Stuart Gathman ba5854fc91 Simple implementation of trusted_forwarder list. Inefficient for
more than 1 or 2 entries.
2005-10-31 00:09:41 +00:00
Stuart Gathman a0878320fa Doc updates 2005-10-31 00:09:12 +00:00
Stuart Gathman d1583d88c9 Add titles. 2005-10-30 01:08:52 +00:00
Stuart Gathman 3ad67bd33b Ignore records missing spaces. 2005-10-30 01:08:14 +00:00
Stuart Gathman eb2e730b5d Don't check internal_domains for trusted_relay. 2005-10-28 19:36:54 +00:00
Stuart Gathman daa1eacff3 Do not send quarantine DSN when sender is DSN. 2005-10-28 09:30:49 +00:00
Stuart Gathman aaf23f35f8 New webpage design based on ht2html. 2005-10-25 21:39:47 +00:00
Stuart Gathman 25b6378631 Consider MAIL FROM a match for supply_sender when a subdomain of From or Sender 2005-10-23 16:01:30 +00:00
Stuart Gathman c6ac3ddad8 Release 0.8.4 2005-10-20 23:36:11 +00:00
Stuart Gathman b3dce26928 Include smfi_progress is SMFIR_PROGRESS defined 2005-10-20 23:23:36 +00:00
Stuart Gathman fcd85dbfb5 Add optional idx for position of added header. 2005-10-20 23:04:49 +00:00
Stuart Gathman 3a1c964f0d Configure auto_whitelist senders. 2005-10-20 18:47:27 +00:00
Stuart Gathman 36ae390f01 access.db stores keys in lower case 2005-10-19 21:07:49 +00:00
Stuart Gathman 4c0cf4fb95 Train screener on whitelisted messages. 2005-10-19 19:37:50 +00:00
Stuart Gathman 8f8de8fa97 Auto whitelist refinements. 2005-10-14 16:17:31 +00:00
Stuart Gathman bc516456c1 Auto whitelist feature. 2005-10-14 01:14:08 +00:00
Stuart Gathman a50194d07f Release 0.8.3 2005-10-12 17:21:13 +00:00
Stuart Gathman 1cf272ceb0 Release 0.8.3 2005-10-12 16:45:58 +00:00
Stuart Gathman d2dc09f979 Release 0.8.3 2005-10-12 16:43:14 +00:00
Stuart Gathman ea82d6d608 Release 0.8.3 2005-10-12 16:36:30 +00:00
Stuart Gathman ace3e13685 Always check HELO except for SPF pass, temperror. 2005-10-11 22:50:07 +00:00
Stuart Gathman 78ea2e2263 Use logging module to make logging threadsafe (avoid splitting log lines) 2005-10-10 23:50:20 +00:00
Stuart Gathman d34efa39bb Configure SPF policy via sendmail access file. 2005-10-10 20:15:33 +00:00
Stuart Gathman 36b5b4e6d4 Milter.py moved to Milter subpackage. 2005-10-07 03:25:24 +00:00
Stuart Gathman 04874d6e35 Banned users option. Experimental feature to supply Sender when
missing and MFROM domain doesn't match From.  Log cipher bits for
SMTP AUTH.  Sketch access file feature.
2005-10-07 03:23:40 +00:00
Stuart Gathman 073f87dcc7 Handle perverse MFROM quoting. 2005-09-08 03:55:09 +00:00
Stuart Gathman 7ab5ddf053 Getting ready for 0.8.3 2005-08-18 04:19:26 +00:00
Stuart Gathman d6ef1a4007 Don't innoculate with SCREENED mail. 2005-08-18 03:36:54 +00:00
Stuart Gathman 2a4ab4e87c Send DSN before adding message to quarantine. 2005-08-17 19:35:28 +00:00
Stuart Gathman 241717b0e2 quarantine template 2005-08-16 22:46:33 +00:00
Stuart Gathman bd8fabae0f Example of wiretap with multiple destinations. 2005-08-16 22:46:10 +00:00
Stuart Gathman d119af1a3e Trean non-existant include as no match in "lax" mode. 2005-08-12 17:36:51 +00:00
Stuart Gathman f1f082fe8a Consider SMTP AUTH connections internal. 2005-08-11 22:17:59 +00:00
Stuart Gathman b0286bff22 Treat fail like softfail for selected (braindead) domains.
Treat mail according to extended processing results, but
report any PermError that would officially result via DSN.
2005-08-04 21:21:33 +00:00
Stuart Gathman a9663a23d7 Keep screened honeypot mail, but optionally discard honeypot only mail. 2005-08-02 18:04:36 +00:00
Stuart Gathman 8df5cd026e Limit CNAME chains independently of DNS lookup limit 2005-07-22 16:00:23 +00:00
Stuart Gathman 0cbfc0d249 Limit CNAME lookups. 2005-07-21 17:59:46 +00:00
Stuart Gathman 46ad2794f1 Handle corrupt ZIP attachments 2005-07-20 14:56:38 +00:00
Stuart Gathman 8fef702522 Handle corrupt and empty ZIP files. 2005-07-20 14:49:45 +00:00
Stuart Gathman 62b33bd964 Check pydspam version for honeypot, include latest pyspf changes. 2005-07-20 03:30:04 +00:00
Stuart Gathman ffcadf6c01 Remove debug print 2005-07-18 14:36:23 +00:00
Stuart Gathman 9f7d52118a Release 0.8.2 2005-07-17 01:33:13 +00:00
Stuart Gathman 95b24f7663 Log as well as use extended result for best guess. 2005-07-17 01:25:44 +00:00
Stuart Gathman db0f1095e5 Release 0.8.2 2005-07-15 22:24:10 +00:00
Stuart Gathman f749b6f2cd Support callback exception policy 2005-07-15 22:18:17 +00:00
Stuart Gathman 23485978fc Latest pyspf updates 2005-07-15 22:17:41 +00:00
Stuart Gathman e1f4744a22 Use extended results processing for best_guess. 2005-07-15 20:25:36 +00:00
Stuart Gathman ef413913d0 Allow extended processing for MX count. 2005-07-15 20:00:35 +00:00
Stuart Gathman 8ad4b16156 Import bug fixes from pyspf module. CID xml support removed. 2005-07-14 03:47:40 +00:00
Stuart Gathman b28a56ea37 Make SES package optional. Initial honeypot support. 2005-07-14 03:23:33 +00:00
Stuart Gathman e3b18d61c9 Initial SES integration. 2005-07-06 04:05:40 +00:00
Stuart Gathman 5335e18925 Questions from email answered. 2005-07-04 21:06:31 +00:00
Stuart Gathman e2f1587832 Don't match hostnames for internal connects. 2005-07-02 23:27:31 +00:00
Stuart Gathman febf56b031 Always log trusted Received and Received-SPF headers. 2005-07-01 16:30:24 +00:00
Stuart Gathman e9f6773096 Report context allocation error. 2005-06-24 04:20:07 +00:00
Stuart Gathman 2276762c52 Remove unused name argument to generic wrappers. 2005-06-24 04:12:43 +00:00
Stuart Gathman a142fefb19 Handle close called before connect. 2005-06-24 03:57:35 +00:00
Stuart Gathman 900b7ef3fb Setreply for rejectvirus. 2005-06-20 22:35:35 +00:00
Stuart Gathman d07e536f44 Release 0.8.1 2005-06-17 02:23:34 +00:00
Stuart Gathman 2d291d35f6 Release 0.8.1 2005-06-17 02:07:20 +00:00
Stuart Gathman a94f82d8f3 Handle zip within zip. 2005-06-17 01:49:39 +00:00
Stuart Gathman 124747c309 Update faq. 2005-06-17 00:46:29 +00:00
Stuart Gathman 4c659c7f87 Acknowlege that current env callback protocol is entrenched. 2005-06-17 00:45:10 +00:00
Stuart Gathman 3e47952438 Change more info page back to spf.pobox.com which is now maintained. 2005-06-17 00:38:39 +00:00
Stuart Gathman a01c5d31f1 Ignore HeaderParseError decoding header 2005-06-16 18:35:51 +00:00
Stuart Gathman 493741c81e Return consistent tuple on error. 2005-06-15 19:45:47 +00:00
15 changed files with 2257 additions and 416 deletions
+794
View File
@@ -0,0 +1,794 @@
## @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.
__version__ = '0.9.8'
import os
import re
import milter
import thread
from milter import *
from functools import wraps
_seq_lock = thread.allocate_lock()
_seq = 0
## @fn set_flags(flags)
# @brief Enable optional %milter actions.
# Certain %milter actions need to be enabled before calling milter.runmilter()
# or they throw an exception.
# @param flags Bit ored mask of optional actions to enable
def uniqueID():
"""Return a unique sequence number (incremented on each call).
"""
global _seq
_seq_lock.acquire()
seqno = _seq = _seq + 1
_seq_lock.release()
return seqno
## @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_HEAD_LEADSPC flag. By default,
# header continuation lines are collected and joined before getting
# sent to a milter. Headers modified or added by the milter are
# folded by the MTA as necessary according to its own standards.
# 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_HEAD_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):
## 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)
## Defined by subclasses to write log messages.
def log(self,*msg): pass
## Called for each connection to the MTA. Called by the
# <a href="https://www.milter.org/developers/api/xxfi_connect">
# 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="https://www.milter.org/developers/api/xxfi_envfrom">
# xxfi_envfrom</a> callback.
# Returning REJECT rejects the message, but not the connection.
# The sender is the "envelope" from as defined by
# <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="https://www.milter.org/developers/api/xxfi_envrcpt">
# xxfi_envrcpt</a> callback.
# Returning REJECT rejects the current recipient, not the entire message.
# The recipient is the "envelope" recipient as defined by
# <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="https://www.milter.org/developers/api/xxfi_negotiate">
# xffi_negotiate</a> callback. This is an advanced callback,
# do not override unless you know what you are doing. Most
# negotiation can be done simply by using the supplied
# 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="https://www.milter.org/developers/api/smfi_getsymval">
# 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="https://www.milter.org/developers/api/smfi_setreply">
# smfi_setreply</a> for more information.
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
# @param msg The text part of the SMTP reply. If msg is None,
# an empty message is used.
# @param ml Optional additional message lines.
def setreply(self,rcode,xcode=None,msg=None,*ml):
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="https://www.milter.org/developers/api/smfi_addheader">
# 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="https://www.milter.org/developers/api/smfi_chgheader">
# 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="https://www.milter.org/developers/api/smfi_addrcpt">
# smfi_addrcpt</a>.
# If no corresponding mail header is added, this is like a Bcc.
# The syntax of the recipient is the same as used in the SMTP
# 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="https://www.milter.org/developers/api/smfi_delrcpt">
# smfi_delrcpt</a>.
# The recipient should match one passed to the envrcpt callback.
# The <code>Milter.DELRCPT</code> action flag must be set.
#
# 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="https://www.milter.org/developers/api/smfi_replacebody">
# smfi_replacebody</a>.
# The entire message body must be replaced.
# Call repeatedly with blocks of data until the entire body is transferred.
# 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="https://www.milter.org/developers/api/smfi_chgfrom">
# smfi_chgfrom</a>.
# The syntax of the sender is that same as used in the SMTP
# MAIL FROM command (and as delivered to the envfrom callback),
# 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="https://www.milter.org/developers/api/smfi_quarantine">
# smfi_quarantine</a>.
# When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined.
# 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="https://www.milter.org/developers/api/smfi_progress">
# 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:',
for i in msg: print i,
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
str -> tuple additional ESMTP parameters
"""
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 eom(self):
"Called at the end of message."
self.log("eom")
return CONTINUE
def abort(self):
"Called if the connection is terminated abnormally."
self.log("abort")
return CONTINUE
def close(self):
"Called at the end of connection, even if aborted."
self.log("close")
return CONTINUE
## 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
## @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)
## @private
# @brief Disconnect milterContext and call close method.
def close_callback(ctx):
m = ctx.getpriv()
if not m: return CONTINUE
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 parms with values to keyword dictionary."
kw = {}
for s in args:
pos = s.find('=')
if pos > 0:
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
ESMTP parameters as python keyword parameters."""
kw = {}
pargs = [args[0]]
for s in args[1:]:
pos = s.find('=')
if pos > 0:
kw[s[:pos].upper()] = s[pos+1:]
else:
pargs.append(s)
return c(*pargs,**kw)
## 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):
# This bit is here on the assumption that you will be starting this filter
# before sendmail. If sendmail is not running and the socket already exists,
# libmilter will throw a warning. If sendmail is running, this is still
# safe if there are no messages currently being processed. It's safer to
# shutdown sendmail, kill the filter process, restart the filter, and then
# restart sendmail.
pos = socketname.find(':')
if pos > 1:
s = socketname[:pos]
fname = socketname[pos+1:]
else:
s = "unix"
fname = socketname
if s == "unix" or s == "local":
print "Removing %s" % fname
try:
os.unlink(fname)
except os.error, x:
import errno
if x.errno != errno.ENOENT:
raise milter.error(x)
# The default flags set include everything
# milter.set_flags(milter.ADDHDRS)
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
# arbitrary keywords without crashing. We do provide envcallback and
# dictfromlist to make parsing the ESMTP args convenient.
milter.set_envfrom_callback(lambda ctx,*str: ctx.getpriv().envfrom(*str))
milter.set_envrcpt_callback(lambda ctx,*str: ctx.getpriv().envrcpt(*str))
milter.set_header_callback(lambda ctx,fld,val: ctx.getpriv().header(fld,val))
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
milter.set_abort_callback(lambda ctx: ctx.getpriv().abort())
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,
data=lambda ctx: ctx.getpriv().data(),
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
negotiate=ncb
)
start_seq = _seq
try:
milter.main()
except milter.error:
if start_seq == _seq: raise # couldn't start
# milter has been running for a while, but now it can't start new threads
raise milter.error("out of thread resources")
__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
#
+161
View File
@@ -0,0 +1,161 @@
# Email address list with expiration
#
# This class acts like a map. Entries with a value of None are persistent,
# but disappear after a time limit. This is useful for automatic whitelists
# and blacklists with expiration. The persistent store is a simple ascii
# file with sender and timestamp on each line. Entries can be appended
# to the store, and will be picked up the next time it is loaded.
#
# Entries with other values are not persistent. This is used to hold failed
# 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
# tighten up MAIL FROM parsing to match RFC.
#
# Revision 1.7 2007/01/25 22:47:26 customdesigned
# Persist blacklisting from delayed DSNs.
#
# Revision 1.6 2007/01/19 23:31:38 customdesigned
# Move parse_header to Milter.utils.
# Test case for delayed DSN parsing.
# Fix plock when source missing or cannot set owner/group.
#
# Revision 1.5 2007/01/11 19:59:40 customdesigned
# Purge old entries in auto_whitelist and send_dsn logs.
#
# Revision 1.4 2007/01/11 04:31:26 customdesigned
# Negative feedback for bad headers. Purge cache logs on startup.
#
# Revision 1.3 2007/01/08 23:20:54 customdesigned
# Get user feedback.
#
# Revision 1.2 2007/01/05 23:33:55 customdesigned
# Make blacklist an AddrCache
#
# Revision 1.1 2007/01/05 21:25:40 customdesigned
# Move AddrCache to Milter package.
#
# 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 time
from plock import PLock
class AddrCache(object):
time_format = '%Y%b%d %H:%M:%S %Z'
def __init__(self,renew=7,fname=None):
self.age = renew
self.cache = {}
self.fname = fname
def load(self,fname,age=0):
"Load address cache from persistent store."
if not age:
age = self.age
self.fname = fname
cache = {}
self.cache = cache
now = time.time()
lock = PLock(self.fname)
wfp = lock.lock()
changed = False
try:
too_old = now - age*24*60*60 # max age in days
try:
fp = open(self.fname)
except OSError:
fp = ()
for ln in fp:
try:
rcpt,ts = ln.strip().split(None,1)
try:
l = time.strptime(ts,AddrCache.time_format)
t = time.mktime(l)
if t < too_old:
changed = True
continue
cache[rcpt.lower()] = (t,None)
except: # unparsable timestamp - likely garbage
changed = True
continue
except: # manual entry (no timestamp)
cache[ln.strip().lower()] = (now,None)
wfp.write(ln)
if changed:
lock.commit(self.fname+'.old')
else:
lock.unlock()
except IOError:
lock.unlock()
def has_precise_key(self,sender):
"""True if precise sender is cached and has not expired. Don't
try looking up wildcard entries.
"""
try:
lsender = sender and sender.lower()
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 True
del self.cache[lsender]
except KeyError: pass
return False
def has_key(self,sender):
"True if sender is cached and has not expired."
if self.has_precise_key(sender):
return True
try:
user,host = sender.split('@',1)
return self.has_precise_key(host)
except: pass
return False
__contains__ = has_key
def __getitem__(self,sender):
try:
lsender = sender.lower()
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
del self.cache[lsender]
raise KeyError, sender
except KeyError,x:
try:
user,host = sender.split('@',1)
return self.__getitem__(host)
except ValueError:
raise x
def addperm(self,sender,res=None):
"Add a permanent sender."
lsender = sender.lower()
if self.has_key(lsender):
ts,res = self.cache[lsender]
if not ts: return # already permanent
self.cache[lsender] = (None,res)
if not res:
print >>open(self.fname,'a'),sender
def __setitem__(self,sender,res):
lsender = sender.lower()
now = time.time()
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
def __len__(self):
return len(self.cache)
+66
View File
@@ -0,0 +1,66 @@
from ConfigParser import ConfigParser
import os.path
class MilterConfigParser(ConfigParser):
def __init__(self,defaults={}):
ConfigParser.__init__(self)
self.defaults = defaults
# The defaults provided by ConfigParser show up in all sections,
# which screws up iterating over all options in a section.
# Worse, passing "defaults" with vars= overrides the config file!
# So we roll our own defaults.
def get(self,sect,opt):
if not self.has_option(sect,opt) and opt in self.defaults:
return self.defaults[opt]
return ConfigParser.get(self,sect,opt)
def getlist(self,sect,opt):
if self.has_option(sect,opt):
return [q.strip() for q in self.get(sect,opt).split(',')]
return []
def getaddrset(self,sect,opt,dir=''):
if not self.has_option(sect,opt):
return {}
s = self.get(sect,opt)
d = {}
for q in s.split(','):
q = q.strip()
if q.startswith('file:'):
domain = q[5:].lower()
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)
return d
def getaddrdict(self,sect,opt,dir=''):
if not self.has_option(sect,opt):
return {}
d = {}
for q in self.get(sect,opt).split(','):
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 = 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
+123
View File
@@ -0,0 +1,123 @@
## @package Milter.dns
# Provide a higher level interface to pydns.
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()
# key k: ('wayforward.net', 'A'), value v
# FIXME: pydns returns AAAA RR as 16 byte binary string, but
# A RR as dotted quad. For consistency, this driver should
# return both as binary string.
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
except IOError, x:
raise DNSError, str(x)
class Session(object):
"""A Session object has a simple cache with no TTL that is valid
for a single "session", for example an SMTP conversation."""
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')
))
## 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.
If the result is in cache, return that. Otherwise pull the
result from DNS, and cache ALL answers, so additional info
is available for further queries later.
CNAMEs are followed.
If there is no data, [] is returned.
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)
result = self.cache.get( (name, qtype) )
cname = None
if result: return result
cnamek = (name,'CNAME')
cname = self.cache.get( cnamek )
if cname:
cname = cname[0]
else:
safe2cache = Session.SAFE2CACHE
for k, v in DNSLookup(name, qtype):
if k == cnamek:
cname = v
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:
if not cnames:
cnames = {}
elif len(cnames) >= MAX_CNAME:
#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')
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)
+235
View File
@@ -0,0 +1,235 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2005 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
# 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.
#
# Revision 1.14 2007/03/03 18:19:40 customdesigned
# Handle DNS error sending DSN.
#
# Revision 1.13 2007/01/04 18:01:11 customdesigned
# Do plain CBV when template missing.
#
# Revision 1.12 2006/07/26 16:37:35 customdesigned
# Support timeout.
#
# Revision 1.11 2006/06/21 21:07:11 customdesigned
# Include header fields in DSN template.
#
# 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.
#
import smtplib
import socket
from email.Message import Message
import Milter
import time
import dns
## 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.rsplit('@',1)
if not session: session = dns.Session()
try:
mxlist = session.dns(domain,'MX')
except dns.DNSError:
return (450,'DNS Timeout: %s MX'%domain) # temp error
if not mxlist:
mxlist = (0,domain), # fallback to A record when no MX
else:
mxlist.sort()
smtp = smtplib.SMTP()
toolate = time.time() + timeout
for prior,host in mxlist:
try:
smtp.connect(host)
code,resp = smtp.helo(receiver)
# some wiley spammers have MX records that resolve to 127.0.0.1
a = resp.split()
if not a:
return (553,'MX for %s has no hostname in banner: %s' % (domain,host))
if a[0] == receiver:
return (553,'Fraudulent MX for %s: %s' % (domain,host))
if not (200 <= code <= 299):
raise smtplib.SMTPHeloError(code, resp)
if msg:
try:
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)
else: # CBV
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:
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:
return x.args # permanent error
except smtplib.SMTPException:
pass # any other error, try next MX
except socket.error:
pass # MX didn't accept connections, try next one
except socket.timeout:
pass # MX too slow, try next one
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
class Vars: pass
# NOTE: Caller can pass an object to create_msg that in a typical milter
# collects things like heloname or sender anyway.
def create_msg(v,rcptlist=None,origmsg=None,template=None):
"""Create a DSN message from a template. Template must be '\n' separated.
v - an object whose attributes are used for substitutions. Must
have sender and receiver attributes at a minimum.
rcptlist - used to set v.rcpt if given
origmsg - used to set v.subject and v.spf_result if given
template - a '\n' separated string with python '%(name)s' substitutions.
"""
if not template:
return None
if hasattr(v,'perm_error'):
# likely to be an spf.query, try translating for backward compatibility
q = v
v = Vars()
try:
v.heloname = q.h
v.sender = q.s
v.connectip = q.i
v.receiver = q.r
v.sender_domain = q.o
v.result = q.result
v.perm_error = q.perm_error
except: v = q
if rcptlist:
v.rcpt = '\n\t'.join(rcptlist)
if origmsg:
try: v.subject = origmsg['Subject']
except: v.subject = '(none)'
try:
v.spf_result = origmsg['Received-SPF']
except: v.spf_result = None
msg = Message()
msg.add_header('X-Mailer','PyMilter-'+Milter.__version__)
msg.set_type('text/plain')
hdrs,body = template.split('\n\n',1)
for ln in hdrs.splitlines():
name,val = ln.split(':',1)
msg.add_header(name,(val % v.__dict__).strip())
msg.set_payload(body % v.__dict__)
# add headers if missing from old template
if 'to' not in msg:
msg.add_header('To',v.sender)
if 'from' not in msg:
msg.add_header('From','postmaster@%s'%v.receiver)
if 'auto-submitted' not in msg:
msg.add_header('Auto-Submitted','auto-generated')
return msg
if __name__ == '__main__':
import spf
q = spf.query('192.168.9.50',
'SRS0=pmeHL=RH==stuart@example.com',
'red.example.com',receiver='mail.example.com')
q.result = 'softfail'
q.perm_error = None
msg = create_msg(q,['charlie@example.com'],None,
"""From: postmaster@%(receiver)s
To: %(sender)s
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())
+96
View File
@@ -0,0 +1,96 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2005 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
# Heuristically determine whether a domain name is for a dynamic IP.
# examples we don't yet recognize:
#
# 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)
import re
ip3 = re.compile('[0-9]{1,3}')
hpats = (
'h[0-9a-f]{12}[.]',
'h\d*n\d*c\d*o\d*\.',
'pcp\d{6,10}pcs[.]',
'no-reverse',
'S[0-9a-f]{16}[.][a-z]{2}[.]',
'user<3>\.',
'[Cc]ust<3>\.',
'^<3>\.',
'ppp[^.]*<3>\.',
'-ppp\d*\.',
'\d*-<3>\.',
'[0-9a-f]{1,3}-<3>\.',
'p<3>\.pool',
'h<3>\.',
'xdsl-\d*\.',
'-\d*-\d*\.',
'\.adsl\.',
'\.cable\.'
)
rehmac = re.compile('|'.join(hpats))
def is_dynip(host,addr):
"""Return True if hostname is for a dynamic ip.
Examples:
>>> is_dynip('post3.fabulousdealz.com','69.60.99.112')
False
>>> is_dynip('adsl-69-208-201-177.dsl.emhril.ameritech.net','69.208.201.177')
True
>>> is_dynip('[1.2.3.4]','1.2.3.4')
True
>>> is_dynip('c-71-63-151-151.hsd1.mn.comcast.net','71.63.151.151')
True
"""
if host.startswith('[') and host.endswith(']'):
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)
h = host
m = ip3.findall(host)
if m:
g = map(int,m)[:4]
ia3 = (ia[1:],ia[:3])
if g[-3:] in ia3: return True
if g[0] == ia[3] and g[1:3] == ia[:2]: return True
if g[-2:] == ia[2:]: return True
g.reverse()
if g[:3] in ia3: return True
if g[:2] == ia[2:]: return True
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
if rehmac.search(h): return True
if host.find(''.join(a[:3])) >= 0: return True
if host.find(''.join(a[1:])) >= 0: return True
x = "%02x%02x%02x%02x" % tuple(ia)
if host.lower().find(x) >= 0: return True
return False
if __name__ == '__main__':
import fileinput
import sets
seen = sets.Set()
for ln in fileinput.input():
a = ln.split()
if a[3:5] == ['connect','from']:
host = a[5]
if host.startswith('[') and host.endswith(']'):
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)
else:
print '%s\t%s' % (ip,host)
+102
View File
@@ -0,0 +1,102 @@
import time
import shelve
import thread
import logging
import urllib
log = logging.getLogger('milter.greylist')
def quoteAddress(s):
'''Quote an address so that it's safe to store in the file-system.
Address can either be a domain name, or local part.
Returns the quoted address.'''
s = urllib.quote(s, '@_-+~!.%')
if s.startswith('.'): s = '%2e' + s[1:]
return s
class Record(object):
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
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):
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.dbp = shelve.open(dbname,'c',protocol=2)
self.lock = thread.allocate_lock()
def clean(self,timeinc=0):
"Delete records past the retention limit."
now = time.time() + timeinc
cnt = 0
dbp = self.dbp
for key, r in dbp.iteritems():
#print key,r,time.ctime(now)
if now > r.lastseen + self.greylist_retain:
self.lock.acquire()
try:
r = dbp[key]
now = time.time() + timeinc
if now > r.lastseen + self.greylist_retain:
del dbp[key]
cnt += 1
finally:
self.lock.release()
return cnt
def check(self,ip,sender,recipient,timeinc=0):
"Return number of allowed messages for greylist triple."
sender = quoteAddress(sender)
recipient = quoteAddress(recipient)
key = ip + ':' + sender + ':' + recipient
self.lock.acquire()
try:
dbp = self.dbp
try:
r = dbp[key]
now = time.time() + timeinc
if now > r.lastseen + self.greylist_retain:
# expired
log.debug('Expired greylist: %s',key)
r = Record(timeinc)
elif now < r.firstseen + self.greylist_time + 5:
# still greylisted
log.debug('Early greylist: %s',key)
#r = Record(timeinc)
r.lastseen = now
elif r.cnt or now < r.firstseen + self.greylist_expire:
# in greylist window or active
r.lastseen = now
r.cnt += 1
log.debug('Active greylist(%d): %s',r.cnt,key)
else:
# passed greylist window
log.debug('Late greylist: %s',key)
r = Record(timeinc)
dbp[key] = r
except:
r = Record(timeinc)
dbp[key] = r
dbp.sync()
finally:
self.lock.release()
return r.cnt
def close(self):
self.dbp.close()
+86
View File
@@ -0,0 +1,86 @@
import time
import logging
import urllib
import sqlite3
import thread
from datetime import datetime
log = logging.getLogger('milter.greylist')
_db_lock = thread.allocate_lock()
class Greylist(object):
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
self.ignoreLastByte = False
self.greylist_time = grey_time * 60 # minutes
self.greylist_expire = grey_expire * 3600 # hours
self.greylist_retain = grey_retain * 24 * 3600 # days
self.conn = sqlite3.connect(dbname)
self.conn.row_factory = sqlite3.Row
try:
self.conn.execute('''create table greylist(
ip text , sender text, recipient text,
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
primary key (ip,sender,recipient))''')
except: pass
def 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()
+66
View File
@@ -0,0 +1,66 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
import os
from time import sleep
class PLock(object):
"A simple /etc/passwd style lock,update,rename protocol for updating files."
def __init__(self,basename):
self.basename = basename
self.fp = None
def lock(self,lockname=None,mode=0660,strict_perms=False):
"Start an update transaction. Return FILE to write new version."
self.unlock()
if not lockname:
lockname = self.basename + '.lock'
self.lockname = lockname
try:
st = os.stat(self.basename)
mode |= st.st_mode
except OSError: pass
u = os.umask(0002)
try:
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
finally:
os.umask(u)
self.fp = os.fdopen(fd,'w')
try:
os.chown(self.lockname,-1,st.st_gid)
except:
if strict_perms:
self.unlock()
raise
return self.fp
def wlock(self,lockname=None):
"Wait until lock is free, then start an update transaction."
while True:
try:
return self.lock(lockname)
except OSError:
sleep(2)
def commit(self,backname=None):
"Commit update transaction with optional backup file."
if not self.fp:
raise IOError,"File not locked"
self.fp.close()
self.fp = None
if backname:
try:
os.remove(backname)
except OSError: pass
os.link(self.basename,backname)
os.rename(self.lockname,self.basename)
def unlock(self):
"Cancel update transaction."
if self.fp:
try:
self.fp.close()
except: pass
self.fp = None
os.remove(self.lockname)
+117
View File
@@ -0,0 +1,117 @@
"""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.
"""
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,x: print x
::1.2.3.4.5
"""
if p == '::':
return '\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
+192
View File
@@ -0,0 +1,192 @@
## @package Milter.test
# A test framework for milters
import rfc822
import StringIO
import Milter
Milter.NOREPLY = Milter.CONTINUE
## Test mixin for unit testing milter applications.
# This mixin overrides many Milter.MilterBase methods
# with stub versions that simply record what was done.
# @since 0.9.8
class TestBase(object):
def __init__(self,logfile='test/milter.log'):
self._protocol = 0
self.logfp = open(logfile,"a")
## List of recipients deleted
self._delrcpt = []
## List of recipients added
self._addrcpt = []
## Macros defined
self._macros = { }
## The message body.
self._body = None
## True if the milter replaced the message body.
self._bodyreplaced = False
## True if the milter changed any headers.
self._headerschanged = False
## Reply codes and messages set by milter
self._reply = None
## The rfc822 message object for the current email being fed to the milter.
self._msg = None
self._symlist = [ None, None, None, None, None, None, None ]
def log(self,*msg):
for i in msg: print >>self.logfp, i,
print >>self.logfp
## Set a macro value.
# These are retrieved by the milter with getsymval.
# @param name the macro name, as passed to getsymval
# @param val the macro value
def setsymval(self,name,val):
self._macros[name] = val
def getsymval(self,name):
# FIXME: track stage, and use _symlist
return self._macros.get(name,'')
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self._bodyreplaced = True
else:
raise IOError,"replacebody not called from eom()"
# FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a milter
def chgheader(self,field,idx,value):
if not self._body:
raise IOError,"chgheader not called from eom()"
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '':
del self._msg[field]
else:
self._msg[field] = value
self._headerschanged = True
def addheader(self,field,value,idx=-1):
if not self._body:
raise IOError,"addheader not called from eom()"
self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value
self._headerschanged = True
def delrcpt(self,rcpt):
if not self._body:
raise IOError,"delrcpt not called from eom()"
self._delrcpt.append(rcpt)
def addrcpt(self,rcpt):
if not self._body:
raise IOError,"addrcpt not called from eom()"
self._addrcpt.append(rcpt)
## Save the reply codes and messages in self._reply.
def setreply(self,rcode,xcode,*msg):
self._reply = (rcode,xcode) + msg
def setsymlist(self,stage,macros):
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
# not used yet, but just for grins we save the data
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(' ')
except: pass
a += m
self._symlist[stage] = set(a)
## Feed a file like object to the milter. Calls envfrom, envrcpt for
# each recipient, header for each header field, body for each body
# block, and finally eom. A return code from the milter other than
# CONTINUE returns immediately with that return code.
#
# This is a convenience method, a test could invoke the callbacks
# in sequence on its own - and for some complex tests, this may
# be necessary.
# @param fp the file with rfc2822 message stream
# @param sender the MAIL FROM
# @param rcpt RCPT TO - additional recipients may follow
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
self._body = None
self._bodyreplaced = False
self._headerschanged = False
self._reply = None
msg = rfc822.Message(fp)
rc = self.envfrom('<%s>'%sender)
if rc != Milter.CONTINUE: return rc
for rcpt in (rcpt,) + rcpts:
rc = self.envrcpt('<%s>'%rcpt)
if rc != Milter.CONTINUE: return rc
line = None
for h in msg.headers:
if h[:1].isspace():
line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
if len(s) > 1: val = s[1].strip()
else: val = ''
rc = self.header(s[0],val)
if rc != Milter.CONTINUE: return rc
line = h
if line:
s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
while 1:
buf = fp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = StringIO.StringIO()
rc = self.eom()
if self._bodyreplaced:
body = self._body.getvalue()
else:
msg.rewindbody()
body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body)
return rc
## Feed an email contained in a file to the milter.
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
# @param sender MAIL FROM
# @param rcpts RCPT TO, multiple recipients may be supplied
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
with open('test/'+fname,'r') as fp:
return self.feedFile(fp,sender,*rcpts)
## Call the connect and helo callbacks.
# The helo callback is not called if connect does not return CONTINUE.
# @param host the hostname passed to the connect callback
# @param helo the hostname passed to the helo callback
# @param ip the IP address passed to the connect callback
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
self._body = None
self._bodyreplaced = False
opts = [ Milter.CURR_ACTS,~0,0,0 ]
rc = self.negotiate(opts)
rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE:
self.close()
return rc
rc = self.hello(helo)
if rc != Milter.CONTINUE:
self.close()
return rc
+17
View File
@@ -0,0 +1,17 @@
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2005 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
# The localpart of SMTP return addresses is often signed. The format
# of the signing is application specific and doesn't concern us -
# except that we wish to extract some sort of fixed string from
# the variable signature which represents the "source" of the message.
def unsign(s):
"""Attempt to unsign localpart and return original email.
No attempt is made to verify the signature.
>>> unsign('SRS0=8Y3CZ=3U=jsconnor.com=bills@bmsi.com')
'bills@jsconnor.com'
"""
# not implemented yet
return s
+202
View File
@@ -0,0 +1,202 @@
## @package Milter.utils
# Miscellaneous functions.
#
import re
import struct
import socket
import email.Errors
from fnmatch import fnmatchcase
from email.Header import decode_header
#import email.Utils
import rfc822
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(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)
def bin2long6(s):
"""Convert binary IP6 address into an unsigned Python long integer."""
h, l = struct.unpack("!QQ", s)
return h << 64 | l
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 = 0xFFFFFFFFL
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
def cidr(i,n,mask=MASK):
return ~(mask >> n) & mask & i
def iniplist(ipaddr,iplist):
"""Return whether ip is in cidr list
>>> iniplist('66.179.26.146',['127.0.0.1','66.179.26.128/26'])
True
>>> iniplist('127.0.0.1',['127.0.0.1','66.179.26.128/26'])
True
>>> iniplist('192.168.0.45',['192.168.0.*'])
True
>>> 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
"""
if ip4re.match(ipaddr):
ipnum = addr2bin(ipaddr)
elif ip6re.match(ipaddr):
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])
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 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>.
#
def parseaddr(t):
"""Split email into Fullname and address.
>>> parseaddr('user@example.com')
('', 'user@example.com')
>>> parseaddr('"Full Name" <foo@example.com>')
('Full Name', 'foo@example.com')
>>> parseaddr('spam@spammer.com <foo@example.com>')
('spam@spammer.com', 'foo@example.com')
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
('God@heaven', 'jeff@spec.org')
>>> parseaddr('Real Name ((comment)) <addr...@example.com>')
('Real Name', 'addr...@example.com')
>>> parseaddr('a(WRONG)@b')
('WRONG', 'a@b')
"""
#return email.Utils.parseaddr(t)
res = rfc822.parseaddr(t)
# dirty fix for some broken cases
if not res[0]:
pos = t.find('<')
if pos > 0 and t[-1] == '>':
addrspec = t[pos+1:-1]
pos1 = addrspec.rfind(':')
if pos1 > 0:
addrspec = addrspec[pos1+1:]
return rfc822.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
if not res[1]:
pos = t.find('<')
if pos > 0 and t[-1] == '>':
addrspec = t[pos+1:-1]
pos1 = addrspec.rfind(':')
if pos1 > 0:
addrspec = addrspec[pos1+1:]
return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
return res
def parse_addr(t):
"""Split email into user,domain.
>>> 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']
>>> 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:]]
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.
"""
try:
h = decode_header(val)
if not len(h) or (not h[0][1] and len(h) == 1): return val
u = []
for s,enc in h:
if enc:
try:
u.append(unicode(s,enc,'replace'))
except LookupError:
u.append(unicode(s))
else:
u.append(unicode(s))
u = ''.join(u)
for enc in ('us-ascii','iso-8859-1','utf8'):
try:
return u.encode(enc)
except UnicodeError: continue
except UnicodeDecodeError: pass
except LookupError: pass
except ValueError: pass
except email.Errors.HeaderParseError: pass
return val
-163
View File
@@ -1,163 +0,0 @@
Here is a history of user visible changes to Python milter.
0.8.0 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.
Properly log pydspam exceptions
0.7.3 Experimental release with python2.4 support
0.7.2 Return unknown for invalid ip address in mechanism
Recognize dynamic PTR names, and don't count them as authentication.
Three strikes and yer out rule.
Block softfail by default when no PTR or HELO
Return unknown for null mechanism
Try best guess on HELO also
Expand setreply for common errors
make rhsbl.m4 hack available for sendmail.mc
0.7.1 Handle modifying mislabeled multipart messages without an exception
Support setbacklog, setmlreply
Allow multi-recipient CBV
Return TEMPFAIL for SPF softfail
0.7.0 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
0.6.9 Reject invalid SRS immediately for benefit of callback verifiers
Fix include bug in spf.py
Fix check_header bug
Fix setup.py to work with python < 2.2.3, thanks to Eric S. Johansson
Test driver for SPF test suite. Fix bugs and add features to
pass most of test suite.
Use best_guess() and get_header() in bms.py for SPF support
0.6.8 Defang message/rfc822 content_type with boundary
Support SPF delegation
Reject neutral SPF result for selected domains
Support SPF default (best_guess)
Don't report "spoofed" unless rcpt looks like SRS
Check for bounce with multiple rcpts
Make dspam see Received-SPF headers
Fix sysv init for Redhat 9 and other single ps line per process systems
0.6.7 Fix failure to remove explicit unix socket thanks to Alexander again.
Support SRS forgery detection.
Detect thread resource starvation in Milter.py.
Decode obfuscated subject headers.
0.6.6 Another memory leak plugged by Alexander Kourakos.
Support SPF checking: http://spf.pobox.com
Hello blacklist
RPM compiled for python2.3 and sendmail-8.12
0.6.5 Plug memory leak in wrap_connect thanks to Alexander Kourakos.
Support progress notification.
Log Received header for trusted relay.
Support wildcard user for smart alias.
0.6.4 Exempt entire domains.
Tweak SMTP error codes reported.
Suppress traceback for Dspam lock timeouts.
Dspam internal mail for dspam users.
Match hostname for internal connection test, even if no ipaddr.
Fix for not saving defang of false positive triggered rejecting it
as a virus from self.
Size limit for dspam to work around dspam-2.6.5.2 bug.
(dspam-2.8 still showstopper buggy for libdspam API.)
Whitelist for dspam.
Reject list for dspam (REJECT rather than quarantine SCREENed
spam for listed domains).
Report dspam header changes to sendmail, fix headerChange
to handle deleting absent header.
dspam feature requires pydspam-1.1.5
0.6.3 dspam screening (with pydspam-1.1.4)
Don't write "defang" file for false positive feedback
0.6.2 Work around email package bug in get_filename().
add dspam_exempt list to milter.cfg
REJECT messages with missing MIME boundaries (almost always spam)
DISCARD messages which any dspam user flags as spam
start.sh was calling python instead of python2 on Linux
0.6.1 Work with python-2.2.3
Integrate full dspam application
0.6.0 Use email package in python-2.2.2
0.5.6 Include dspam interface for Bayesian filtering
0.5.5 Allow passing None to setreply and chgheader thanks to George Graf.
Experimental IPv6 support thanks to Deron Meranda.
Allow removing callbacks by passing None to set_XXX_callback.
Recognize internal connections in bms.py.
Give users a clue when rejecting banned subjects.
0.5.4 Wiretap redirection feature, smart alias feature, QUARANTINE support
0.5.3 Tweak to run under 2.2 in production
0.5.2 Fix and add to unit test another parsing failure.
0.5.1 Properly handle modifications to rfc822 attachments.
Handle encoded rfc822 attachments.
0.5.0 Use config file so users don't have to keep syncing the
bms.py script. Keep bms.py marked as %config for a while
to avoid wiping out their customizations just yet.
0.4.5 Work with sgmlop package to speed up HTML parsing.
Reduce various local hacks to config variables.
0.4.4 Bug fixes for HTML encoding.
0.4.3 Handle quoted-printable HTML attachments. Remove entire
attachment when HTML can't be parsed.
0.4.2 Parse HTML attachments to remove <script ...>...</script>.
Klez virus uses malformed MIME part separators to prevent
the multifile module and other virus scanners from seeing its
HTML attachment (which contains Javascript and VBScript). Outhouse
happily accepts and executes the malformed attachments, but
we still kill the Klez virus because we:
Defang attachment when any Content-Type attribute ends with
a banned extension - one of the Outhouse bugs exploited by the
Klez virus. Outhouse really, really stinks . . .
0.4.1 Bug fix from Jason Erikson for NULL hostaddr in connect callback.
0.4.0 New check_attachments(msg,check) function in mime module allows
filtering based on attachment contents. Distribution now includes
bms.py, an example milter used in production - including use of the
new check_attachments(msg,check) API.
Report hostname in WARNING.TXT.
More parameter list bug fixes.
0.3.10 Parse quotes in parameter lists to handle embedded ';'.
Move test data to subdirectory, write non-junit output to
log file in test subdirectory.
0.3.9 Handle non-multipart messages with executable content in sample.py,
add more extensions to banned list.
0.3.8 Handle malformed Content-Type in mime.py. Test viruses have
been deactivated by deleting most of the viral code.
0.3.7 Put back hint on running sample.py. Add .bat as banned extension.
More sample spam filtering logic.
0.3.6 Ran through pychecker-0.8.5. Most systems will name the sendmail
user library (used by the milter extension module) 'libsm', but AIX
still needs to call it 'libsmutil' because there is a system library
called 'libsm'.
0.3.5 Enhanced logging. Fix bug in sample milter where headers were
included in body when removing a virus.
0.3.4 Tested distribution on RH6.2 and updated sample.py and docs.
Tested with gcc-2.95.2, python-2.1.1, sendmail-8.11.6-2.6.x
The RH6.2 spec file to enable libmilter for sendmail-8.11.6
can be obtained from http://www.bmsi.com/linux/sendmail-rhmilter.spec
The SRPM can be obtained from http://www.redhat.com
0.3.3 Remove reference to sa_len - not supported by linux.
0.3.2 Rename and add more hints to the sample milter.
0.3.1 Pass a more useful hostaddr to the connect callback.
0.3 Interface now uses a milterContext extension object instead of
an index. A PyThreadContext is now created for each milterContext so that
"simultaneously" processing multiple messages at once (as often happens
on a busy server) actually works.
Many milter methods are now object methods of the milterContext
extension object. No compatibility API is provided for this change due
to the limited user base at this stage. The setname method has been removed,
and the name is now passed to register.
A simple class to provide an OO wrapper to the milter API is
provided.
A simple class to parse multipart mime messages into parts and replace
selected parts is provided. The sample filter will eventually use the mimelib
package instead, but mimelib currently requires reading the entire message
into memory.
A sample filter that replaces attachments with naughty extensions
with a warning message is provided.
-253
View File
@@ -1,253 +0,0 @@
%define name milter
%define version 0.8.0
%define release 3.RH7
# what version of RH are we building for?
%define redhat9 0
%define redhat7 1
%define redhat6 0
# Options for Redhat version 6.x:
# rpm -ba|--rebuild --define "rh6 1"
%{?rh6:%define redhat7 0}
%{?rh6:%define redhat6 1}
# some systems dont have initrddir defined
%{?_initrddir:%define _initrddir /etc/rc.d/init.d}
%if %{redhat9}
%define sysvinit milter.rc
%else # Redhat 7.x and earlier (multiple ps lines per thread)
%define sysvinit milter.rc7
%endif
# RH9, other systems (single ps line per process)
%ifos Linux
%define python python2.4
%else
%define python python
%endif
Summary: Python interface to sendmail milter API
Name: %{name}
Version: %{version}
Release: %{release}
Source: %{name}-%{version}.tar.gz
#Patch: %{name}-%{version}.patch
Copyright: GPL
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot
Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, sendmail >= 8.12.10
%ifos Linux
Requires: chkconfig
%endif
BuildRequires: %{python}-devel , sendmail-devel >= 8.12.10
%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.
%prep
%setup
#%patch -p1
%build
env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
mkdir -p $RPM_BUILD_ROOT/var/log/milter
mkdir -p $RPM_BUILD_ROOT/etc/mail
mkdir $RPM_BUILD_ROOT/var/log/milter/save
cp bms.py strike3.txt softfail.txt $RPM_BUILD_ROOT/var/log/milter
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
# 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
}
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
EOF
chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
%ifos aix4.1
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF'
#!/bin/sh
cd /var/log/milter
# uncomment to enable sgmlop if installed
#export PYTHONPATH=/usr/local/lib/python2.1/site-packages
exec /usr/local/bin/python bms.py >>milter.log 2>&1
EOF
%else
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF'
#!/bin/sh
cd /var/log/milter
exec >>milter.log 2>&1
%{python} bms.py &
echo $! >/var/run/milter/milter.pid
EOF
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
/^python=/
c
python="%{python}"
.
w
q
EOF
%endif
chmod a+x $RPM_BUILD_ROOT/var/log/milter/start.sh
mkdir -p $RPM_BUILD_ROOT/var/run/milter
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 /var/log/milter/start.sh -u 25 -S -n 15 -f 9 -G mail || :
%preun
if [ $1 = 0 ]; then
rmssys -s milter || :
fi
%else
%post
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
/sbin/chkconfig --add milter
%preun
if [ $1 = 0 ]; then
/sbin/chkconfig --del milter
fi
%endif
%clean
rm -rf $RPM_BUILD_ROOT
%files -f INSTALLED_FILES
%defattr(-,root,root)
%doc README NEWS TODO CREDITS sample.py
/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/run/milter
%dir /var/log/milter/save
%config /var/log/milter/start.sh
%config /var/log/milter/bms.py
%config /var/log/milter/strike3.txt
%config /var/log/milter/softfail.txt
%config(noreplace) /etc/mail/pymilter.cfg
/usr/share/sendmail-cf/hack/rhsbl.m4
%changelog
* 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