Compare commits

...

285 Commits

Author SHA1 Message Date
cvs2svn 59f8c39e60 This commit was manufactured by cvs2svn to create tag 'pymilter-0_9_1'.
Sprout from master 2009-02-06 04:29:49 UTC Stuart Gathman <stuart@gathman.org> 'Release 0.9.1'
Cherrypick from bmsi 2005-05-31 18:23:49 UTC Stuart Gathman <stuart@gathman.org> 'Development changes since 0.7.2':
    sample.py
    test/amazon
    test/big5
    test/bounce
    test/bounce1
    test/bound
    test/honey
    test/missingboundary
    test/samp1
    test/spam44
    test/spam7
    test/spam8
    test/test1
    test/test8
    test/virus1
    test/virus13
    test/virus2
    test/virus3
    test/virus4
    test/virus5
    test/virus6
    test/virus7
    testsample.py
2009-02-06 04:29:50 +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
Stuart Gathman 9a969e8f60 Release 0.8.0 2005-06-14 22:02:46 +00:00
Stuart Gathman 3d7003a638 Web site updates. 2005-06-14 22:02:09 +00:00
Stuart Gathman f643cafc04 Check internal_domains for outgoing mail. 2005-06-14 21:55:30 +00:00
Stuart Gathman 09582a2e86 fix pychecker nits 2005-06-14 20:31:26 +00:00
Stuart Gathman 7eb2fb09ef Properly log exceptions from pydspam 2005-06-06 18:24:59 +00:00
Stuart Gathman 07c56ce667 include DSN templates 2005-06-05 02:44:41 +00:00
Stuart Gathman ecb870acaa Fix bugs from testing RPM 2005-06-04 19:41:17 +00:00
Stuart Gathman e99117e8f6 Organize config reader by section. Create defang section. 2005-06-03 04:57:05 +00:00
Stuart Gathman 0283c20eef Configure banned extensions. Scan zipfile option with test case. 2005-06-02 15:00:17 +00:00
Stuart Gathman bdc6b71845 Update copyright notices after reading article on /. 2005-06-02 04:18:55 +00:00
Stuart Gathman 053734d435 Record timestamp in send_dsn.log 2005-06-02 02:09:00 +00:00
Stuart Gathman 56f1f58be8 Reject on PermErr 2005-06-02 02:08:12 +00:00
Stuart Gathman 5d6ceaefe4 Support configurable templates for DSNs. 2005-06-02 01:00:37 +00:00
Stuart Gathman 1d10bb172f Create Milter package. 2005-05-31 20:39:16 +00:00
Stuart Gathman 8e93d4be38 Move development to sourceforge. 2005-05-31 20:34:40 +00:00
Stuart Gathman ea81a31044 Clear unknown mechanism list at proper time. 2005-05-31 18:57:59 +00:00
Stuart Gathman 2ad3e1cd6e This commit was generated by cvs2svn to track changes on a CVS vendor
branch.
2005-05-31 18:23:49 +00:00
Stuart Gathman 9fb3ad70d4 Development changes since 0.7.2 2005-05-31 18:23:49 +00:00
Stuart Gathman b056551e16 This commit was generated by cvs2svn to track changes on a CVS vendor
branch.
2005-05-31 18:10:47 +00:00
Stuart Gathman 20fb6efab0 Release 0.7.2 2005-05-31 18:10:47 +00:00
Stuart Gathman 6277f05e6a This commit was generated by cvs2svn to track changes on a CVS vendor
branch.
2005-05-31 18:09:06 +00:00
Stuart Gathman 16dea6e187 Release 0.7.1 2005-05-31 18:09:06 +00:00
Stuart Gathman 19ad88b6b2 This commit was generated by cvs2svn to track changes on a CVS vendor
branch.
2005-05-31 18:08:20 +00:00
Stuart Gathman 802dc01c84 Release 0.7.0 2005-05-31 18:08:20 +00:00
Stuart Gathman e688112eed This commit was generated by cvs2svn to track changes on a CVS vendor
branch.
2005-05-31 18:07:19 +00:00
Stuart Gathman c510c4576f This commit was generated by cvs2svn to track changes on a CVS vendor
branch.
2005-05-31 18:04:05 +00:00
44 changed files with 2865 additions and 3939 deletions
+339
View File
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+12 -1
View File
@@ -5,10 +5,18 @@ wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a it with distutils, and generally transformed it from a quick hack to a
real, usable Python extension. real, usable Python extension.
Other contributors: Other contributors (in random order):
Dwayne Litzenberger, B.A.Sc.
for library_dirs patch to compile on Debian
Dave MacQuigg
for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter.
Terence Way Terence Way
for providing a Python port of SPF for providing a Python port of SPF
Scott Kitterman
for doing lots of testing and debugging of SPF against draft standard,
and for putting up a web page that validates SPF records using spf.py
Alexander Kourakos Alexander Kourakos
for plugging several memory leaks for plugging several memory leaks
George Graf at Vienna University of Economics and Business Administration George Graf at Vienna University of Economics and Business Administration
@@ -22,6 +30,9 @@ John Draper
then pointing out that it would be easier to just write the MTA in Python. then pointing out that it would be easier to just write the MTA in Python.
Eric S. Johansson Eric S. Johansson
for helpful design discussions while working on camram for helpful design discussions while working on camram
Alex Savguira
for finding bugs with international headers and
suggesting the scan_zip option.
Business Management Systems - http://www.bmsi.com Business Management Systems - http://www.bmsi.com
for hosting the website, and providing paying clients who need milter service for hosting the website, and providing paying clients who need milter service
so I can work on it as part of my day job. so I can work on it as part of my day job.
+214
View File
@@ -0,0 +1,214 @@
# Revision 1.69 2006/11/04 22:09:39 customdesigned
# Another lame DSN heuristic. Block PTR cache poisoning attack.
#
# Revision 1.68 2006/10/04 03:46:01 customdesigned
# Fix defaults.
#
# Revision 1.67 2006/10/01 01:44:06 customdesigned
# case_sensitive_localpart option, more delayed bounce heuristics,
# optional smart_alias section.
#
# Revision 1.66 2006/07/26 16:42:26 customdesigned
# Support CBV timeout
#
# Revision 1.65 2006/06/21 22:22:00 customdesigned
# Handle multi-line headers in delayed dsns.
#
# Revision 1.64 2006/06/21 21:12:04 customdesigned
# More delayed reject token headers.
# Don't require HELO pass for CBV.
#
# Revision 1.63 2006/05/21 03:41:44 customdesigned
# Fail dsn
#
# Revision 1.61 2006/05/17 21:28:07 customdesigned
# Create GOSSiP record only when connection will procede to DATA.
#
# Revision 1.60 2006/05/12 16:14:48 customdesigned
# Don't require SPF pass for white/black listing mail from trusted relay.
# Support localpart wildcard for white and black lists.
#
# Revision 1.59 2006/04/06 18:14:17 customdesigned
# Check whitelist/blacklist even when not checking SPF (e.g. trusted relay).
#
# Revision 1.58 2006/03/10 20:52:49 customdesigned
# Use re to recognize failure DSNs.
#
# Revision 1.57 2006/03/07 20:50:54 customdesigned
# Use signed Message-ID in delayed reject to blacklist senders
#
# Revision 1.56 2006/02/24 02:12:54 customdesigned
# Properly report hard PermError (lax mode fails also) by always setting
# perm_error attribute with PermError exception. Improve reporting of
# invalid domain PermError.
#
# Revision 1.55 2006/02/17 05:04:29 customdesigned
# Use SRS sign domain list.
# Accept but do not use for training whitelisted senders without SPF pass.
# Immediate rejection of unsigned bounces.
#
# Revision 1.54 2006/02/16 02:16:36 customdesigned
# User specific SPF receiver policy.
#
# Revision 1.53 2006/02/12 04:15:01 customdesigned
# Remove spf dependency for iniplist
#
# Revision 1.52 2006/02/12 02:12:08 customdesigned
# Use CIDR notation for internal connect list.
#
# Revision 1.51 2006/02/12 01:13:58 customdesigned
# Don't check rcpt user list when signed MFROM.
#
# Revision 1.50 2006/02/09 20:39:43 customdesigned
# Use CIDR notation for trusted_relay iplist
#
# Revision 1.49 2006/01/30 23:14:48 customdesigned
# put back eom condition
#
# Revision 1.48 2006/01/12 20:31:24 customdesigned
# Accelerate training via whitelist and blacklist.
#
# Revision 1.47 2005/12/29 04:49:10 customdesigned
# Do not auto-whitelist autoreplys
#
# Revision 1.46 2005/12/28 20:17:29 customdesigned
# Expire and renew AddrCache entries
#
# Revision 1.45 2005/12/23 22:34:46 customdesigned
# Put guessed result in separate header.
#
# Revision 1.44 2005/12/23 21:47:07 customdesigned
# Move Received-SPF header to top.
#
# Revision 1.43 2005/12/09 16:54:01 customdesigned
# Select neutral DSN template for best_guess
#
# Revision 1.42 2005/12/01 22:42:32 customdesigned
# improve gossip support.
# Initialize srs_domain from srs.srs config property. Should probably
# always block unsigned DSN when signing all.
#
# Revision 1.41 2005/12/01 18:59:25 customdesigned
# Fix neutral policy. pobox.com -> openspf.org
#
# Revision 1.40 2005/11/07 21:22:35 customdesigned
# GOSSiP support, local database only.
#
# Revision 1.39 2005/10/31 00:04:58 customdesigned
# Simple implementation of trusted_forwarder list. Inefficient for
# more than 1 or 2 entries.
#
# Revision 1.38 2005/10/28 19:36:54 customdesigned
# Don't check internal_domains for trusted_relay.
#
# Revision 1.37 2005/10/28 09:30:49 customdesigned
# Do not send quarantine DSN when sender is DSN.
#
# Revision 1.36 2005/10/23 16:01:29 customdesigned
# Consider MAIL FROM a match for supply_sender when a subdomain of From or Sender
#
# Revision 1.35 2005/10/20 18:47:27 customdesigned
# Configure auto_whitelist senders.
#
# Revision 1.34 2005/10/19 21:07:49 customdesigned
# access.db stores keys in lower case
#
# Revision 1.33 2005/10/19 19:37:50 customdesigned
# Train screener on whitelisted messages.
#
# Revision 1.32 2005/10/14 16:17:31 customdesigned
# Auto whitelist refinements.
#
# Revision 1.31 2005/10/14 01:14:08 customdesigned
# Auto whitelist feature.
#
# Revision 1.30 2005/10/12 16:36:30 customdesigned
# Release 0.8.3
#
# Revision 1.29 2005/10/11 22:50:07 customdesigned
# Always check HELO except for SPF pass, temperror.
#
# Revision 1.28 2005/10/10 23:50:20 customdesigned
# Use logging module to make logging threadsafe (avoid splitting log lines)
#
# Revision 1.27 2005/10/10 20:15:33 customdesigned
# Configure SPF policy via sendmail access file.
#
# Revision 1.26 2005/10/07 03:23:40 customdesigned
# 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.
#
# Revision 1.25 2005/09/08 03:55:08 customdesigned
# Handle perverse MFROM quoting.
#
# Revision 1.24 2005/08/18 03:36:54 customdesigned
# Don't innoculate with SCREENED mail.
#
# Revision 1.23 2005/08/17 19:35:27 customdesigned
# Send DSN before adding message to quarantine.
#
# Revision 1.22 2005/08/11 22:17:58 customdesigned
# Consider SMTP AUTH connections internal.
#
# Revision 1.21 2005/08/04 21:21:31 customdesigned
# 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.
#
# Revision 1.20 2005/08/02 18:04:35 customdesigned
# Keep screened honeypot mail, but optionally discard honeypot only mail.
#
# Revision 1.19 2005/07/20 03:30:04 customdesigned
# Check pydspam version for honeypot, include latest pyspf changes.
#
# Revision 1.18 2005/07/17 01:25:44 customdesigned
# Log as well as use extended result for best guess.
#
# Revision 1.17 2005/07/15 20:25:36 customdesigned
# Use extended results processing for best_guess.
#
# Revision 1.16 2005/07/14 03:23:33 customdesigned
# Make SES package optional. Initial honeypot support.
#
# Revision 1.15 2005/07/06 04:05:40 customdesigned
# Initial SES integration.
#
# Revision 1.14 2005/07/02 23:27:31 customdesigned
# Don't match hostnames for internal connects.
#
# Revision 1.13 2005/07/01 16:30:24 customdesigned
# Always log trusted Received and Received-SPF headers.
#
# Revision 1.12 2005/06/20 22:35:35 customdesigned
# Setreply for rejectvirus.
#
# Revision 1.11 2005/06/17 02:07:20 customdesigned
# Release 0.8.1
#
# Revision 1.10 2005/06/16 18:35:51 customdesigned
# Ignore HeaderParseError decoding header
#
# Revision 1.9 2005/06/14 21:55:29 customdesigned
# Check internal_domains for outgoing mail.
#
# Revision 1.8 2005/06/06 18:24:59 customdesigned
# Properly log exceptions from pydspam
#
# Revision 1.7 2005/06/04 19:41:16 customdesigned
# Fix bugs from testing RPM
#
# Revision 1.6 2005/06/03 04:57:05 customdesigned
# Organize config reader by section. Create defang section.
#
# Revision 1.5 2005/06/02 15:00:17 customdesigned
# Configure banned extensions. Scan zipfile option with test case.
#
# Revision 1.4 2005/06/02 04:18:55 customdesigned
# Update copyright notices after reading article on /.
#
# Revision 1.3 2005/06/02 02:09:00 customdesigned
# Record timestamp in send_dsn.log
#
# Revision 1.2 2005/06/02 01:00:36 customdesigned
# Support configurable templates for DSNs.
+4 -8
View File
@@ -3,19 +3,15 @@ include TODO
include NEWS include NEWS
include CREDITS include CREDITS
include README include README
include ChangeLog
include MANIFEST.in include MANIFEST.in
include testsample.py include testsample.py
include testmime.py include testmime.py
include testbms.py include testutils.py
include testdspam.py
include bms.py
include spf.py
include spfquery.py
include test.py include test.py
include sample.py include sample.py
include milter-template.py
include test/* include test/*
include Milter/*.py
include *.spec include *.spec
include start.sh include start.sh
include milter.rc
include milter.rc7
include milter.cfg
+57 -30
View File
@@ -1,22 +1,22 @@
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc. # Copyright 2001 Business Management Systems, Inc.
# This code is under GPL. See COPYING for details. # This code is under the GNU General Public License. See COPYING for details.
# A thin OO wrapper for the milter module
import os import os
import milter import milter
import thread import thread
from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \ from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
set_flags, setdbg, \ set_flags, setdbg, setbacklog, settimeout, error, \
ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \ ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \
V1_ACTS, V2_ACTS, CURR_ACTS V1_ACTS, V2_ACTS, CURR_ACTS
try: try: from milter import QUARANTINE
from milter import QUARANTINE except: pass
except:
#print 'No QUARANTINE support' __version__ = '0.8.5'
pass
_seq_lock = thread.allocate_lock() _seq_lock = thread.allocate_lock()
_seq = 0 _seq = 0
@@ -44,7 +44,7 @@ class Milter:
for i in msg: print i, for i in msg: print i,
print print
def connect(self,hostname,unused,hostaddr): def connect(self,hostname,family,hostaddr):
"Called for each connection to sendmail." "Called for each connection to sendmail."
self.log("connect from %s at %s" % (hostname,hostaddr)) self.log("connect from %s at %s" % (hostname,hostaddr))
return CONTINUE return CONTINUE
@@ -100,18 +100,20 @@ class Milter:
def getsymval(self,sym): def getsymval(self,sym):
return self.__ctx.getsymval(sym) return self.__ctx.getsymval(sym)
def setreply(self,rcode,xcode,msg): # If sendmail does not support setmlreply, then only the
return self.__ctx.setreply(rcode,xcode,msg) # first msg line is used.
def setreply(self,rcode,xcode=None,msg=None,*ml):
return self.__ctx.setreply(rcode,xcode,msg,*ml)
# Milter methods which can only be called from eom callback. # Milter methods which can only be called from eom callback.
def addheader(self,field,value): def addheader(self,field,value,idx=-1):
return self.__ctx.addheader(field,value) return self.__ctx.addheader(field,value,idx)
def chgheader(self,field,idx,value): def chgheader(self,field,idx,value):
return self.__ctx.chgheader(field,idx,value) return self.__ctx.chgheader(field,idx,value)
def addrcpt(self,rcpt): def addrcpt(self,rcpt,params=None):
return self.__ctx.addrcpt(rcpt) return self.__ctx.addrcpt(rcpt,params)
def delrcpt(self,rcpt): def delrcpt(self,rcpt):
return self.__ctx.delrcpt(rcpt) return self.__ctx.delrcpt(rcpt)
@@ -119,6 +121,11 @@ class Milter:
def replacebody(self,body): def replacebody(self,body):
return self.__ctx.replacebody(body) return self.__ctx.replacebody(body)
def chgfrom(self,sender,params=None):
return self.__ctx.chgfrom(sender,params)
# When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined.
def quarantine(self,reason): def quarantine(self,reason):
return self.__ctx.quarantine(reason) return self.__ctx.quarantine(reason)
@@ -135,20 +142,34 @@ def connectcallback(ctx,hostname,family,hostaddr):
def closecallback(ctx): def closecallback(ctx):
m = ctx.getpriv() m = ctx.getpriv()
if not m: return CONTINUE if not m: return CONTINUE
rc = m.close() try:
m._setctx(None) # release milterContext rc = m.close()
finally:
m._setctx(None) # release milterContext
return rc return rc
def dictfromlist(args):
"Convert ESMTP parm list to keyword dictionary."
kw = {}
for s in args:
pos = s.find('=')
if pos > 0:
kw[s[:pos].upper()] = s[pos+1:]
return kw
def envcallback(c,args): def envcallback(c,args):
"""Convert ESMTP parms to keyword parameters. """Call function c with ESMTP parms converted to keyword parameters.
Can be used in the envfrom and/or envrcpt callbacks to process Can be used in the envfrom and/or envrcpt callbacks to process
ESMTP parameters as python keyword parameters.""" ESMTP parameters as python keyword parameters."""
kw = {} kw = {}
pargs = [args[0]]
for s in args[1:]: for s in args[1:]:
pos = s.find('=') pos = s.find('=')
if pos > 0: if pos > 0:
kw[s[:pos]] = s[pos+1:] kw[s[:pos].upper()] = s[pos+1:]
return apply(c,args,kw) else:
pargs.append(s)
return c(*pargs,**kw)
def runmilter(name,socketname,timeout = 0): def runmilter(name,socketname,timeout = 0):
# This bit is here on the assumption that you will be starting this filter # This bit is here on the assumption that you will be starting this filter
@@ -168,21 +189,22 @@ def runmilter(name,socketname,timeout = 0):
print "Removing %s" % fname print "Removing %s" % fname
try: try:
os.unlink(fname) os.unlink(fname)
except: except os.error, x:
pass import errno
if x.errno != errno.ENOENT:
raise milter.error(x)
# The default flags set include everything # The default flags set include everything
# milter.set_flags(milter.ADDHDRS) # milter.set_flags(milter.ADDHDRS)
milter.set_connect_callback(connectcallback) milter.set_connect_callback(connectcallback)
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host)) milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
milter.set_envfrom_callback(lambda ctx,*str: # For envfrom and envrcpt, we would like to convert ESMTP parms to keyword
ctx.getpriv().envfrom(*str)) # parms, but then all existing users would have to include **kw to accept
# envcallback(ctx.getpriv().envfrom,str)) # arbitrary keywords without crashing. We do provide envcallback and
milter.set_envrcpt_callback(lambda ctx,*str: # dictfromlist to make parsing the ESMTP args convenient.
ctx.getpriv().envrcpt(*str)) milter.set_envfrom_callback(lambda ctx,*str: ctx.getpriv().envfrom(*str))
# envcallback(ctx.getpriv().envrcpt,str)) milter.set_envrcpt_callback(lambda ctx,*str: ctx.getpriv().envrcpt(*str))
milter.set_header_callback(lambda ctx,fld,val: milter.set_header_callback(lambda ctx,fld,val: ctx.getpriv().header(fld,val))
ctx.getpriv().header(fld,val))
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh()) milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk)) milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom()) milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
@@ -200,3 +222,8 @@ def runmilter(name,socketname,timeout = 0):
if start_seq == _seq: raise # couldn't start if start_seq == _seq: raise # couldn't start
# milter has been running for a while, but now it can't start new threads # milter has been running for a while, but now it can't start new threads
raise milter.error("out of thread resources") 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()
+158
View File
@@ -0,0 +1,158 @@
# 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.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)
+64
View File
@@ -0,0 +1,64 @@
from ConfigParser import ConfigParser
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):
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()
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
else:
user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user)
return d
def getaddrdict(self,sect,opt):
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 = 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
+90
View File
@@ -0,0 +1,90 @@
# provide a higher level interface to pydns
import DNS
from DNS import DNSError
MAX_CNAME = 10
def DNSLookup(name, qtype):
try:
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 = {}
# 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.
SAFE2CACHE = {
('MX','A'): None,
('MX','MX'): None,
('CNAME','A'): None,
('CNAME','CNAME'): None,
('A','A'): None,
('AAAA','AAAA'): None,
('PTR','PTR'): None,
('NS','NS'): None,
('NS','A'): None,
('TXT','TXT'): None,
('SPF','SPF'): None
}
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)
"""
result = self.cache.get( (name, qtype) )
cname = None
if not result:
safe2cache = Session.SAFE2CACHE
for k, v in DNSLookup(name, qtype):
if k == (name, 'CNAME'):
cname = v
if (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)
return result
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)
+167
View File
@@ -0,0 +1,167 @@
# 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.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.
#
import smtplib
import socket
from email.Message import Message
import Milter
import time
import dns
def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
"""Send DSN. If msg is None, do callback verification.
Mailfrom is original sender we are sending DSN or CBV to.
Receiver is the MTA sending the DSN.
Return None for success or (code,msg) for failure."""
user,domain = mailfrom.split('@')
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('<>',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: <>')
if code != 250:
raise smtplib.SMTPSenderRefused(code, resp, '<>')
code,resp = smtp.rcpt(mailfrom)
if code not in (250,251):
return (code,resp) # permanent error
smtp.quit()
return None # success
except smtplib.SMTPRecipientsRefused,x:
return x.recipients[mailfrom] # permanent error
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
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())
+95
View File
@@ -0,0 +1,95 @@
# 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
if addr:
if host.find(addr) >= 0: return True
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)
+74
View File
@@ -0,0 +1,74 @@
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):
now = time.time()
self.firstseen = now
self.lastseen = now
self.cnt = 0
self.umis = None
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 check(self,ip,sender,recipient):
"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()
if now > r.lastseen + self.greylist_retain:
# expired
log.debug('Expired greylist: %s',key)
r = Record()
elif now < r.firstseen + self.greylist_time:
# still greylisted
log.debug('Early greylist: %s',key)
#r = Record()
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()
dbp[key] = r
except:
r = Record()
dbp[key] = r
dbp.sync()
finally:
self.lock.release()
return r.cnt
+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)
+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
+125
View File
@@ -0,0 +1,125 @@
import re
import struct
import socket
import email.Errors
from fnmatch import fnmatchcase
from email.Header import decode_header
#import email.Utils
import rfc822
ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$')
# from spf.py
def addr2bin(str):
"Convert a string IPv4 address into an unsigned integer."
return struct.unpack("!L", socket.inet_aton(str))[0]
MASK = 0xFFFFFFFFL
def cidr(i,n):
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
"""
ipnum = addr2bin(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 fnmatchcase(ipaddr,pat):
return True
return False
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']
"""
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
if t.startswith('"'):
if t.endswith('"'): return [t[1:-1]]
pos = t.find('"@')
if pos > 0: return [t[1:pos],t[pos+2:]]
return t.split('@')
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))
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 email.Errors.HeaderParseError: pass
return val
+89 -1
View File
@@ -1,5 +1,93 @@
Here is a history of user visible changes to Python milter. See pymilter.spec for recent history.
Here is a history of older changes to Python milter.
0.8.8 move AddrCache, parse_addr, iniplist, parse_header to Milter package
fix plock for missing source and can't change owner/group
add sample spfmilter.py milter
private_relay config option
0.8.7 Move spf module to pyspf
Prevent PTR cache poisoning
More lame bounce heuristics
Do plain CBV when template is missing
0.8.6 Support CBV timeout
Support fail template, headers in templates
Create GOSSiP record only when connection will procede to DATA.
More SPF lax heuristics
Don't require SPF pass for white/black listing mail from trusted relay.
Support localpart wildcard for white and black lists.
Delay reject of unsigned RCPT for postmaster and abuse only
Fix dsn reporting of hard permerror
Resolve FIXME for wrap_close in miltermodule.c
Add Message-ID to DSNs
Use signed Message-ID in delayed reject to blacklist senders
Auto-train via blacklist and auto-whitelist
Don't check userlist for signed MFROM
Accept but skip DSPAM training for whitelisted senders without SPF PASS
Report GC stats
Support CIDR matching for IP lists
Support pysrs sign feature
Support localpart specific SPF policy in access file
0.8.5 Simple trusted_forwarder implementation.
Fix access_file neutral policy
Move Received-SPF header to beginning of headers
Supply keyword info for all results in Received-SPF header.
Move guessed SPF result to separate header
Activate smfi_insheader only when SMFIR_INSHEADER defined
Handle NULL MX in spf.py
in-process GOSSiP server support (to be extended later)
Expire CBV cache and renew auto-whitelist entries
0.8.4 Auto-whitelist recipients of outgoing email.
Fix SPF policy via sendmail access map (case insensitive keys).
Train screener on whitelisted messages
Optional idx parameter to addheader to invoke smfi_insheader
Activate progress API when SMFIR_PROGRESS defined
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
spf_accept_fail option for braindead SPF senders
(treats fail like softfail)
Option to set SPF policy via sendmail access map.
Option to supply Sender header from MAIL FROM when missing.
Consider SMTP AUTH connections internal.
Send DSN for SPF errors corrected by extended processing.
Send DSN before SCREENED mail is quarantined
Use logging package to keep log lines atomic.
0.8.2 Strict processing limits per SPF RFC
Fixed several parsing bugs under RFC
Support official IANA SPF record (type99)
Honeypot support (requires pydspam-1.1.9)
Extended SPF processing results beyond strict RFC limits
Support original SES for bounce protection (requires pysrs-0.30.10)
Callback exception processing option in milter module
Handle corrupt ZIP attachments
0.8.1 Fix zip in zip loop in mime.py
Fix HeaderParseError in bms.py header callback
Check internal_domains for outgoing mail
Fix inconsistent results from send_dsn
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 0.6.9 Reject invalid SRS immediately for benefit of callback verifiers
Fix include bug in spf.py Fix include bug in spf.py
Fix check_header bug Fix check_header bug
+13 -14
View File
@@ -42,7 +42,7 @@ Quick Installation
1. Build and install Sendmail, enabling libmilter (see libmilter/README). 1. Build and install Sendmail, enabling libmilter (see libmilter/README).
2. Build and install Python, enabling threading. 2. Build and install Python, enabling threading.
3. Install this module: python setup.py --help 3. Install this module: python setup.py --help
4. Add these two lines to sendmail.cf: 4. Add these two lines to sendmail.cf[*]:
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:/home/username/pythonsock Xpythonfilter, S=local:/home/username/pythonsock
@@ -51,9 +51,17 @@ Xpythonfilter, S=local:/home/username/pythonsock
Note that milters should almost certainly not run as root. Note that milters should almost certainly not run as root.
That's it. Incoming mail will cause the milter to print some things, and That's it. Incoming mail will cause the milter to print some things, and
some email will be rejected (see the "header" method). Edit and play. See some email will be rejected (see the "header" method). Edit and play.
bms.py for an example milter used in production. See spfmilter.py for a functional SPF milter, or see bms.py for an complex
milter used in production.
[*] This is for a quick test. Your sendmail.cf in most distros will get
overwritten whenever sendmail.mc is updated. To make a milter permanent,
add something like:
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
to sendmail.mc instead.
Not-so-quick Installation Not-so-quick Installation
------------------------- -------------------------
@@ -90,9 +98,10 @@ some options associated with it. In this case, we have the "S" option, which
names the socket that sendmail will use to communicate with this particular names the socket that sendmail will use to communicate with this particular
milter. This milter's socket is a unix-domain socket in the filesystem. milter. This milter's socket is a unix-domain socket in the filesystem.
See libmilter/README for the definitive list of options. See libmilter/README for the definitive list of options.
NB: The name is specified in two places: here, in sendmail's cf file, and NB: The name is specified in two places: here, in sendmail's cf file, and
in the milter itself. Make sure the two match. in the milter itself. Make sure the two match.
NB: OpenBSD must use an inet socket. See the web page for details.
NB: The above lines can be added in your .mc file with this line: NB: The above lines can be added in your .mc file with this line:
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock') INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock')
@@ -124,16 +133,6 @@ and headers at
http://www.bmsi.com/linux/sendmail-rh72.spec http://www.bmsi.com/linux/sendmail-rh72.spec
OpenBSD Notes
-------------
Sendmail is broken on OpenBSD for unix domain sockets. You must use an
inet socket for milter. The sendmail.cf 'X' config line would look like:
Xpythonfilter, S=inet:1234@localhost
and the sample milter needs to be modified accordingly.
IPv6 Notes IPv6 Notes
---------- ----------
+5 -49
View File
@@ -1,50 +1,6 @@
Web admin interface Support smfi_negotiate and auto negotiate only those callbacks for which
RHBL Milter.Milter methods have been overridden. (Python should be able to
Check valid domains allowed by internal senders to detect PCs infected do that.)
with spam trojans.
Do CBV (callback verification) for mail with no published SPF record.
message log for automated stats and blacklisting
adapt init script to work on RH9
Skip dspam when SPF pass?
Report 551 with rcpt on SPF fail?
check spam keywords with character classes, e.g.
{a}=[a@ãä], {i}=[i1í], {e}=[eë], {o}=[o0ö]
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS Lookup exact RFC syntax of real name / email and make
forwarder accounts, and a util provides a special local alias for the Milter.utils.parse_addr() pass all unit tests.
user to give to the forwarder. Alias only works for mail from that
forwarder. Milter gets forwarder domain from alias and uses it to
SPF check forwarder.
Another special dspam user, 'honeypot', can be listed in innoculations.
All email to those addresses is treated as known spam.
Framework for modular Python milter components within a single VM.
Python milters can be already be composed through sendmail by running each in
a separate process. However, a significant amount of memory is wasted
for each additional Python VM, and communication between milters
is cumbersome (e.g., adding mail headers, writing external files).
Backup copies for outgoing/incoming mail.
Allow multiple wiretap groups, each with its own destination. Perhaps
also copy incoming wiretap mail, even though sendmail alias works perfectly
for the purpose, to avoid having to change two configs for a wiretap.
Provide a way to reload milter.cfg without stopping/restarting milter.
Allow selected Windows extensions for specific domains via milter.cfg
Fix setup.py so that _FFR_QUARANTINE is automatically defined when
available in libmilter.
Keep separate ismodified flag for headers and body. This is important
when rejecting outgoing mail with viruses removed (so as not to
embarrass yourself), and also removing Received headers with hidepath.
Wrap smfi_setbacklog(int) - but it is only available in sendmail >= 8.12.3,
so how can we detect whether to wrap it?
Need a test module to feed sample messages to a milter though a live
sendmail and SMTP. The mockup currently used is probably not very accurate,
and doesn't test the threading code.
-987
View File
@@ -1,987 +0,0 @@
#!/usr/bin/env python
# A simple milter.
# $Log$
# Revision 1.105 2004/04/20 15:16:00 stuart
# Release 0.6.9
#
# Revision 1.104 2004/04/19 21:56:26 stuart
# Support SPF best_guess and get_header
#
# Revision 1.103 2004/04/10 02:31:01 stuart
# Fix timeout config
#
# Revision 1.102 2004/04/08 20:25:11 stuart
# Make libmilter timeout a config option
#
# Revision 1.101 2004/04/08 19:18:16 stuart
# Preserve case of local part in sender
#
# Revision 1.100 2004/04/08 18:41:15 stuart
# Reject numeric hello names
#
# Revision 1.99 2004/04/06 19:46:39 stuart
# Reject invalid SRS immediately for benefit of CallBack Verifiers.
#
# Revision 1.98 2004/04/06 15:28:20 stuart
# Release 0.6.8-2
#
# Revision 1.97 2004/04/06 13:07:43 stuart
# Pass original header name to check_header
#
# Revision 1.96 2004/04/06 03:27:03 stuart
# bugs from Redhat 9 testing
#
# Revision 1.95 2004/04/05 22:37:08 stuart
# Include Received-SPF headers in dspam.
#
# Revision 1.94 2004/04/05 22:16:50 stuart
# Separate check_header method taking decoded header.
# Reject multiple recipients for a bounce.
#
# Revision 1.93 2004/04/01 20:57:45 stuart
# Report only SRS like addresses as spoofed.
# Return TEMPFAIL on SPF error.
#
# Revision 1.92 2004/03/25 17:45:53 stuart
# Make spf_reject_neutral global in bms.py
#
# Revision 1.91 2004/03/25 03:38:02 stuart
# Reject neutral SPF result for selected domains.
#
# Revision 1.90 2004/03/25 03:27:33 stuart
# Support delegation of SPF records.
#
# Revision 1.89 2004/03/23 22:02:49 stuart
# Header decoding bug.
#
# Revision 1.88 2004/03/23 05:08:45 stuart
# Decode headers, indirect srs config.
#
# Revision 1.87 2004/03/18 02:21:16 stuart
# SRS checking
#
# Revision 1.86 2004/03/11 05:00:37 stuart
# Don't wipe out fail messages from SPF records.
# Hello blacklist
#
# Revision 1.85 2004/03/10 01:49:22 stuart
# Enhanced SPF support.
#
# Revision 1.84 2004/03/09 17:04:49 stuart
# Received-SPF header.
#
# Revision 1.83 2004/03/08 20:23:26 stuart
# SPF support
#
# Revision 1.82 2004/03/01 18:56:50 stuart
# Support progress reporting.
#
# Revision 1.81 2004/03/01 18:36:09 stuart
# Trusted relay.
#
# Revision 1.80 2004/01/12 21:10:58 stuart
# Support wildcard user for smart_alias
#
# Revision 1.79 2003/12/04 23:46:06 stuart
# Release 0.6.4
#
# Revision 1.78 2003/12/04 23:20:24 stuart
# Make headerChange handle deleting absent header
#
# Revision 1.77 2003/12/04 22:01:40 stuart
# Limit size of messages which will be dspammed. This works around a bug
# in dspam-2.6.5.2 where it scans large binary attachments. I've never
# seen really big spam anyway.
#
# Revision 1.76 2003/12/04 21:44:33 stuart
# Pass header changes from Dspam to sendmail
#
# Revision 1.75 2003/11/25 17:43:07 stuart
# Update FAQ.
#
# Revision 1.74 2003/11/25 17:36:58 stuart
# dspam_reject
#
# Revision 1.73 2003/11/24 15:46:00 stuart
# Missing global for dspam_whitelist
#
# Revision 1.72 2003/11/22 02:52:07 stuart
# Handle multiple x-dspam-recipients properly on false positive
#
# Revision 1.71 2003/11/22 02:49:57 stuart
# dspam whitelist
#
# Revision 1.70 2003/11/09 03:53:34 stuart
# Don't block delivery of defanged false positives.
#
# Revision 1.69 2003/11/08 22:47:04 stuart
# Exempt entire domains with '@domain.com'
#
# Revision 1.68 2003/11/02 03:06:16 stuart
# Adjust error codes again.
#
# Revision 1.67 2003/11/02 03:01:46 stuart
# Adjust SMTP error codes after careful reading of standard.
#
# Revision 1.66 2003/11/02 01:56:43 stuart
# Use busy SMTP code.
#
# Revision 1.65 2003/11/02 01:44:11 stuart
# Suppress traceback for Dspam lock timeouts
#
# Revision 1.64 2003/10/28 01:00:19 stuart
# Dspam internal mail for dspam users
#
# Revision 1.63 2003/10/25 02:10:34 stuart
# Match hostname for internal connection test, even if no ipaddr.
#
# Revision 1.62 2003/10/24 04:34:52 stuart
# Fix for not saving defang of false positive triggered rejecting it
# as a virus from self.
#
# Revision 1.61 2003/10/22 22:03:14 stuart
# Apply dspam_exempt to screening
#
# Revision 1.60 2003/10/22 21:58:42 stuart
# Don't save false positives as defang file.
#
# Revision 1.59 2003/10/22 05:02:27 stuart
# Add support for dspam screeners
#
# Revision 1.58 2003/10/16 22:19:24 stuart
# Redirect Dspam logging to bms milter
#
# Revision 1.57 2003/10/10 00:15:04 stuart
# DISCARD message if quarrantined for any recipient.
#
# Revision 1.56 2003/10/06 19:30:27 stuart
# REJECT messages with boundard errors
#
# Revision 1.55 2003/10/03 18:20:31 stuart
# Opt-out feature to exempt certain recipients from header filtering.
#
# Revision 1.54 2003/09/22 13:36:04 stuart
# Release 0.6.1
#
# Revision 1.53 2003/09/06 07:08:36 stuart
# dspam support improvements.
#
# Revision 1.51 2003/09/02 00:27:27 stuart
# Should have full milter based dspam support working
#
# Revision 1.50 2003/08/26 06:08:17 stuart
# Use new python boolean since we now require 2.2.2
#
# Revision 1.49 2003/08/26 05:45:51 stuart
# Fix conditional import of dspam. Update web page.
#
# Revision 1.48 2003/08/26 05:10:43 stuart
# Readability tweaks
#
# Revision 1.47 2003/08/26 05:01:38 stuart
# Release 0.6.0
#
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc.
# This code is under GPL. See COPYING for details.
import sys
import os
import StringIO
import rfc822
import mime
import email.Errors
import Milter
import tempfile
import ConfigParser
import time
import re
from fnmatch import fnmatchcase
from email.Header import decode_header
# Import pysrs if available
try:
import SRS
srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE)
except: SRS = None
# Import spf if available
try: import spf
except: spf = None
ip4re = re.compile(r'^[1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*$')
#import syslog
#syslog.openlog('milter')
# Thanks to Chris Liechti for config parsing suggestions
# Global configuration defaults suitable for test framework.
socketname = "/tmp/pythonsock"
reject_virus_from = ()
wiretap_users = {}
discard_users = {}
wiretap_dest = None
blind_wiretap = True
check_user = {}
block_forward = {}
hide_path = ()
log_headers = False
block_chinese = False
spam_words = ()
porn_words = ()
scan_html = True
scan_rfc822 = True
internal_connect = ()
trusted_relay = ()
internal_domains = ()
hello_blacklist = ()
smart_alias = {}
dspam_dict = None
dspam_users = {}
dspam_userdir = None
dspam_exempt = {}
dspam_whitelist = {}
dspam_screener = None
dspam_internal = True # True if internal mail should be dspammed
dspam_reject = ()
dspam_sizelimit = 180000
srs = None
srs_reject_spoofed = False
spf_reject_neutral = ()
spf_best_guess = False
timeout = 600
class MilterConfigParser(ConfigParser.ConfigParser):
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):
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:]
d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
else:
user,domain = q.split('@')
d.setdefault(domain,[]).append(user)
return d
def getaddrdict(self,sect,opt):
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 = 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 read_config(list):
cp = MilterConfigParser({
'tempdir': "/var/log/milter/save",
'socket': "/var/log/milter/pythonsock",
'timeout': '600',
'scan_html': 'no',
'scan_rfc822': 'yes',
'block_chinese': 'no',
'log_headers': 'no',
'blind_wiretap': 'yes',
'maxage': '8',
'hashlength': '8',
'reject_spoofed': 'no',
'best_guess': 'no'
})
cp.read(list)
tempfile.tempdir = cp.get('milter','tempdir')
global socketname, scan_rfc822, scan_html, block_chinese, timeout
socketname = cp.get('milter','socket')
timeout = cp.getint('milter','timeout')
scan_rfc822 = cp.getboolean('milter','scan_rfc822')
scan_html = cp.getboolean('milter','scan_html')
block_chinese = cp.getboolean('milter','block_chinese')
global hide_path, block_forward, log_headers
hide_path = cp.getlist('scrub','hide_path')
block_forward = cp.getaddrset('milter','block_forward')
log_headers = cp.getboolean('milter','log_headers')
global blind_wiretap, wiretap_users, wiretap_dest, discard_users
blind_wiretap = cp.getboolean('wiretap','blind')
wiretap_users = cp.getaddrset('wiretap','users')
discard_users = cp.getaddrset('wiretap','discard')
wiretap_dest = cp.getdefault('wiretap','dest')
if wiretap_dest: wiretap_dest = '<%s>' % wiretap_dest
global check_user, reject_virus_from, internal_connect, internal_domains
check_user = cp.getaddrset('milter','check_user')
reject_virus_from = cp.getlist('scrub','reject_virus_from')
internal_connect = cp.getlist('milter','internal_connect')
internal_domains = cp.getlist('milter','internal_domains')
global porn_words, spam_words, smart_alias, trusted_relay, hello_blacklist
trusted_relay = cp.getlist('milter','trusted_relay')
porn_words = cp.getlist('milter','porn_words')
spam_words = cp.getlist('milter','spam_words')
hello_blacklist = cp.getlist('milter','hello_blacklist')
for sa in cp.getlist('wiretap','smart_alias'):
sm = cp.getlist('wiretap',sa)
if len(sm) < 2:
print 'malformed smart alias:',sa
continue
if len(sm) == 2: sm.append(sa)
key = (sm[0],sm[1])
smart_alias[key] = sm[2:]
global dspam_dict, dspam_users, dspam_userdir, dspam_exempt
global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit
global spf_reject_neutral,spf_best_guess,SRS
dspam_dict = cp.getdefault('dspam','dspam_dict')
dspam_exempt = cp.getaddrset('dspam','dspam_exempt')
dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist')
dspam_users = cp.getaddrdict('dspam','dspam_users')
dspam_userdir = cp.getdefault('dspam','dspam_userdir')
dspam_screener = cp.getdefault('dspam','dspam_screener')
dspam_reject = cp.getlist('dspam','dspam_reject')
if cp.has_option('dspam','dspam_sizelimit'):
dspam_sizelimit = cp.getint('dspam','dspam_sizelimit')
if spf:
spf.DELEGATE = cp.getdefault('spf','delegate')
spf_reject_neutral = cp.getlist('spf','reject_neutral')
spf_best_guess = cp.getboolean('spf','best_guess')
srs_config = cp.getdefault('srs','config')
if srs_config: cp.read([srs_config])
srs_secret = cp.getdefault('srs','secret')
if SRS and srs_secret:
global srs,srs_reject_spoofed
database = cp.getdefault('srs','database')
srs_reject_spoofed = cp.getboolean('srs','reject_spoofed')
maxage = cp.getint('srs','maxage')
hashlength = cp.getint('srs','hashlength')
separator = cp.getdefault('srs','separator','=')
if database:
import SRS.DB
srs = SRS.DB.DB(database=database,secret=srs_secret,
maxage=maxage,hashlength=hashlength,separator=separator)
else:
srs = SRS.Guarded.Guarded(secret=srs_secret,
maxage=maxage,hashlength=hashlength,separator=separator)
def parse_addr(t):
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
return t.split('@')
def parse_header(val):
h = decode_header(val)
if not len(h) or (not h[0][1] and len(h) == 1): return val
try:
u = []
for s,enc in h:
if enc:
u.append(unicode(s,enc))
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 LookupError:
return val
class bmsMilter(Milter.Milter):
"Milter to replace attachments poisonous to Windows with a WARNING message."
def log(self,*msg):
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
for i in msg: print i,
print
def __init__(self):
self.tempname = None
self.mailfrom = None # sender in SMTP form
self.canon_from = None # sender in end user form
self.fp = None
self.bodysize = 0
self.id = Milter.uniqueID()
# delrcpt can only be called from eom(). This accumulates recipient
# changes which can then be applied by alter_recipients()
def del_recipient(self,rcpt):
rcpt = rcpt.lower()
if not rcpt in self.discard_list:
self.discard_list.append(rcpt)
# addrcpt can only be called from eom(). This accumulates recipient
# changes which can then be applied by alter_recipients()
def add_recipient(self,rcpt):
rcpt = rcpt.lower()
if not rcpt in self.redirect_list:
self.redirect_list.append(rcpt)
# addheader can only be called from eom(). This accumulates added headers
# which can then be applied by alter_headers()
def add_header(self,name,val):
self.new_headers.append((name,val))
self.log('%s: %s' % (name,val))
def connect(self,hostname,unused,hostaddr):
self.internal_connection = False
self.trusted_relay = False
self.receiver = self.getsymval('j')
if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0]
for pat in internal_connect:
if fnmatchcase(ipaddr,pat):
self.internal_connection = True
break
for pat in trusted_relay:
if fnmatchcase(ipaddr,pat):
self.trusted_relay = True
break
self.connectip = ipaddr
else:
self.connectip = None
for pat in internal_connect:
if fnmatchcase(hostname,pat):
self.internal_connection = True
break
if self.internal_connection:
connecttype = 'INTERNAL'
else:
connecttype = 'EXTERNAL'
if self.trusted_relay:
connecttype += ' TRUSTED'
self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype))
return Milter.CONTINUE
def hello(self,hostname):
self.hello_name = hostname
self.log("hello from %s" % hostname)
if ip4re.match(hostname):
self.log("REJECT: numeric hello name:",hostname)
self.setreply('550','5.7.1','hello name cannot be numeric ip')
return Milter.REJECT
if not self.internal_connection and hostname in hello_blacklist:
self.log("REJECT: spam from self:",hostname)
self.setreply('550','5.7.1','I hate talking to myself.')
return Milter.REJECT
return Milter.CONTINUE
# multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message.
def envfrom(self,f,*str):
self.log("mail from",f,str)
self.fp = StringIO.StringIO()
self.tempname = None
self.mailfrom = f
self.forward = True
self.bodysize = 0
self.hidepath = False
self.discard = False
self.dspam = True
self.reject_spam = True
self.data_allowed = True
self.trust_received = self.trusted_relay
self.redirect_list = []
self.discard_list = []
self.new_headers = []
self.recipients = []
t = parse_addr(f.lower())
self.canon_from = '@'.join(t)
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
if len(t) == 2:
user,domain = t
if not self.internal_connection:
for pat in internal_domains:
if fnmatchcase(domain,pat):
self.log("REJECT: spam from self",pat)
self.setreply('550','5.7.1','I hate talking to myself.')
return Milter.REJECT
self.rejectvirus = domain in reject_virus_from
if user in wiretap_users.get(domain,()):
self.add_recipient(wiretap_dest)
if user in discard_users.get(domain,()):
self.discard = True
exempt_users = dspam_whitelist.get(domain,())
if user in exempt_users or '' in exempt_users:
self.dspam = False
else:
self.rejectvirus = False
if not (self.internal_connection or self.trusted_relay) \
and self.connectip and spf:
return self.check_spf()
return Milter.CONTINUE
def check_spf(self):
t = parse_addr(self.mailfrom)
if len(t) == 2: t[1] = t[1].lower()
q = spf.query(self.connectip,'@'.join(t),self.hello_name)
q.set_default_explanation('SPF fail: see http://spf.pobox.com/why.html')
res,code,txt = q.check()
receiver = self.receiver
if res == 'none' and spf_best_guess:
#self.log('SPF: no record published, guessing')
q.set_default_explanation('SPF guess: see http://spf.pobox.com/why.html')
# best_guess should not result in fail
res,code,txt = q.best_guess()
receiver += ': guessing'
if res in ('deny', 'fail'):
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
return Milter.REJECT
if res == 'neutral' and q.o in spf_reject_neutral:
self.log('REJECT: SPF neutral for',q.s)
self.setreply('550','5.7.1',
'mail from %s must pass SPF: http://spf.pobox.com/why.html' % q.o
)
return Milter.REJECT
if res == 'error':
self.setreply(str(code),'4.3.0',txt)
return Milter.TEMPFAIL
self.add_header('Received-SPF',q.get_header(res,receiver))
return Milter.CONTINUE
# hide_path causes a copy of the message to be saved - until we
# track header mods separately from body mods - so use only
# in emergencies.
def envrcpt(self,to,*str):
# mail to MAILER-DAEMON is generally spam that bounced
if to.startswith('<MAILER-DAEMON@'):
self.log('DISCARD: RCPT TO:',to,str)
return Milter.DISCARD
self.log("rcpt to",to,str)
t = parse_addr(to.lower())
if len(t) == 2:
if self.mailfrom == '<>':
if self.recipients:
self.log('REJECT: Multiple bounce recipients')
self.setreply('550','5.7.1','Multiple bounce recipients')
return Milter.REJECT
if srs and not (self.internal_connection or self.trusted_relay):
oldaddr = '@'.join(parse_addr(to))
try:
newaddr = srs.reverse(oldaddr)
self.log("srs rcpt:",newaddr)
except:
if srsre.match(oldaddr):
self.log("REJECT: srs spoofed:",oldaddr)
self.setreply('550','5.7.1','Invalid SRS signature')
return Milter.REJECT
self.data_allowed = not srs_reject_spoofed
self.recipients.append('@'.join(t))
user,domain = t
users = check_user.get(domain)
if self.discard:
self.del_recipient(to)
if users and not user in users:
self.log('REJECT: RCPT TO:',to,str)
return Milter.REJECT
if user in block_forward.get(domain,()):
self.forward = False
exempt_users = dspam_exempt.get(domain,())
if user in exempt_users or '' in exempt_users:
self.dspam = False
if domain in hide_path:
self.hidepath = True
if not domain in dspam_reject:
self.reject_spam = False
if smart_alias:
cf = self.canon_from
cf0 = cf.split('@',1)
if len(cf0) == 2:
cf0 = '@' + cf0[1]
else:
cf0 = cf
ct = '@'.join(t)
for key in ((cf,ct),(cf0,ct)):
if smart_alias.has_key(key):
self.del_recipient(to)
for t in smart_alias[key]:
self.add_recipient('<%s>'%t)
#rcpt = self.getsymval("{rcpt_addr}")
#self.log("rcpt-addr",rcpt);
return Milter.CONTINUE
# Heuristic checks for spam headers
def check_header(self,name,val):
lname = name.lower()
# val is decoded header value
if lname == 'subject':
# check for common spam keywords
for wrd in spam_words:
if val.find(wrd) >= 0:
self.log('REJECT: %s: %s' % (name,val))
self.setreply('550','5.7.1','That subject is not allowed')
return Milter.REJECT
# check for spam that claims to be legal
lval = val.lower().strip()
for adv in ("adv:","adv.","adv ","[adv]","(adv)","advt:","advert:"):
if lval.startswith(adv):
self.log('REJECT: %s: %s' % (name,val))
self.setreply('550','5.7.1','Advertising not accepted here')
return Milter.REJECT
for adv in ("adv","(adv)","[adv]"):
if lval.endswith(adv):
self.log('REJECT: %s: %s' % (name,val))
self.setreply('550','5.7.1','Advertising not accepted here')
return Milter.REJECT
# check for porn keywords
for w in porn_words:
if lval.find(w) >= 0:
self.log('REJECT: %s: %s' % (name,val))
self.setreply('550','5.7.1','That subject is not allowed')
return Milter.REJECT
# check for annoying forwarders
if not self.forward:
if lval.startswith("fwd:") or lval.startswith("[fw"):
self.log('REJECT: %s: %s' % (name,val))
self.setreply('550','5.7.1','I find unedited forwards annoying')
return Milter.REJECT
# check for invalid message id
if lname == 'message-id' and len(val) < 4:
self.log('REJECT: %s: %s' % (name,val))
return Milter.REJECT
# check for common bulk mailers
if lname == 'x-mailer':
mailer = val.lower()
if mailer in ('direct email','calypso','mail bomber') \
or mailer.find('optin') >= 0:
self.log('REJECT: %s: %s' % (name,val))
return Milter.REJECT
elif self.trust_received and lname == 'received':
self.trust_received = False
self.log('%s: %s' % (name,val.splitlines()[0]))
return Milter.CONTINUE
def header(self,name,hval):
if not self.data_allowed:
self.log('REJECT: bounce with no SRS encoding')
self.setreply('550','5.7.1',"spoofed reply address")
return Milter.REJECT
lname = name.lower()
# decode near ascii text to unobfuscate
val = parse_header(hval)
if not self.internal_connection:
# even if we wanted the Taiwanese spam, we can't read Chinese
if block_chinese and lname == 'subject':
if hval.startswith('=?big5') or hval.startswith('=?ISO-2022-JP'):
self.log('REJECT: %s: %s' % (name,hval))
self.setreply('550','5.7.1',"We don't understand chinese")
return Milter.REJECT
rc = self.check_header(name,val)
if rc != Milter.CONTINUE: return rc
# log selected headers
if log_headers or lname in ('subject','x-mailer'):
self.log('%s: %s' % (name,val))
if self.fp:
try:
val = val.encode('us-ascii')
except:
val = hval
self.fp.write("%s: %s\n" % (name,val)) # add header to buffer
return Milter.CONTINUE
def eoh(self):
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
for name,val in self.new_headers:
self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer
self.fp.write("\n") # terminate headers
self.fp.seek(0)
# copy headers to a temp file for scanning the body
headers = self.fp.getvalue()
self.fp.close()
self.tempname = fname = tempfile.mktemp(".defang")
self.fp = open(fname,"w+b")
self.fp.write(headers) # IOError (e.g. disk full) causes TEMPFAIL
# check if headers are really spammy
if dspam_dict and not self.internal_connection:
ds = dspam.dspam(dspam_dict,dspam.DSM_PROCESS,
dspam.DSF_CHAINED|dspam.DSF_CLASSIFY)
try:
ds.process(headers)
if ds.probability > 0.93 and self.dspam:
self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability)
self.setreply('550','5.7.1','Your Message looks spammy')
return Milter.REJECT
self.add_header('X-DSpam-HeaderScore','%f'%ds.probability)
finally:
ds.destroy()
return Milter.CONTINUE
def body(self,chunk): # copy body to temp file
if self.fp:
self.fp.write(chunk) # IOError causes TEMPFAIL in milter
self.bodysize += len(chunk)
return Milter.CONTINUE
def _headerChange(self,msg,name,value):
if value: # add header
self.addheader(name,value)
else: # delete all headers with name
h = msg.getheaders(name)
if h:
for i in range(len(h),0,-1):
self.chgheader(name,i-1,'')
def _chk_attach(self,msg):
"Filter attachments by content."
mime.check_name(msg,self.tempname) # check for bad extensions
if scan_html:
mime.check_html(msg,self.tempname) # remove scripts from HTML
# don't let a tricky virus slip one past us
if scan_rfc822:
msg = msg.get_submsg()
if msg: return mime.check_attachments(msg,self._chk_attach)
return Milter.CONTINUE
def alter_recipients(self,discard_list,redirect_list):
for rcpt in discard_list:
if rcpt in redirect_list: continue
self.log("DISCARD RCPT: %s" % rcpt) # log discarded rcpt
self.delrcpt(rcpt)
for rcpt in redirect_list:
if rcpt in discard_list: continue
self.log("APPEND RCPT: %s" % rcpt) # log appended rcpt
self.addrcpt(rcpt)
if not blind_wiretap:
self.addheader('Cc',rcpt)
# check spaminess for recipients in dictionary groups
# if there are multiple users getting dspammed, then
# a signature tag for each is added to the message.
# FIXME: quarantine messages rejected via fixed patterns above
# this will give a fast start to stats
def check_spam(self):
if not dspam_userdir: return False
ds = Dspam.DSpamDirectory(dspam_userdir)
ds.log = self.log
ds.headerchange = self._headerChange
modified = False
for rcpt in self.recipients:
if dspam_users.has_key(rcpt):
user = dspam_users.get(rcpt)
if user:
try:
self.fp.seek(0)
txt = self.fp.read()
if user == 'spam' and self.internal_connection:
sender = dspam_users.get(self.canon_from)
if sender:
self.log("SPAM: %s" % sender) # log user for FP
ds.add_spam(sender,txt)
txt = None
self.fp = None
return False
elif user == 'falsepositive' and self.internal_connection:
sender = dspam_users.get(self.canon_from)
if sender:
self.log("FP: %s" % sender) # log user for FP
txt = ds.false_positive(sender,txt)
self.fp = StringIO.StringIO(txt)
self.delrcpt('<%s>' % rcpt)
self.recipients = None
self.rejectvirus = False
return True
elif not self.internal_connection or dspam_internal:
if len(txt) > dspam_sizelimit:
self.log("Large message:",len(txt))
return False
txt = ds.check_spam(user,txt,self.recipients)
if not txt:
# DISCARD if quarrantined for any recipient. It
# will be resent to all recipients if they submit
# as a false positive.
self.log("DSPAM:",user,rcpt)
self.fp = None
return False
self.fp = StringIO.StringIO(txt)
modified = True
except Exception,x:
print x
# screen if no recipients are dspam_users
if not modified and dspam_screener and not self.internal_connection \
and (self.dspam or self.reject_spam):
self.fp.seek(0)
txt = self.fp.read()
if len(txt) > dspam_sizelimit:
self.log("Large message:",len(txt))
return False
if not ds.check_spam(dspam_screener,txt,self.recipients,
classify=True,quarantine=not self.reject_spam):
self.fp = None
if self.reject_spam:
self.log("DSPAM:",dspam_screener,
'REJECT: X-DSpam-Score: %f' % ds.probability)
self.setreply('550','5.7.1','Your Message looks spammy')
return True
self.log("DSPAM:",dspam_screener,"SCREENED")
return modified
def eom(self):
if not self.fp:
return Milter.ACCEPT # no message collected - so no eom processing
try:
# analyze external mail for spam
spam_checked = self.check_spam() # tag or quarantine for spam
if not self.fp:
if spam_checked: return Milter.REJECT
return Milter.DISCARD # message quarantined for all recipients
# analyze all mail for dangerous attachments and scripts
self.fp.seek(0)
msg = mime.MimeMessage(self.fp)
# pass header changes in top level message to sendmail
msg.headerchange = self._headerChange
# filter leaf attachments through _chk_attach
rc = mime.check_attachments(msg,self._chk_attach)
except: # milter crashed trying to analyze mail
exc_type,exc_value = sys.exc_info()[0:2]
if dspam_userdir and exc_type == dspam.error:
if not exc_value.strerror:
exc_value.strerror = exc_value.args[0]
if exc_value.strerror == 'Lock failed':
self.log("LOCK: BUSY") # log filename
self.setreply('450','4.2.0',
'Too busy discarding spam. Please try again later.')
return Milter.TEMPFAIL
fname = tempfile.mktemp(".fail") # save message that caused crash
os.rename(self.tempname,fname)
self.tempname = None
self.log("FAIL: %s" % fname) # log filename
if exc_type == email.Errors.BoundaryError:
self.setreply('554','5.7.7',
'Boundary error in your message, are you a spammer?')
return Milter.REJECT
if exc_type == email.Errors.HeaderParseError:
self.setreply('554','5.7.7',
'Header parse error in your message, are you a spammer?')
return Milter.REJECT
# let default exception handler print traceback and return 451 code
raise
if rc == Milter.REJECT: return rc;
if rc == Milter.DISCARD: return rc;
if rc == Milter.CONTINUE: rc = Milter.ACCEPT # for testbms.py compat
defanged = msg.ismodified()
if self.hidepath: del msg['Received']
if self.recipients == None:
# false positive being recirculated
self.recipients = msg.get_all('x-dspam-recipients',[])
if self.recipients:
for rcptlist in self.recipients:
for rcpt in rcptlist.split(','):
self.addrcpt('<%s>' % rcpt.strip())
del msg['x-dspam-recipients']
else:
self.addrcpt(self.mailfrom)
else:
self.alter_recipients(self.discard_list,self.redirect_list)
for name,val in self.new_headers:
self.addheader(name,val)
if not defanged and not spam_checked:
os.remove(self.tempname)
self.tempname = None # prevent re-removal
self.log("eom")
return rc # no modified attachments
# Body modified, copy modified message to a temp file
if defanged:
if self.rejectvirus and not self.hidepath:
self.log("REJECT virus from",self.mailfrom)
self.tempname = None
return Milter.REJECT
self.log("Temp file:",self.tempname)
self.tempname = None # prevent removal of original message copy
out = tempfile.TemporaryFile()
try:
msg.dump(out)
out.seek(0)
msg = rfc822.Message(out)
msg.rewindbody()
while True:
buf = out.read(8192)
if len(buf) == 0: break
self.replacebody(buf) # feed modified message to sendmail
if spam_checked: self.log("dspam")
return rc
finally:
out.close()
return Milter.TEMPFAIL
def close(self):
sys.stdout.flush() # make log messages visible
if self.tempname:
os.remove(self.tempname) # remove in case session aborted
if self.fp:
self.fp.close()
sys.stdout.flush()
return Milter.CONTINUE
def abort(self):
self.log("abort after %d body chars" % self.bodysize)
return Milter.CONTINUE
def main():
Milter.factory = bmsMilter
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
if wiretap_dest or smart_alias or dspam_userdir:
flags = flags + Milter.ADDRCPT
if srs or len(discard_users) > 0 or smart_alias or dspam_userdir:
flags = flags + Milter.DELRCPT
Milter.set_flags(flags)
print "bms milter startup"
sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,timeout)
print "bms milter shutdown"
if __name__ == "__main__":
read_config(["milter.cfg"])
if dspam_dict:
import dspam # low level spam check
if dspam_userdir:
import dspam
import Dspam # high level spam check
try:
dspam_version = Dspam.VERSION
except:
dspam_version = '1.1.4'
assert dspam_version >= '1.1.5'
main()
-138
View File
@@ -1,138 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Python Milter FAQ</title>
</head><body>
<h1> Python Milter <a name=faq>FAQ</a> </h1>
<ol>
<h3> Compiling Python Milter </h3>
<li> Q. I have installed sendmail from source, but Python milter won't
compile.
<p> A. Even though libmilter is officially supported in sendmail-8.12,
you need to build and install it in separate steps. Take a look
at the <a href="/aix/sendmail12.spec">RPM spec file</a> for sendmail-8.12.
The %prep section shows you how to create
a site.config.m4 that enables MILTER. The %build section shows you how
to build libmilter in a separate invocation of make. The %install section
shows you how to install libmilter with a separate invocation of make.
<p>
<li> Q. Why is mfapi.h not found when I try to compile Python milter on
RedHat 7.2?
<p> A. RedHat forgot to include the header in the RPM. See the
<a href="milter.html#rh72">RedHat 7.2 requirements</a>.
<p>
<h3> Running Python Milter </h3>
<li> Q. The sample.py milter prints a message, then just sits there.
<pre>
To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=pythonfilter
Xpythonfilter, S=local:inet:1030@localhost
See the sendmail README for libmilter.
sample milter startup
</pre>
<p> A. You need to tell sendmail to connect to your milter. The
sample milter tells you what to add to your sendmail.cf to tell
sendmail to use the milter. You can also add an INPUT_MAIL_FILTER
macro to your sendmail.mc file and rebuild sendmail.cf - see the sendmail
README for milters.
<p>
<li> Q. I've configured sendmail properly, but still nothing happens
when I send myself mail!
<p> A. Sendmail only milters SMTP mail. Local mail is not miltered.
You can pipe a raw message through sendmail to test your milter:
<pre>
$ cat rawtextmsg | sendmail myname@my.full.domain
</pre>
Now check your milter log.
<p>
<li> Q. Why do I get this ImportError exception?
<pre>
File "mime.py", line 370, in ?
from sgmllib import declstringlit, declname
ImportError: cannot import name declstringlit
</pre>
<p> A. <code>declstringlit</code> is not provided by sgmllib in all versions
of python. For instance, python-2.2 does not have it. Upgrade to
milter-0.4.5 or later to remove this dependency.
<p>
<li> Q. Why do I get <code>milter.error: cannot add recipient</code>?
<pre>
</pre>
<p> A. You must tell libmilter how you might mutate the message with
<code>set_flags()</code> before calling <code>runmilter()</code>. For
instance, <code>Milter.set_flags(Milter.ADDRCPT)</code>. You must add together
all of <code>ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS</code> that apply.
<p>
<li> Q. Why does sendmail sometimes print something like:
"...write(D) returned -1, expected 5: Broken pipe"
in the sendmail log?
<p> A. Libmilter expects "rcpt to" shortly after getting "mail from".
"Shortly" is defined by the timeout parameter you passed to
<code>Milter.runmilter()
</code> or <code>milter.settimeout()</code>. If the timeout is 10 seconds,
and looking up the first recipient in DNS takes more than
10 seconds, libmilter will give up and break the connection.
<code>Milter.runmilter()</code> defaulted to 10 seconds in 0.3.4. In 0.3.5
it will keep the libmilter default of 2 hours.
<p>
<li> Q. Why does milter block messages with big5 encoding? What if I
want to receive them?
<p> A. sample.py is a sample. It is supposed to be easily modified
for your specific needs. We will of course continue to move generic
code out of the sample as the project evolves. Think of sample.py as
an active config file.
<p>
<li> Q. Why does sendmail coredump with milters on OpenBSD?
<p> A. Sendmail has a problem with unix sockets on OpenBSD. Use
an internet domain socket instead. For example, in <code>sendmail.cf</code> use
<pre>
Xpythonfilter, S=inet:1234@localhost
</pre>
and change sample.py accordingly.
<p>
<li> Q. How can I change the bounce message for an invalid recipient?
I can only change the recipient in the eom callback, but the eom callback
is never called when the recipient is invalid!
<p> A. Configure sendmail to use virtusertable, and send all unknown
addresses to /dev/null. For example,
<h4>/etc/mail/virtusertable</h4>
<pre>
@mycorp.com dev-null
dan@mycorp.com dan
sally@mycorp.com sally
</pre>
<h4>/etc/aliases</h4>
<pre>
dev-null: /dev/null
</pre>
Now your milter will get to the eom callback, and can change the
envelope recipient at will. Thanks to Dredd at
<a href=http://www.milter.org/>milter.org</a> for this solution.
<p>
<li> Q. I am having trouble with the setreply method. It always outputs
"milter.error: cannot set reply".
<p> A. Check the sendmail log for errors. If sendmail is getting
milter timeouts, then your milter is taking too long and sendmail gave
up waiting. You can adjust the timeouts in your sendmail config. Here
is a milter declaration for sendmail.cf with all timeouts specified:
<pre>
Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
</pre>
</ol>
</html>
+138
View File
@@ -0,0 +1,138 @@
## To roll your own milter, create a class that extends Milter.
# See the pymilter project at http://bmsi.com/python/milter.html
# based on Sendmail's milter API http://www.milter.org/milter_api/api.html
# This code is open-source on the same terms as Python.
## Milter calls methods of your class at milter events.
## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
## You can also add/del recipients, replacebody, add/del headers, etc.
import Milter
import StringIO
import time
import email
from socket import AF_INET, AF_INET6
from Milter import parse_addr
class myMilter(Milter.Milter):
def __init__(self): # A new instance with each new connection.
self.id = Milter.uniqueID() # Integer incremented with each call.
# each connection runs in its own thread and has its own myMilter
# instance. Python code must be thread safe. This is trivial if only stuff
# in myMilter instances is referenced.
def connect(self, IPname, family, hostaddr):
# (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
# (self, 'ip6.mxout.example.com', AF_INET6,
# ('3ffe:80e8:d8::1', 4720, 1, 0) )
self.IP = hostaddr[0]
self.port = hostaddr[1]
if family == AF_INET6:
self.flow = hostaddr[2]
self.scope = hostaddr[3]
else:
self.flow = None
self.scope = None
self.IPname = IPname # Name from a reverse IP lookup
self.H = None
self.fp = None
self.receiver = self.getsymval('j')
self.log("connect from %s at %s" % (IPname, hostaddr) )
return Milter.CONTINUE
## def hello(self,hostname):
def hello(self, heloname):
# (self, 'mailout17.dallas.texas.example.com')
self.H = heloname
self.log("HELO %s" % heloname)
if heloname.find('.') < 0: # illegal helo name
# NOTE: example only - too many real braindead clients to reject on this
self.setreply('550','5.7.1','Sheesh people! Use a proper helo name!')
return Milter.REJECT
return Milter.CONTINUE
## def envfrom(self,f,*str):
def envfrom(self, mailfrom, *str):
self.F = mailfrom
self.R = [] # list of recipients
self.fromparms = Milter.dictfromlist(str) # ESMTP parms
self.user = self.getsymval('{auth_authen}') # authenticated user
self.log("mail from:", mailfrom, *str)
self.fp = StringIO.StringIO()
self.canon_from = '@'.join(parse_addr(mailfrom))
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
return Milter.CONTINUE
## def envrcpt(self, to, *str):
def envrcpt(self, recipient, *str):
rcptinfo = to,Milter.dictfromlist(str)
self.R.append(rcptinfo)
return Milter.CONTINUE
def header(self, name, hval):
self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
return Milter.CONTINUE
def eoh(self):
self.fp.write("\n") # terminate headers
return Milter.CONTINUE
def body(self, chunk):
self.fp.write(chunk)
return Milter.CONTINUE
def eom(self):
self.fp.seek(0)
msg = email.message_from_file(self.fp)
self.setreply('250','2.5.1','Grokked by pymilter')
# many milter functions can only be called from eom()
# example of adding a Bcc:
self.addrcpt('<%s>' % 'spy@example.com')
return Milter.ACCEPT
def close(self):
# always called, even when abort is called. Clean up
# any external resources here.
return Milter.CONTINUE
def abort(self):
# client disconnected prematurely
return Milter.CONTINUE
## === Support Functions ===
def log(self,*msg):
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print i,
print
## ===
def main():
# Register to have the Milter factory create instances of your class:
Milter.factory = myMilter
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
flags += Milter.ADDRCPT
flags += Milter.DELRCPT
Milter.set_flags(flags) # tell Sendmail which features we use
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,timeout)
print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__":
main()
-120
View File
@@ -1,120 +0,0 @@
# features intended to filter or block incoming mail
[milter]
;socket=/var/log/milter/pythonsock
tempdir = /var/log/milter/save
;timeout=600
scan_rfc822 = 1
# can be CPU intensive
scan_html = 0
# reject asian fonts because we can't read them
block_chinese = 1
# users who hate forwarded mail
;block_forward = egghead@mycorp.com, busybee@mycorp.com
log_headers = 0
# Reject mail for domains mentioned unless user is mentioned here also
;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
# porn words are case insensitive
porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck,
vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck,
p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam,
v1@gra, xan@x, cialis, ci@lis, frëe, xãnax, valíum, vãlium, via-gra,
x@n3x, vicod3n, penís, v|c0d1n, phentermine, en1arge, dip1oma, v1codin
# spam words are case sensitive
spam_words = $$$, !!!, XXX, FREE, HGH
# connection ips and hostnames are matched against this glob style list
# to recognize internal senders
;internal_connect = 192.168.*.*
# mail that is not an internal_connect and claims to be from an
# internal domain is rejected.
;internal_domains = mycorp.com
# connections from a trusted relay can trust the first Received header
;trusted_relay = 1.2.3.4, 66.12.34.56
# reject external senders with hello names no legit external sender would use
;hello_blacklist = mycorp.com, 66.12.34.56
[srs]
config=/etc/mail/pysrs.cfg
;secret="shhhh!"
;maxage=21
;hashlength=4
;database=/var/log/milter/srsdata
;fwdomain = mydomain.com
# turn this on after a grace period
reject_spoofed = 0
[spf]
# namespace where SPF records can be supplied for domains without one
# records are search for under _spf.domain.com
;delegate = domain.com
# domains where a neutral SPF result should cause mail to be rejected
;reject_neutral = aol.com
# use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published
;best_guess = 0
# features intended to clean up outgoing mail
[scrub]
# domains that stupidly block visible private nodes
;hide_path = jcpenney.com
# block, don't just replace with warning, viruses from these domains
;reject_virus_from = mycorp.com
# features intended for spying on users and coworkers
[wiretap]
blind = 1
#
# wiretap lets you surreptitiously monitor a users outgoing email
# (sendmail aliases let you monitor incoming mail)
#
;users = disloyal@bigcorp.com, bigmouth@bigcorp.com
;dest = spy@bigcorp.com
# discard outgoing mail without alerting sender
# can be used in conjunction with wiretap to censor outgoing mail
;discard_users = canned@bigcorp.com
#
# smart aliases trigger on both sender and recipient
#
;smart_alias = copycust,walter
# mail from client@clientcorp.com to sue@bigcorp.com is redirected to
# local alias copycust
;copycust = client@clientcorp.com,sue@bigcorp.com
# mail from cust@othercorp.com to walter@bigcorp.com is redirected to
# boss@bigcorp.com
;walter = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com
# additional copies can be added
;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com,
; walter@bigcorp.com
[dspam]
# Select a well moderated dspam dictionary to reject spammy headers
# dspam-python must be installed to use: http://bmsi.com/python/dspam.html
# only EXTERNAL messages are dspam filtered
;dspam_dict=/var/lib/dspam/moderator.dict
# Opt-opt recipients from dspam screening and header triage
;dspam_exempt=getitall@mycorp.com
# Do not scan mail (ostensibly) from these senders
;dspam_whitelist=getitall@sender.com
# Reject spam to these domains, perhaps because we are a backup MX server
;dspam_reject=othercorp.com
# directory for dspam user quarantine, signature db, and dictionaries
# defining this activates the dspam application
# dspam and dspam-python must be installed
;dspam_userdir=/var/lib/dspam
# do not dspam messages larger than this
;dspam_sizelimit=180000
# Map email addresses and aliases to dspam users
;dspam_users=david,goliath,spam,falsepositive
;david=david@foocorp.com,david.yelnetz@foocorp.com,david@bar.foocorp.com
;goliath=giant@foocorp.com,goliath.philistine@foocorp.com
# address to forward spam to. milter will process these and not deliver
;spam=spam@foocorp.com
# address to forward false positives to. milter will process and not deliver
;falsepositive=ham@foocorp.com
# the dspam_screener is used to screen mail for all recipients who are
# not dspam_users. Spam goes to the screeners quarantine, and the original
# recipients saved so that false positives can be properly delivered.
# The dspam CGI can also be used: logins must match dspam users
-649
View File
@@ -1,649 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Python Milters</title>
</head><body>
<P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
<IMG SRC="/art/brain1.gif"
ALT="Viewable With Any Browser" BORDER="0"></A>
<img src="/art/banner_4.gif" width="468" height="60" border="0"
usemap="#banner_4" alt="Your vote?">
<map name="banner_4">
<area shape="rect" coords="330,25,426,59"
href="http://www.sepschool.org/survey/" alt="I Disagree">
<area shape="rect" coords="234,28,304,57" href="http://sepschool.org/" alt="I Agree">
</map>
</P>
<h1 align=center>Sendmail Milters in Python</h1>
<h4 align=center>by <a href="mailto:%75%72%6D%61%6E%65%40%6E%65%75%72%61l%61%63%63%65%73%73%2E%63%6F%6D">Jim Niemira</a>
and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
Stuart D. Gathman</a><br>
This web page is written by Stuart D. Gathman<br>and<br>sponsored by
<a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
Last updated Apr 21, 2004</h4>
See the <a href="faq.html">FAQ</a> | <a href="#download">Download now</a> |
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a>
<p>
<img src="python55.gif" align=left alt="A Python">
<a href="//www.sendmail.org/">Sendmail</a> introduced a
<a href="http://www.milter.org/milter_api/api.html"> new API</a> beginning with version 8.10 -
libmilter. The milter module for <a href="//www.python.org">Python</a>
provides a python interface to libmilter that exploits all its features.
<p>
Sendmail 8.12 officially releases libmilter.
Version 8.12 seems to be more robust, and includes new privilege
separation features to enhance security.
I recommend upgrading.
<h2> <a name=dspam>Bayesian Filtering</a> </h2>
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
dspam bayes filter project</a> and <a href="dspam.html">
packaged it for python</a>.
Release 0.6.6 adds support for <a href="http://spf.pobox.com/">SPF</a>,
a protocol to prevent forging of the envelope from address.
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
The included spf.py module is an updated version of the original 1.6
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
The updated version tracks the draft RFC and test suite.
<p>
Release 0.6.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers. Since sendmail has to
read the entire message anyway once we start reading headers, it
would probably be better to scan the whole message - except that
we replace dangerous attachments elsewhere in the milter - which screws up the
body statistics for messages with dangerous attachments.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
<h2> Enough Already! </h2>
Nearly a dozen people have emailed me begging for a feature to copy
outgoing and/or incoming mail to a backup directory by user. Ok, it
looks like this is a most requested feature for 0.5.6. In the meantime,
here are some things to consider:
<ul>
<li> If you want to equivalent of a Bcc added to each message, this
is very easy to do in the python code for bms.py. See below.
<li> If you want to copy to a file in a directory (thus avoiding having to
set up aliases), this is slightly more involved. The bms.py milter already
copies the message to a temporary file for use in replacing the message body
when banned attachments are found. You have to open a file, and copy the
Mesage object to it in eom().
<li> Finally, you are probably aware that most email clients already
keep a copy of outgoing mail? Presumably there is a good reason for
keeping another copy on the server.
</ul>
<p>
To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
determining whether you want to copy (e.g. whether the sender is local). For
example,
<pre>
def envfrom(...
...
if len(t) == 2:
self.rejectvirus = t[1] in reject_virus_from
if t[0] in wiretap_users.get(t[1],()):
self.add_recipient(wiretap_dest)
if t[1] == 'mydomain.com':
self.add_recipient('&lt;copy-%s&gt;' % t[0])
...
</pre>
<p>
To make this a generic feature requires thinking about how the configuration
would look. Feel free to make specific suggestions about config file
entries. Be sure to handle both Bcc and file copies, and designating what
mail should be copied. How should "outgoing" be defined? Implementing it is
easy once the configuration is designed.
<h3>Overview</h3>
This package provides a robust toolkit for Python <a
href="#milter">milters</a>, and the beginnings of a general purpose mail
filtering system written in Python.
<p>
At the lowest level, the 'milter' module provides a thin wrapper around the
<a href="http://www.milter.org/milter_api/api.html">
sendmail libmilter API</a>. This API lets you register callbacks for
a number of events in the
<a href="http://www.cs.concordia.ca/~group/fig/public/email/relay/milter+ruleset-checks.html">process of sendmail receiving a message via SMTP</a>.
These events include the initial connection from a MTA,
the envelope sender and recipients, the top level mail headers, and
the message body. There are options to mangle all of these components
of the message as it passes through the milter.
<p>
At the next level, the 'Milter' module (note the case difference) provides a
Python friendly object oriented wrapper for the low level API. To use the
Milter module, an application registers a 'factory' to create an object
for each connection from a MTA to sendmail. These connection objects
must provide methods corresponding to the libmilter callback events.
<p>
Each event method returns a code to tell sendmail whether to proceed
with processing the message. This is a big advantage of milters over
other mail filtering systems. Unwanted mail can be stopped in its
tracks at the earliest possible point.
<p>
The Milter.Milter class provides default implementations for event
methods that
do nothing, and also provides wrappers for the libmilter methods to mutate
the message.
<p>
Finally, the bms.py application is both a sample of how to use the
Milter module, and the beginnings of a general purpose SPAM filtering,
wiretapping, and Win32 virus protection milter.
<h3><a name=download>Downloading</a></h3>
The latest stable release is <a href="#stable">0.6.9</a>. A stable
release is one which has been installed (and working correctly) on
production systems long enough to convince me that it is stable. As
the package gains more features and complexity, stable will mean no
bug reports from outside users either.
<p>
The latest version is 0.6.9-1. See the <a href=NEWS>Change Log</a>.
<p>
<a name="stable"><b>Stable</b></a>
<a href="http://bmsi.com/python/milter-0.6.9.tar.gz">
milter-0.6.9.tar.gz</a> Add SPF test suite driver, and validate
spf.py against test suite. Add best_guess and get_header to spf.py.
Libmilter timeout option in config.
<br>
<a href="http://bmsi.com/linux/rh72/milter-0.6.9-1.i386.rpm">
milter-0.6.9-1.i386.rpm</a> Binary RPM for Redhat 7.x, now requires
sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
python2.3</a>.
<br>
<a href="http://bmsi.com/linux/rh9/milter-0.6.9-1.src.rpm">
milter-0.6.9-1.src.rpm</a> Source RPM for Redhat 9,7.x.
<p>
<a href="http://bmsi.com/python/milter-0.6.8.tar.gz">
milter-0.6.8.tar.gz</a> Include Received-SPF headers in Dspam analysis.
Fix sysv init for Redhat 9 and later. Reject bounces with multiple
recipients.
<br>
<a href="http://bmsi.com/python/milter-0.6.8.patch">milter-0.6.8.patch</a>
Last minutes fixes from production testing.
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.8-3.i386.rpm">
milter-0.6.8-3.i386.rpm</a> Binary RPM for Redhat 7.x, now requires
sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
python2.3</a>.
<br>
<a href="http://bmsi.com/linux/rh9/milter-0.6.8-3.src.rpm">
milter-0.6.8-3.src.rpm</a> Source RPM for Redhat 9,7.x.
<p>
<a href="http://bmsi.com/python/milter-0.6.7.tar.gz">
milter-0.6.7.tar.gz</a> Explicit local socket bug,
<a href="http://spf.pobox.com/srs.html">SRS</a> forgery detection,
thread resource starvation detection.
SRS support requires <a href="http://bmsi.com/python/pysrs.html">pysrs</a>.
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.7-3.i386.rpm">
milter-0.6.7-3.i386.rpm</a> Binary RPM for Redhat 7.x, now requires
sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
python2.3</a>.
<br>
<a href="http://bmsi.com/linux/rh72/milter-0.6.7-3.src.rpm">
milter-0.6.7-3.src.rpm</a> Source RPM for Redhat 7.x.
Release 0.6.7-3 patches:
<ul>
<li> Defang message/rfc822 content_type with boundary
<li> Support SPF delegation
<li> Reject neutral SPF result for selected domains
</ul>
<p>
<a href="http://bmsi.com/python/milter-0.6.6.tar.gz">
milter-0.6.6.tar.gz</a> Plug another memory leak,
<a href="http://spf.pobox.com/">SPF</a> support, hello blacklist.
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
NOTE - the spf.py module included is modified from the official 1.6
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
I neglected to add the CVS log. The changes are expanded result codes
and tolerating common method misspellings in SPF records. I have notified the
author, but haven't heard back. At some point, the RPM will
include the official pyspf tarball and apply patches.
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.6-2.i386.rpm">
milter-0.6.6-2.i386.rpm</a> Binary RPM for Redhat 7.x, now requires
sendmail-8.12 and <a href="http://www.python.org/2.3.3/rpms.html">
python2.3</a>. Release 2 fixes sysv init script bug for python2.3.
<br>
<a href="http://bmsi.com/linux/rh72/milter-0.6.6-2.src.rpm">
milter-0.6.6-2.src.rpm</a> Source RPM for Redhat 7.x
<p>
<a href="http://bmsi.com/python/milter-0.6.5.tar.gz">
milter-0.6.5.tar.gz</a> Plug memory leak, progress reporting, trusted relay.
Redhat RPM now requires sendmail-8.12.
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.5-2.i386.rpm">
milter-0.6.5-2.i386.rpm</a> Binary RPM for Redhat 7.x
<br>
<a href="http://bmsi.com/linux/rh72/milter-0.6.5-2.src.rpm">
milter-0.6.5-2.src.rpm</a> Source RPM for Redhat 7.x
<p>
<a href="http://bmsi.com/python/milter-0.6.4.tar.gz">
milter-0.6.4.tar.gz</a> Numerous Dspam fixes. Requires
<a href="dspam.html">pydspam-1.1.5</a> and
<a href="/libdspam/dspam.html">dspam-2.6.5.2</a>
for Dspam features. The dspam-python RPM has been replaced by pydspam.
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.4-1.i386.rpm">
milter-0.6.4-1.i386.rpm</a> Binary RPM for Redhat 7.x
<p>
<a href="http://bmsi.com/python/milter-0.6.3.1.tar.gz">
milter-0.6.3.1.tar.gz</a> New dspam SCREENER feature with pydspam-1.1.4.
Don't save a defang copy of false positives. Fixed an oops from last fix,
rejecting false positives. BUG: sendmail-8.11 doesn't invoke milter
when sending mail via sendmail from command line (8.12 works). Therefore,
the supplied falsepositive script for milter based dspam doesn't work
with stock RedHat 7.x. I am writing a HOWTO for configuring milter
based dspam that will address this (and a fix in the next version).
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.3-1.i386.rpm">
milter-0.6.3-1.i386.rpm</a> Binary RPM for Redhat 7.x
<p>
<a href="http://bmsi.com/python/milter-0.6.2.tar.gz">
milter-0.6.2.tar.gz</a> work around email.Message.get_filename bug,
dspam_exempt list, REJECT messages with missing MIME boundaries (which
are almost always spam),
DISCARD messages which any dspam user flags as spam,
start.sh was calling python instead of python2 on Linux.
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.2-1.src.rpm">
milter-0.6.2-1.src.rpm</a> Source RPM for Redhat 7.x (and likely
higher versions)
<p>
<a href="http://bmsi.com/python/milter-0.6.1.tar.gz">
milter-0.6.1.tar.gz</a> dspam milter application, python-2.2.3 support.
<p>
You must have <a href=dspam.html>dspam and dspam-python</a> loaded for
the dspam feature to work. Brief instructions for configuring are
in the default config file. This is working at a customer, but I'm
sure a few more iterations will be required to make setup as smooth
as possible.
<p>
NOTE: Outlook destroys dspam tags when forwarding mail (while converting
HTML to text). Perhaps some config option will turn this abominable
"feature" off. Working around this by making dspam tags visble on
HTML mail is ugly. My suggestion is to not use Outlook, for this and
many other reasons - especially security. Any other suggestions for
those married to Microsoft are welcome. The DSPAM LDA works around this
by making the tags visible in HTML attachments. This is ugly, and
occasionally corrupts attachments.
<p>
We have to supply workarounds for bugs in the email module (reported
to sourceforge). The workarounds reference some internal variables
which change with python versions.
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.1-1.i386.rpm">
milter-0.6.1-1.i386.rpm</a> Binary RPM for Redhat 7.x
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.1-1.src.rpm">
milter-0.6.1-1.src.rpm</a> Source RPM for Redhat 7.x (and likely
higher versions)
<p>
<a href="http://bmsi.com/python/milter-0.6.0.tar.gz">
milter-0.6.0.tar.gz</a> simple dspam pre-filtering, use email module,
requires python &gt;= 2.2.2.
<ul>
<li> The milter.so module from 0.5.4
is needed to run this release on AIX. Haven't tracked this down yet.
<li> The patches to fix the email packages in mime.py don't work
on python-2.2.3. The email package is still broken in 2.3, and patches
required for that will likely be different still.
</ul>
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.0-1.i386.rpm">
milter-0.6.0-1.i386.rpm</a> Binary RPM for Redhat 7.x
<p>
<a href="http://bmsi.com/linux/rh72/milter-0.6.0-1.src.rpm">
milter-0.6.0-1.src.rpm</a> Source RPM for Redhat 7.x (and likely
higher versions)
<p>
<a href="http://www.bmsi.com/python/milter-0.5.5.tar.gz">
milter-0.5.5.tar.gz</a> IPV6 support, passing None to set_XXX_callback,
set_reply, chg_header, detect internal connections. Note, this release
did not work on AIX4.1.5, probably due to IPV6 support breaking something.
The milter.so module from 0.5.4 can be installed to use this release
with AIX.
<p>
<a href="http://www.bmsi.com/python/milter-0.5.4.tar.gz">
milter-0.5.4.tar.gz</a> wiretap, smart alias features, quarantine support.
<p>
The name of the production "sample" milter "bms.py" now
stands for "Basic Milter System" until someone suggests a better name.
The test coverage is rather
sparse at present.
Please <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">email</a> with proposals for what
to name the milter application.
<h4>NOTES</h4>
<ul>
<li>
Quarantine support requires that you define _FFR_QUARANTINE
when compiling miltermodule.c. I am not sure how to make setup.py
do that for you iff sendmail was actually compiled with _FFR_QUARANTINE.
<li>
While 0.6.0 will use the new email package in Python-2.2, that
package seems to be buggy in Python-2.2.1. The list example in the docs
doesn't find all MIME parts. Update: Python-2.2.2 has fixed the email
package. It can now parse my test cases.
<li>
Preliminary testing with python-2.2 shows that most things work after
adding <code>self.readahead = ""</code> to <code>mimepart.seek</code>.
Python-2.2 <code>multifile</code> reads one less newline per section than
2.1. I'm not not sure which is correct. After adding some calls to
<code>rstrip()</code> in testmime.py, all milter modules pass unit testing
with python-2.2. Python-2.2 patches have been released since 0.5.3.
<li>
sgmlop-1.1a3 has a memory leak (at least Python milter has a
memory leak when using sgmlop instead of sgmllib). Do not make Python
milter use sgmlop-1.1a2 or a3 in a production
system unless you can restart your milter periodically. The amount
of memory leaked seems roughly proportional to the amount of HTML
parsed.
<li>
There are a number of ways that malformed MIME attachments
can cause a python traceback. Uncaught exceptions cause a 415
error to be returned to sendmail. So far, all the malformed messages
I've investigated have been SPAM - so good riddance. I would prefer,
however, that the mime handling libraries were more precise. Beginning
with 0.5.1, bms.py will save messages that cause a traceback during
scanning in the tempfile directory with a ".fail" extension. This
makes it easier to get samples of mail that causes parsing problems
for incorporation into the unit tests.
</ul>
<p>
<a href="http://www.bmsi.com/python/milter-0.5.2.tar.gz">
milter-0.5.2.tar.gz</a> Fix and unittest another HTML parsing bug.<br>
<a href="http://www.bmsi.com/python/milter-0.5.1.tar.gz">
milter-0.5.1.tar.gz</a> Handle encoded rfc822 attachments.<br>
<a href="http://www.bmsi.com/python/milter-0.5.0.tar.gz">
milter-0.5.0.tar.gz</a> Use a config file so users don't have to
keep syncing with bms.py. <br>
<a href="http://www.bmsi.com/python/milter-0.4.5.tar.gz">
milter-0.4.5.tar.gz</a> Work with sgmlop. Reduce local hacks to config variables.
<p>
Python milter is under GPL. The authors can probably be convinced to
change this to LGPL.
<h3>What is a <a name="milter">milter</a>?</h3>
Milters can run on the same machine as sendmail, or another machine. The
milter can even run with a different operating system or processor than
sendmail.
Sendmail talks to the milter via a local or internet socket.
Sendmail keeps the
milter informed of events as it processes a mail connection. At any
point, the milter can cut the conversation short by telling sendmail
to ACCEPT, REJECT, or DISCARD the message. After receiving a complete
message from sendmail, the milter can again REJECT or DISCARD it, but it
can also ACCEPT it with changes to the headers or body.
<h3> What can you do with a milter? </h3>
<menu>
<li> A milter can DISCARD or REJECT spam based based on algorithms scripted
in python rather than sendmail's cryptic "cf" language.
<li> A milter can alter or remove attachments from mail that are poisonous to
Windows.
<li> A milter can scan for viruses and clean them when detected.
<li> A milter scans outgoing as well as incoming mail.
<li> A milter can add and delete recipients to forward or secretly
copy mail.
<li> For more ideas, check the <a href="//www.milter.org">Milter Web Page</a>.
</menu>
<a href="http://www.milter.org/milter_api/api.html">
Documentation</a> for the C API is provided with sendmail. Miltermodule
provides a thin python wrapper for the C API. Milter.py provides a simple
OO wrapper on top of that.
<p>
The Python milter package includes a sample milter that replaces dangerous
attachments with a warning message, discards mail addressed to
MAILER-DAEMON, and demonstrates several SPAM abatement strategies.
The MimeMessage class to do this used to be based on the
<code>mimetools</code> and <code>multifile</code> standard python packages.
As of milter version 0.6.0, it is based on the email standard
python packages, which were derived from the
<a href="http://sourceforge.net/projects/mimelib">mimelib</a> project.
The MimeMessage class patches several bugs in the email package,
and provides some backward compatibility.
<p>
The "defang" function of the sample milter was inspired by
<a href="http://www.roaringpenguin.com/mimedefang/">MIMEDefang</a>,
a Perl milter with flexible attachment processing options. The latest
version of MIMEDefang uses an apache style process pool to avoid reloading
the Perl interpreter for each message. This makes it fast enough for
production and does not use Perl threading.
<p>
<a href="http://sourceforge.net/projects/mailchecker">mailchecker</a> is
a Python project to provide flexible attachment processing for mail. I
will be looking at plugging mailchecker into a milter.
<p>
<a href="http://software.libertine.org/tmda/">TMDA</a> is a Python project
to require confirmation the first time someone tries to send to your
mailbox. This would be a nice feature to have in a milter.
<p>
There is also a <a href="http://www.milter.org/">Milter community website</a>
where milter software and gory details of the API are discussed.
<h3> Is a milter written in python efficient? </h3>
The python milter process is multi-threaded and startup cost is incurred
only once. This is much more efficient than some implementations that
start a new interpreter for each connection. Testing in a production
environment did not use a significant percentage of the CPU. Furthermore,
python is easily extended in C for any step requiring expensive CPU
processing.
<p>
For example, the HTML parsing feature to remove scripts from HTML attachments
is rather CPU intensive in pure python. Using the C replacement for sgmllib
greatly speeds things up.
<h3> Goals </h3>
<menu>
<li> Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
forwarder accounts (perhaps in <code>~/.forwarders</code>), and a util
provides a special local alias for the user to give to the forwarder.
Alias only works for mail from that forwarder. Milter gets forwarder
domain from alias and uses it to SPF check forwarder. Requires
milter to have read access to <code>~/.forwarders</code> or else
a way for user to submit entries to milter database.
<li> The bms.py milter has too many features. Create a framework where
numerous small feature modules can be plugged together in the
configuration.
<li> Create a pure python substitute for miltermodule and libmilter that
implements the <a
href="http://www.duh.org/cvsweb.cgi/~checkout~/pmilter/doc/milter-protocol.txt?rev=1">
libmilter protocol</a> in python.
<li> Find or write a faster implementation of sgmllib. The
<a href="http://www.effbot.org/zone/sgmlop-index.htm">sgmlop package</a>
is not very compatible with
<a href="http://www.python.org/doc/2.1.3/lib/module-sgmllib.html">
Python-2.1 sgmllib</a>, but it is a start, and is supported in
milter-0.4.5 or later.
<li> Implement all or most of the features of
<a href="http://www.roaringpenguin.com/mimedefang/">MIMEDefang</a>.
<li> Follow the official <a href="http://www.python.org/peps/pep-0008.html">
Python coding standards</a> more closely.
<li> Make unit test code more like other python modules.
</menu>
<h3> Confirmed Installations </h3>
Please <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">email</a>
me if you successfully install milter on a system not mentioned below.
<p>
<table>
<tr>
<th>Operating System</th> <th>Compiler</th> <th>Python</th> <th>Sendmail</th>
<th>milter</th>
<tr>
<td>Mandrake 8.0</td><td>gcc-3.0.1</td><td>2.1.1</td><td>8.12.0</td>
<td>0.3.3</td><tr>
<td>Mandrake 8.0</td><td>gcc-2.96</td><td>2.0</td><td>8.11.2</td>
<td>0.3.6</td><tr>
<td>RedHat 6.2</td><td>egcs-1.1.2</td><td>2.2.2</td><td>8.11.6</td>
<td>0.5.4</td><tr>
<td>RedHat 7.1</td><td>gcc-2.96</td><td>?</td><td>8.12.1</td>
<td>0.3.5</td><tr>
<td>RedHat 7.2</td><td>gcc-2.96</td><td>2.1.1</td><td>8.11.6</td>
<td>0.4.1</td><tr>
<td>RedHat 7.2</td><td>gcc-2.96</td><td>2.2.1</td><td>8.11.6</td>
<td>0.4.5</td><tr>
<td>RedHat 7.2</td><td>gcc-2.96</td><td>2.2.2</td><td>8.11.6</td>
<td>0.5.5</td><tr>
<td>RedHat 7.2</td><td>gcc-2.96</td><td>2.3.3</td><td>8.12.10</td>
<td>0.6.6</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.2.2</td><td>8.11.6</td>
<td>0.5.5</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.12.10</td>
<td>0.6.6</td><tr>
<td>RedHat 8.0</td><td>gcc-3.2</td><td>2.2.1</td><td>8.12.6</td>
<td>0.5.2</td><tr>
<td>Debian Linux</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.0</td>
<td>0.3.7</td><tr>
<td>Debian Linux</td><td>gcc-3.2.2</td><td>2.2.2</td><td>8.12.7</td>
<td>0.5.4</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.11.5</td>
<td>0.3.3</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.1</td>
<td>0.3.4</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.1.3</td><td>8.12.3</td>
<td>0.4.2</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.2.2</td><td>8.12.6</td>
<td>0.5.4</td><tr>
<td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td>
<td>0.3.8</td><tr>
<td>Slackware 9.0</td><td>gcc-3.2.2</td><td>2.2.3</td><td>8.12.9</td>
<td>0.5.4</td><tr>
<td>OpenBSD</td><td>?</td><td>2.1.1</td><td>8.11.6</td>
<td>0.3.9</td><tr>
<td>SuSE 7.3</td><td>gcc-2.95.3</td><td>2.1.1</td><td>8.12.2</td>
<td>0.3.9</td><tr>
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.1</td><td>8.12.3</td>
<td>0.4.0</td><tr>
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td>
<td>0.5.5</td><tr>
<td>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td>
<td>0.6.6</td><tr>
</table>
<h3> Requirements </h3>
<menu>
<li> While the miltermodule will work with python 1.5, you probably
want to use python 2.0 or better. The python code uses a number of
python 2 features.
<li> Python must be configured with thread support. This is because
sendmail's libmilter requires thread support.
<li> You must compile sendmail with libmilter enabled. In versions of
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
is not installed by default.
Sendmail 8.12 still does not enable libmilter by default. You must
explicitly select the "MILTER" option when compiling.
<li> Python milter has been tested against sendmail-8.11 and sendmail-8.12.
<li> Python milter must be compiled for the specific version of sendmail
it will run with. (Since the result is dynamically loaded, there could
conceivably be multiple versions available and selected at startup - but
that will have to wait.) This situation may only exist for sendmail
versions prior to 8.12. The protocol seems designed for backward
compatibility - and 8.12 is the first official milter release.
<li> Mea Culpa! After reading the Python Style guide, I realize that
my Python code is not up to snuff. Apparently mixed tabs and spaces
are anathema to those using Windows editors, where tabs can be expanded using
any arbitrary algorithm. Other than that, my
intuition matched Guido's pretty well - although I like to indent by 2
rather than 4. I will arrange to have tabs expanded to spaces when
exporting new versions. Until then, beware!
</menu>
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
to represent the stuff that comes with AIX. You might also want
my <a href="/aix/python.spec">python-2.1.1</a> spec file for AIX. It
does not include Tk or curses modules, sorry. If y'all trust me, you can
download rpms for AIX 4.x from my <a href="/aix">AIX RPM directory</a>.
<p>
Sendmail-8.12 renames
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system
shared library. Therefore, I rename libsm.a back to libsmutil.a for
AIX. This presents a problem for setup.py.
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
If you are running Redhat 7.2, the distributed version of sendmail
now enables libmilter by default. RedHat 7.2 bundles
the development libraries with the main sendmail package, so
there is no sendmail-devel package. However, they forgot to include the
headers! So you'll have to get the SRPM and modify it. I suggest
moving the static libs to a devel package and adding the headers. If
this is too much trouble, you can get the <a href="mfapi.h">mfapi.h</a>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
<p>
If you do modify the SRPM, I suggest renaming libsmutil.a
to libsm.a - just like sendmail-8.12 will. If you manually install
mfapi.h or don't rename libsmutil.a, you'll
need to force <code>libs = ["milter", "smutil"]</code> in setup.py.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
If you are running Redhat 6.2, the distributed version of sendmail
does not enable libmilter. You can download the Redhat 7.2 sendmail.spec
modified to compile on RedHat 6.2:
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
sendmail-rhmilter.spec</a>. The <a
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
SRPM for sendmail-8.11.6</a> is available from
<a href="http://www.redhat.com">Redhat</a> under
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
Errata for RH6.2</a>. But that doesn't include the latest security
patches since RH6.2 is no longer supported.
<p>
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> directory.
The lastest RPMs were built by taking a RH7.2 SRPMS and removing some
RPM features from the spec file that RH6.2 doesn't support, then
recompiling on RH6.2. You can check this by installing the RH7.2 SRPM,
then diffing my sendmail.spec with theirs. Then run
"rpm -bb sendmail-rhmilter.spec" when you are satisfied.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
You'll need to install the sendmail-devel package to compile milter.
<hr>
<p>
<a href="http://validator.w3.org/check/referer">
<img border=0 src="/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
<a href="http://www.redhat.com">
<img src="/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
</p>
</body></html>
-81
View File
@@ -1,81 +0,0 @@
#!/bin/bash
#
# milter This shell script takes care of starting and stopping milter.
#
# chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail.
# processname: milter
# config: /var/log/milter/bms.py
# pidfile: /var/run/milter/milter.pid
python="python2.3"
pidof() {
set - ""
if set - `ps -e -o pid,cmd | grep "${python} bms.py"` &&
[ "$2" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
[ -x /var/log/milter/start.sh ] || exit 0
RETVAL=0
prog="milter"
start() {
# Start daemons.
echo -n "Starting $prog: "
daemon --check milter --user mail /var/log/milter/start.sh
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc milter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/milter ]; then
stop
start
RETVAL=$?
fi
;;
status)
status milter
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
-81
View File
@@ -1,81 +0,0 @@
#!/bin/bash
#
# milter This shell script takes care of starting and stopping milter.
#
# chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail.
# processname: milter
# config: /var/log/milter/bms.py
# pidfile: /var/run/milter/milter.pid
python="python2.3"
pidof() {
set - ""
if set - `ps -e -o pid,wchan,cmd | grep "rt_sig ${python} bms.py"` &&
[ "$3" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
[ -x /var/log/milter/start.sh ] || exit 0
RETVAL=0
prog="milter"
start() {
# Start daemons.
echo -n "Starting $prog: "
daemon --check milter --user mail /var/log/milter/start.sh
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc milter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/milter ]; then
stop
start
RETVAL=$?
fi
;;
status)
status milter
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
-179
View File
@@ -1,179 +0,0 @@
%define name milter
%define version 0.6.9
%define release 1
# Redhat 7.x and earlier (multiple ps lines per thread)
%define sysvinit milter.rc7
# RH9, other systems (single ps line per process)
#define sysvinit milter.rc
%ifos Linux
%define python python2.3
%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.2.2, sendmail >= 8.12
BuildRequires: %{python}-devel >= 2.2.2, sendmail-devel >= 8.12
%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 $RPM_BUILD_ROOT/var/log/milter/save
cp bms.py milter.cfg $RPM_BUILD_ROOT/var/log/milter
# 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
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
%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
%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/milter.cfg
%changelog
* 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
+326 -95
View File
@@ -1,18 +1,20 @@
/* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org) /* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org)
* Portions Copyright (C) 2001,2002,2003,2004,2005,2006,2007
* Stuart Gathman (stuart@bmsi.com)
* *
* This program is free software; you can redistribute it and/or * This program is free software: you can redistribute it and/or modify it
* modify it under the terms of the GNU General Public License * under the terms of the GNU General Public License as published by the
* as published by the Free Software Foundation; either version 2 * Free Software Foundation, either version 2 of the License, or (at your
* of the License, or (at your option) any later version. * option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful, but
* but WITHOUT ANY WARRANTY; without even the implied warranty of * WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* GNU General Public License for more details. * General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License along
* along with this program; if not, write to the Free Software * with this program; if not, write to the Free Software Foundation, Inc.,
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
* *
* milterContext object and thread interface contributed by * milterContext object and thread interface contributed by
* Stuart D. Gathman <stuart@bmsi.com> * Stuart D. Gathman <stuart@bmsi.com>
@@ -33,6 +35,66 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* Revision 1.16 2008/12/16 04:21:05 customdesigned
* Fedora release
*
* Revision 1.15 2008/12/13 20:29:56 customdesigned
* Split off milter applications.
*
* Revision 1.14 2008/12/04 19:43:00 customdesigned
* Doc updates.
*
* Revision 1.13 2008/11/23 03:06:47 customdesigned
* Milter support for chgfrom.
*
* Revision 1.12 2008/11/21 20:42:52 customdesigned
* Support smfi_chgfrom and smfi_addrcpt_par.
*
* Revision 1.11 2007/09/25 02:26:29 customdesigned
* Update license.
*
* Revision 1.10 2006/02/12 02:00:42 customdesigned
* Resolve FIXME for wrap_close.
*
* Revision 1.9 2005/12/23 21:46:36 customdesigned
* Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER)
*
* Revision 1.8 2005/10/20 23:23:36 customdesigned
* Include smfi_progress is SMFIR_PROGRESS defined
*
* Revision 1.7 2005/10/20 23:04:46 customdesigned
* Add optional idx for position of added header.
*
* Revision 1.6 2005/07/15 22:18:17 customdesigned
* Support callback exception policy
*
* Revision 1.5 2005/06/24 04:20:07 customdesigned
* Report context allocation error.
*
* Revision 1.4 2005/06/24 04:12:43 customdesigned
* Remove unused name argument to generic wrappers.
*
* Revision 1.3 2005/06/24 03:57:35 customdesigned
* Handle close called before connect.
*
* Revision 1.2 2005/06/02 04:18:55 customdesigned
* Update copyright notices after reading article on /.
*
* Revision 1.1.1.2 2005/05/31 18:09:06 customdesigned
* Release 0.7.1
*
* Revision 2.31 2004/08/23 02:24:36 stuart
* Support setbacklog
*
* Revision 2.30 2004/08/21 20:29:53 stuart
* Support option of 11 lines max for mlreply.
*
* Revision 2.29 2004/08/21 04:14:29 stuart
* mlreply support
*
* Revision 2.28 2004/08/21 02:45:21 stuart
* Don't leak int constants if module unloaded.
*
* Revision 2.27 2004/04/06 03:19:59 stuart * Revision 2.27 2004/04/06 03:19:59 stuart
* Release 0.6.8 * Release 0.6.8
* *
@@ -127,10 +189,19 @@ $ python setup.py help
* *
*/ */
#include <pthread.h> #ifndef MAX_ML_REPLY
#include <netinet/in.h> #define MAX_ML_REPLY 32
#include <Python.h> #endif
#include <libmilter/mfapi.h> #if MAX_ML_REPLY != 1 && MAX_ML_REPLY != 32 && MAX_ML_REPLY != 11
#error MAX_ML_REPLY must be 1 or 11 or 32
#endif
#define _FFR_MULTILINE (MAX_ML_REPLY > 1)
//#include <pthread.h> // shouldn't be needed - use Python API
#include <Python.h> // Python C API
#include <libmilter/mfapi.h> // libmilter API
#include <netinet/in.h> // socket API
/* See if we have IPv4 and/or IPv6 support in this OS and in /* See if we have IPv4 and/or IPv6 support in this OS and in
* libmilter. We need to make several macro tests because some OS's * libmilter. We need to make several macro tests because some OS's
@@ -169,7 +240,7 @@ $ python setup.py help
/* Yes, these are static. If you need multiple different callbacks, */ /* Yes, these are static. If you need multiple different callbacks, */
/* it's cleaner to use multiple filters. */ /* it's cleaner to use multiple filters, or convert to OO method calls. */
static PyObject *connect_callback = NULL; static PyObject *connect_callback = NULL;
static PyObject *helo_callback = NULL; static PyObject *helo_callback = NULL;
static PyObject *envfrom_callback = NULL; static PyObject *envfrom_callback = NULL;
@@ -196,10 +267,10 @@ typedef struct {
PyThreadState *t; /* python thread state */ PyThreadState *t; /* python thread state */
} milter_ContextObject; } milter_ContextObject;
/* Return a borrowed reference to the python Context. Create a /* Return a borrowed reference to the python Context. Called by callbacks
new Context if needed. The new Python Context is owned by invoked by libmilter. Create a new Context if needed. The new
the SMFICTX. The python interpreter is locked on successful Python Context is owned by the SMFICTX. The python interpreter is locked on
return, otherwise not. */ successful return, otherwise not. */
static milter_ContextObject * static milter_ContextObject *
_get_context(SMFICTX *ctx) { _get_context(SMFICTX *ctx) {
milter_ContextObject *self = smfi_getpriv(ctx); milter_ContextObject *self = smfi_getpriv(ctx);
@@ -214,8 +285,11 @@ _get_context(SMFICTX *ctx) {
PyEval_AcquireThread(t); /* lock interp */ PyEval_AcquireThread(t); /* lock interp */
self = PyObject_New(milter_ContextObject,&milter_ContextType); self = PyObject_New(milter_ContextObject,&milter_ContextType);
if (!self) { if (!self) {
/* Can't pass on exception since we are called from libmilter */ /* Report and clear exception since we are called from libmilter */
PyErr_Clear(); if (PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
PyThreadState_Clear(t); PyThreadState_Clear(t);
PyEval_ReleaseThread(t); PyEval_ReleaseThread(t);
PyThreadState_Delete(t); PyThreadState_Delete(t);
@@ -230,14 +304,15 @@ _get_context(SMFICTX *ctx) {
return self; return self;
} }
/* Find the SMFICTX from a Python Context. The interpreter must be locked. */ /* Find the SMFICTX from a Python Context. Called by context methods invoked
from python. The interpreter must be locked. */
static SMFICTX * static SMFICTX *
_find_context(PyObject *c) { _find_context(PyObject *c) {
SMFICTX *ctx = NULL; SMFICTX *ctx = NULL;
if (c->ob_type == &milter_ContextType) { if (c->ob_type == &milter_ContextType) {
milter_ContextObject *self = (milter_ContextObject *)c; milter_ContextObject *self = (milter_ContextObject *)c;
ctx = self->ctx; ctx = self->ctx;
if (smfi_getpriv(ctx) != self) if (ctx != NULL && smfi_getpriv(ctx) != self)
ctx = NULL; ctx = NULL;
} }
if (ctx == NULL) if (ctx == NULL)
@@ -245,23 +320,6 @@ _find_context(PyObject *c) {
return ctx; return ctx;
} }
/* Release the Python Context for a SMFICTX. */
static void
_clear_context(SMFICTX *ctx) {
milter_ContextObject *self = smfi_getpriv(ctx);
if (self) {
PyThreadState *t = self->t;
PyEval_AcquireThread(t);
self->t = 0;
self->ctx = 0;
smfi_setpriv(ctx,0);
Py_DECREF(self);
PyThreadState_Clear(t);
PyEval_ReleaseThread(t);
PyThreadState_Delete(t);
}
}
static void static void
milter_Context_dealloc(PyObject *s) { milter_Context_dealloc(PyObject *s) {
milter_ContextObject *self = (milter_ContextObject *)s; milter_ContextObject *self = (milter_ContextObject *)s;
@@ -300,13 +358,15 @@ static char milter_set_flags__doc__[] =
Set flags for filter capabilities; OR of one or more of:\n\ Set flags for filter capabilities; OR of one or more of:\n\
ADDHDRS - filter may add headers\n\ ADDHDRS - filter may add headers\n\
CHGBODY - filter may replace body\n\ CHGBODY - filter may replace body\n\
CHGFROM - filter may replace body\n\
ADDRCPT - filter may add recipients\n\ ADDRCPT - filter may add recipients\n\
DELRCPT - filter may delete recipients\n\ DELRCPT - filter may delete recipients\n\
CHGHDRS - filter may change/delete headers"; CHGHDRS - filter may change/delete headers";
static PyObject * static PyObject *
milter_set_flags(PyObject *self, PyObject *args) { milter_set_flags(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "i", &description.xxfi_flags)) return NULL; if (!PyArg_ParseTuple(args, "i:set_flags", &description.xxfi_flags))
return NULL;
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
} }
@@ -472,17 +532,52 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_close_callback", &close_callback); return generic_set_callback(args, "O:set_close_callback", &close_callback);
} }
static int exception_policy = SMFIS_TEMPFAIL;
static char milter_set_exception_policy__doc__[] =
"set_exception_policy(i) -> None\n\
Sets the policy for untrapped Python exceptions during a callback.\n\
Must be one of TEMPFAIL,REJECT,CONTINUE";
static PyObject *
milter_set_exception_policy(PyObject *self, PyObject *args) {
int i;
if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i))
return NULL;
switch (i) {
case SMFIS_REJECT: case SMFIS_TEMPFAIL: case SMFIS_CONTINUE:
exception_policy = i;
Py_INCREF(Py_None);
return Py_None;
}
PyErr_SetString(MilterError,"invalid exception policy");
return NULL;
}
static void
_release_thread(PyThreadState *t) {
if (t != NULL)
PyEval_ReleaseThread(t);
}
/** Report and clear any python exception before returning to libmilter. /** Report and clear any python exception before returning to libmilter.
The interpreter is locked when we are called, and we unlock it. */ The interpreter is locked when we are called, and we unlock it. */
static int _report_exception(milter_ContextObject *self) { static int _report_exception(milter_ContextObject *self) {
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
PyErr_Print(); PyErr_Print();
PyErr_Clear(); /* must clear since not returning to python */ PyErr_Clear(); /* must clear since not returning to python */
PyEval_ReleaseThread(self->t); _release_thread(self->t);
smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure"); switch (exception_policy) {
return SMFIS_TEMPFAIL; case SMFIS_REJECT:
smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure");
return SMFIS_REJECT;
case SMFIS_TEMPFAIL:
smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure");
return SMFIS_TEMPFAIL;
}
return SMFIS_CONTINUE;
} }
PyEval_ReleaseThread(self->t); _release_thread(self->t);
return SMFIS_CONTINUE; return SMFIS_CONTINUE;
} }
@@ -501,7 +596,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
retval = PyInt_AsLong(result); retval = PyInt_AsLong(result);
Py_DECREF(result); Py_DECREF(result);
if (PyErr_Occurred()) return _report_exception(self); if (PyErr_Occurred()) return _report_exception(self);
PyEval_ReleaseThread(self->t); _release_thread(self->t);
return retval; return retval;
} }
@@ -591,7 +686,7 @@ milter_wrap_helo(SMFICTX *ctx, char *helohost) {
} }
static int static int
generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv, const char *name) { generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
PyObject *arglist; PyObject *arglist;
milter_ContextObject *self; milter_ContextObject *self;
int count = 0; int count = 0;
@@ -628,12 +723,12 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv, const char *name) {
static int static int
milter_wrap_envfrom(SMFICTX *ctx, char **argv) { milter_wrap_envfrom(SMFICTX *ctx, char **argv) {
return generic_env_wrapper(ctx,envfrom_callback,argv,"milter_wrap_envfrom"); return generic_env_wrapper(ctx,envfrom_callback,argv);
} }
static int static int
milter_wrap_envrcpt(SMFICTX *ctx, char **argv) { milter_wrap_envrcpt(SMFICTX *ctx, char **argv) {
return generic_env_wrapper(ctx,envrcpt_callback,argv,"milter_wrap_envrcpt"); return generic_env_wrapper(ctx,envrcpt_callback,argv);
} }
static int static int
@@ -649,7 +744,7 @@ milter_wrap_header(SMFICTX *ctx, char *headerf, char *headerv) {
} }
static int static int
generic_noarg_wrapper(SMFICTX *ctx,PyObject *cb,const char *name) { generic_noarg_wrapper(SMFICTX *ctx,PyObject *cb) {
PyObject *arglist; PyObject *arglist;
milter_ContextObject *c; milter_ContextObject *c;
if (cb == NULL) return SMFIS_CONTINUE; if (cb == NULL) return SMFIS_CONTINUE;
@@ -661,7 +756,7 @@ generic_noarg_wrapper(SMFICTX *ctx,PyObject *cb,const char *name) {
static int static int
milter_wrap_eoh(SMFICTX *ctx) { milter_wrap_eoh(SMFICTX *ctx) {
return generic_noarg_wrapper(ctx,eoh_callback,"milter_wrap_eoh"); return generic_noarg_wrapper(ctx,eoh_callback);
} }
static int static int
@@ -679,23 +774,42 @@ milter_wrap_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) {
static int static int
milter_wrap_eom(SMFICTX *ctx) { milter_wrap_eom(SMFICTX *ctx) {
return generic_noarg_wrapper(ctx,eom_callback,"milter_wrap_eom"); return generic_noarg_wrapper(ctx,eom_callback);
} }
static int static int
milter_wrap_abort(SMFICTX *ctx) { milter_wrap_abort(SMFICTX *ctx) {
/* libmilter still calls close after abort */ /* libmilter still calls close after abort */
return generic_noarg_wrapper(ctx,abort_callback,"milter_wrap_abort"); return generic_noarg_wrapper(ctx,abort_callback);
} }
static int static int
milter_wrap_close(SMFICTX *ctx) { milter_wrap_close(SMFICTX *ctx) {
int r = generic_noarg_wrapper(ctx,close_callback,"milter_wrap_close"); /* xxfi_close can be called out of order - even before connect.
/* FIXME: It is inefficient to have released the interp lock only to * There may not yet be a private context pointer. To avoid
acquire it again in _clear_context. We can tell _generic_return and * creating a ThreadContext and allocating a milter context only
friends not to release the lock by, for instance, setting self->t to NULL. * to destroy them, and to avoid invoking the python close_callback when
However, first we make it work. */ * connect has never been called, we don't use generic_noarg_wrapper here. */
_clear_context(ctx); PyObject *cb = close_callback;
milter_ContextObject *self = smfi_getpriv(ctx);
int r = SMFIS_CONTINUE;
if (self != NULL) {
PyThreadState *t = self->t;
PyEval_AcquireThread(t);
self->t = 0;
if (cb != NULL && self->ctx == ctx) {
PyObject *arglist = Py_BuildValue("(O)", self);
/* Call python close callback, but do not ReleaseThread, because
* self->t is NULL */
r = _generic_wrapper(self, cb, arglist);
}
self->ctx = 0;
smfi_setpriv(ctx,0);
Py_DECREF(self);
PyThreadState_Clear(t);
PyEval_ReleaseThread(t);
PyThreadState_Delete(t);
}
return r; return r;
} }
@@ -746,6 +860,18 @@ milter_setdbg(PyObject *self, PyObject *args) {
return _generic_return(smfi_setdbg(val), "cannot set debug value"); return _generic_return(smfi_setdbg(val), "cannot set debug value");
} }
static char milter_setbacklog__doc__[] =
"setbacklog(int) -> None\n\
Set the TCP connection queue size for the milter socket.";
static PyObject *
milter_setbacklog(PyObject *self, PyObject *args) {
int val;
if (!PyArg_ParseTuple(args, "i:setbacklog", &val)) return NULL;
return _generic_return(smfi_setbacklog(val), "cannot set backlog");
}
static char milter_settimeout__doc__[] = static char milter_settimeout__doc__[] =
"settimeout(int) -> None\n\ "settimeout(int) -> None\n\
Set the time (in seconds) that sendmail will wait before\n\ Set the time (in seconds) that sendmail will wait before\n\
@@ -820,41 +946,117 @@ static PyObject *
milter_setreply(PyObject *self, PyObject *args) { milter_setreply(PyObject *self, PyObject *args) {
char *rcode; char *rcode;
char *xcode; char *xcode;
char *message; char *message[MAX_ML_REPLY];
char fmt[MAX_ML_REPLY + 16];
SMFICTX *ctx; SMFICTX *ctx;
if (!PyArg_ParseTuple(args, "szz:setreply", &rcode, &xcode, &message)) int i;
strcpy(fmt,"sz|");
for (i = 0; i < MAX_ML_REPLY; ++i) {
message[i] = 0;
fmt[i+3] = 's';
}
strcpy(fmt+i+3,":setreply");
if (!PyArg_ParseTuple(args, fmt,
&rcode, &xcode, message
#if MAX_ML_REPLY > 1
,message+1,message+2,message+3,message+4,message+5,message+6,
message+7,message+8,message+9,message+10
#if MAX_ML_REPLY > 11
,message+11,message+12,message+13,message+14,message+15,
message+16,message+17,message+18,message+19,message+20,
message+21,message+22,message+23,message+24,message+25,
message+26,message+27,message+28,message+29,message+30,
message+31
#endif
#endif
))
return NULL; return NULL;
ctx = _find_context(self); ctx = _find_context(self);
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
return _generic_return(smfi_setreply(ctx, rcode, xcode, message), #if MAX_ML_REPLY > 1
/*
* C varargs might be convenient for some things, but they sure are a pain
* when the number of args is not known at compile time.
*/
if (message[0] && message[1])
return _generic_return(smfi_setmlreply(ctx, rcode, xcode,
message[0],
message[1],message[2],message[3],message[4],message[5],
message[6],message[7],message[8],message[9],message[10],
#if MAX_ML_REPLY > 11
message[11],message[12],message[13],message[14],message[15],
message[16],message[17],message[18],message[19],message[20],
message[21],message[22],message[23],message[24],message[25],
message[26],message[27],message[28],message[29],message[30],
message[31],
#endif
(char *)0
), "cannot set reply");
#endif
return _generic_return(smfi_setreply(ctx, rcode, xcode, message[0]),
"cannot set reply"); "cannot set reply");
} }
static char milter_addheader__doc__[] = static char milter_addheader__doc__[] =
"addheader(field, value) -> None\n\ "addheader(field, value, idx=-1) -> None\n\
Add a header to the message. This header is not passed to other\n\ Add a header to the message. This header is not passed to other\n\
filters. It is not checked for standards compliance;\n\ filters. It is not checked for standards compliance;\n\
the mail filter must ensure that no protocols are violated\n\ the mail filter must ensure that no protocols are violated\n\
as a result of adding this header.\n\ as a result of adding this header.\n\
field - header field name\n\ field - header field name\n\
value - header field value\n\ value - header field value\n\
idx - optional position in internal header list to insert new header\n\
Both are strings. This function can only be called from the EOM callback."; Both are strings. This function can only be called from the EOM callback.";
static PyObject * static PyObject *
milter_addheader(PyObject *self, PyObject *args) { milter_addheader(PyObject *self, PyObject *args) {
char *headerf; char *headerf;
char *headerv; char *headerv;
int idx = -1;
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
if (!PyArg_ParseTuple(args, "ss:addheader", &headerf, &headerv)) return NULL; if (!PyArg_ParseTuple(args, "ss|i:addheader", &headerf, &headerv, &idx))
return NULL;
ctx = _find_context(self); ctx = _find_context(self);
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
t = PyEval_SaveThread(); t = PyEval_SaveThread();
return _thread_return(t,smfi_addheader(ctx, headerf, headerv), #ifdef SMFIR_INSHEADER
"cannot add header"); return _thread_return(t, (idx < 0) ? smfi_addheader(ctx, headerf, headerv) :
smfi_insheader(ctx, idx, headerf, headerv), "cannot add header");
#else
if (idx < 0)
return _thread_return(t, smfi_addheader(ctx, headerf, headerv),
"cannot add header");
PyErr_SetString(MilterError, "insheader not supported");
return NULL;
#endif
} }
#ifdef SMFIF_CHGFROM
static char milter_chgfrom__doc__[] =
"chgfrom(sender,params) -> None\n\
Change the envelope sender (MAIL From) of the current message.\n\
A filter which calls smfi_chgfrom must have set the CHGFROM flag\n\
in set_flags() before calling register.\n\
This function can only be called from the EOM callback.";
static PyObject *
milter_chgfrom(PyObject *self, PyObject *args) {
char *sender;
char *params;
SMFICTX *ctx;
PyThreadState *t;
if (!PyArg_ParseTuple(args, "s|z:chgfrom", &sender, &params))
return NULL;
ctx = _find_context(self);
if (ctx == NULL) return NULL;
t = PyEval_SaveThread();
return _thread_return(t,smfi_chgfrom(ctx, sender, params),
"cannot change sender");
}
#endif
static char milter_chgheader__doc__[] = static char milter_chgheader__doc__[] =
"chgheader(field, int, value) -> None\n\ "chgheader(field, int, value) -> None\n\
Change/delete a header in the message. \n\ Change/delete a header in the message. \n\
@@ -884,22 +1086,33 @@ milter_chgheader(PyObject *self, PyObject *args) {
} }
static char milter_addrcpt__doc__[] = static char milter_addrcpt__doc__[] =
"addrcpt(string) -> None\n\ "addrcpt(string,params=None) -> None\n\
Add a recipient to the envelope. It must be in the same format\n\ Add a recipient to the envelope. It must be in the same format\n\
as is passed to the envrcpt callback in the first tuple element.\n\ as is passed to the envrcpt callback in the first tuple element.\n\
If params is used, you must pass ADDRCPT_PAR to set_flags().\n\
This function can only be called from the EOM callback."; This function can only be called from the EOM callback.";
static PyObject * static PyObject *
milter_addrcpt(PyObject *self, PyObject *args) { milter_addrcpt(PyObject *self, PyObject *args) {
char *rcpt; char *rcpt;
char *params = 0;
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
int rc;
if (!PyArg_ParseTuple(args, "s:addrcpt", &rcpt)) return NULL; if (!PyArg_ParseTuple(args, "s|z:addrcpt", &rcpt, &params)) return NULL;
ctx = _find_context(self); ctx = _find_context(self);
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
t = PyEval_SaveThread(); t = PyEval_SaveThread();
return _thread_return(t,smfi_addrcpt(ctx, rcpt), "cannot add recipient"); if (params)
#ifdef SMFIF_ADDRCPT_PAR
rc = smfi_addrcpt_par(ctx,rcpt,params);
#else
rc = MI_FAILURE;
#endif
else
rc = smfi_addrcpt(ctx,rcpt);
return _thread_return(t,rc, "cannot add recipient");
} }
static char milter_delrcpt__doc__[] = static char milter_delrcpt__doc__[] =
@@ -939,8 +1152,8 @@ milter_replacebody(PyObject *self, PyObject *args) {
ctx = _find_context(self); ctx = _find_context(self);
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
t = PyEval_SaveThread(); t = PyEval_SaveThread();
return _thread_return(t,smfi_replacebody(ctx, bodyp, bodylen), return _thread_return(t,smfi_replacebody(ctx,
"cannot replace message body"); (unsigned char *)bodyp, bodylen), "cannot replace message body");
} }
static char milter_setpriv__doc__[] = static char milter_setpriv__doc__[] =
@@ -986,7 +1199,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
return o; return o;
} }
#if _FFR_QUARANTINE #ifdef SMFIF_QUARANTINE
static char milter_quarantine__doc__[] = static char milter_quarantine__doc__[] =
"quarantine(string) -> None\n\ "quarantine(string) -> None\n\
Place the message in quarantine. A string with a description of the reason\n\ Place the message in quarantine. A string with a description of the reason\n\
@@ -1007,7 +1220,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
} }
#endif #endif
#if _FFR_SMFI_PROGRESS #ifdef SMFIR_PROGRESS
static char milter_progress__doc__[] = static char milter_progress__doc__[] =
"progress() -> None\n\ "progress() -> None\n\
Notify the MTA that we are working on a message so it will reset timeouts."; Notify the MTA that we are working on a message so it will reset timeouts.";
@@ -1035,11 +1248,14 @@ static PyMethodDef context_methods[] = {
{ "replacebody", milter_replacebody, METH_VARARGS, milter_replacebody__doc__}, { "replacebody", milter_replacebody, METH_VARARGS, milter_replacebody__doc__},
{ "setpriv", milter_setpriv, METH_VARARGS, milter_setpriv__doc__}, { "setpriv", milter_setpriv, METH_VARARGS, milter_setpriv__doc__},
{ "getpriv", milter_getpriv, METH_VARARGS, milter_getpriv__doc__}, { "getpriv", milter_getpriv, METH_VARARGS, milter_getpriv__doc__},
#if _FFR_QUARANTINE #ifdef SMFIF_QUARANTINE
{ "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__}, { "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__},
#endif #endif
#if _FFR_SMFI_PROGRESS #ifdef SMFIR_PROGRESS
{ "progress", milter_progress, METH_VARARGS, milter_progress__doc__}, { "progress", milter_progress, METH_VARARGS, milter_progress__doc__},
#endif
#ifdef SMFIF_CHGFROM
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
#endif #endif
{ NULL, NULL } { NULL, NULL }
}; };
@@ -1077,10 +1293,13 @@ static PyMethodDef milter_methods[] = {
{ "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__}, { "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__},
{ "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__}, { "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__},
{ "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__}, { "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__},
{ "set_exception_policy", milter_set_exception_policy,METH_VARARGS, milter_set_exception_policy__doc__},
{ "register", milter_register, METH_VARARGS, milter_register__doc__},
{ "register", milter_register, METH_VARARGS, milter_register__doc__}, { "register", milter_register, METH_VARARGS, milter_register__doc__},
{ "main", milter_main, METH_VARARGS, milter_main__doc__}, { "main", milter_main, METH_VARARGS, milter_main__doc__},
{ "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__}, { "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__},
{ "settimeout", milter_settimeout, METH_VARARGS, milter_settimeout__doc__}, { "settimeout", milter_settimeout, METH_VARARGS, milter_settimeout__doc__},
{ "setbacklog", milter_setbacklog, METH_VARARGS, milter_setbacklog__doc__},
{ "setconn", milter_setconn, METH_VARARGS, milter_setconn__doc__}, { "setconn", milter_setconn, METH_VARARGS, milter_setconn__doc__},
{ "stop", milter_stop, METH_VARARGS, milter_stop__doc__}, { "stop", milter_stop, METH_VARARGS, milter_stop__doc__},
{ NULL, NULL } { NULL, NULL }
@@ -1116,6 +1335,12 @@ allowing one to write email filters directly in Python.\n\
Libmilter is currently marked FFR, and needs to be explicitly installed.\n\ Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
See <sendmailsource>/libmilter/README for details on setting it up.\n"; See <sendmailsource>/libmilter/README for details on setting it up.\n";
static void setitem(PyObject *d,const char *name,long val) {
PyObject *v = PyInt_FromLong(val);
PyDict_SetItemString(d,name,v);
Py_DECREF(v);
}
void void
initmilter(void) { initmilter(void) {
PyObject *m, *d; PyObject *m, *d;
@@ -1125,24 +1350,30 @@ initmilter(void) {
d = PyModule_GetDict(m); d = PyModule_GetDict(m);
MilterError = PyErr_NewException("milter.error", NULL, NULL); MilterError = PyErr_NewException("milter.error", NULL, NULL);
PyDict_SetItemString(d,"error", MilterError); PyDict_SetItemString(d,"error", MilterError);
PyDict_SetItemString(d,"SUCCESS", PyInt_FromLong((long) MI_SUCCESS)); setitem(d,"SUCCESS", MI_SUCCESS);
PyDict_SetItemString(d,"FAILURE", PyInt_FromLong((long) MI_FAILURE)); setitem(d,"FAILURE", MI_FAILURE);
PyDict_SetItemString(d,"VERSION", PyInt_FromLong((long) SMFI_VERSION)); setitem(d,"VERSION", SMFI_VERSION);
PyDict_SetItemString(d,"ADDHDRS", PyInt_FromLong((long) SMFIF_ADDHDRS)); setitem(d,"ADDHDRS", SMFIF_ADDHDRS);
PyDict_SetItemString(d,"CHGBODY", PyInt_FromLong((long) SMFIF_CHGBODY)); setitem(d,"CHGBODY", SMFIF_CHGBODY);
PyDict_SetItemString(d,"MODBODY", PyInt_FromLong((long) SMFIF_MODBODY)); setitem(d,"MODBODY", SMFIF_MODBODY);
PyDict_SetItemString(d,"ADDRCPT", PyInt_FromLong((long) SMFIF_ADDRCPT)); setitem(d,"ADDRCPT", SMFIF_ADDRCPT);
PyDict_SetItemString(d,"DELRCPT", PyInt_FromLong((long) SMFIF_DELRCPT)); #ifdef SMFIF_ADDRCPT_PAR
PyDict_SetItemString(d,"CHGHDRS", PyInt_FromLong((long) SMFIF_CHGHDRS)); setitem(d,"ADDRCPT_PAR", SMFIF_ADDRCPT_PAR);
PyDict_SetItemString(d,"V1_ACTS", PyInt_FromLong((long) SMFI_V1_ACTS));
PyDict_SetItemString(d,"V2_ACTS", PyInt_FromLong((long) SMFI_V2_ACTS));
PyDict_SetItemString(d,"CURR_ACTS", PyInt_FromLong((long) SMFI_CURR_ACTS));
#ifdef SMFIF_QUARANTINE
PyDict_SetItemString(d,"QUARANTINE",PyInt_FromLong((long)SMFIF_QUARANTINE));
#endif #endif
PyDict_SetItemString(d,"CONTINUE", PyInt_FromLong((long) SMFIS_CONTINUE)); setitem(d,"DELRCPT", SMFIF_DELRCPT);
PyDict_SetItemString(d,"REJECT", PyInt_FromLong((long) SMFIS_REJECT)); setitem(d,"CHGHDRS", SMFIF_CHGHDRS);
PyDict_SetItemString(d,"DISCARD", PyInt_FromLong((long) SMFIS_DISCARD)); setitem(d,"V1_ACTS", SMFI_V1_ACTS);
PyDict_SetItemString(d,"ACCEPT", PyInt_FromLong((long) SMFIS_ACCEPT)); setitem(d,"V2_ACTS", SMFI_V2_ACTS);
PyDict_SetItemString(d,"TEMPFAIL", PyInt_FromLong((long) SMFIS_TEMPFAIL)); setitem(d,"CURR_ACTS", SMFI_CURR_ACTS);
#ifdef SMFIF_QUARANTINE
setitem(d,"QUARANTINE",SMFIF_QUARANTINE);
#endif
#ifdef SMFIF_CHGFROM
setitem(d,"CHGFROM",SMFIF_CHGFROM);
#endif
setitem(d,"CONTINUE", SMFIS_CONTINUE);
setitem(d,"REJECT", SMFIS_REJECT);
setitem(d,"DISCARD", SMFIS_DISCARD);
setitem(d,"ACCEPT", SMFIS_ACCEPT);
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
} }
+193 -277
View File
@@ -1,4 +1,50 @@
# $Log$ # $Log$
# Revision 1.4 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
# Revision 1.3 2005/06/02 15:00:17 customdesigned
# Configure banned extensions. Scan zipfile option with test case.
#
# Revision 1.2 2005/06/02 04:18:55 customdesigned
# Update copyright notices after reading article on /.
#
# Revision 1.1.1.4 2005/05/31 18:23:49 customdesigned
# Development changes since 0.7.2
#
# Revision 1.62 2005/02/14 22:31:17 stuart
# _parseparam replacement not needed for python2.4
#
# Revision 1.61 2005/02/12 02:11:11 stuart
# Pass unit tests with python2.4.
#
# Revision 1.60 2005/02/11 18:34:14 stuart
# Handle garbage after quote in boundary.
#
# Revision 1.59 2005/02/10 01:10:59 stuart
# Fixed MimeMessage.ismodified()
#
# Revision 1.58 2005/02/10 00:56:49 stuart
# Runs with python2.4. Defang not working correctly - more work needed.
#
# Revision 1.57 2004/11/20 16:37:52 stuart
# fix regex for splitting header and body
#
# Revision 1.56 2004/11/09 20:33:51 stuart
# Recognize more dynamic PTR variations.
#
# Revision 1.55 2004/10/06 21:39:20 stuart
# Handle message attachments with boundary errors by not parsing them
# until needed.
#
# Revision 1.54 2004/08/18 01:59:46 stuart
# Handle mislabeled multipart messages
#
# Revision 1.53 2004/04/24 22:53:20 stuart
# Rename some local variables to avoid shadowing builtins
#
# Revision 1.52 2004/04/24 22:47:13 stuart
# Convert header values to str
#
# Revision 1.51 2004/03/25 03:19:10 stuart # Revision 1.51 2004/03/25 03:19:10 stuart
# Correctly defang rfc822 attachments when boundary specified with # Correctly defang rfc822 attachments when boundary specified with
# content-type message/rfc822. # content-type message/rfc822.
@@ -28,183 +74,61 @@
# with a warning message. # with a warning message.
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001 Business Management Systems, Inc. # Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
# This code is under GPL. See COPYING for details. # This code is under the GNU General Public License. See COPYING for details.
import StringIO import StringIO
import socket import socket
import Milter import Milter
import zipfile
import email import email
import email.Message import email.Message
from email.Message import Message from email.Message import Message
from email.Generator import Generator from email.Generator import Generator
from email.Utils import quote from email.Utils import quote
from email import Utils from email import Utils
from email.Parser import Parser
from email import Errors
from types import ListType,StringType from types import ListType,StringType
# Enhance email.Parser def zipnames(txt):
# - Fix _parsebody to decode message attachments before parsing fp = StringIO.StringIO(txt)
zipf = zipfile.ZipFile(fp,'r')
names = []
for nm in zipf.namelist():
names.append(('zipname',nm))
if nm.lower().endswith('.zip'):
names += zipnames(zipf.read(nm))
return names
from email.Parser import Parser class MimeGenerator(Generator):
try: from email.Parser import NLCRE def _dispatch(self, msg):
except: from email.Parser import nlcre as NLCRE # Get the Content-Type: for the message, then try to dispatch to
# self._handle_<maintype>_<subtype>(). If there's no handler for the
# full MIME type, then dispatch to self._handle_<maintype>(). If
# that's missing too, then dispatch to self._writeBody().
main = msg.get_content_maintype()
if msg.is_multipart() and main.lower() != 'multipart':
self._handle_multipart(msg)
else:
Generator._dispatch(self,msg)
from email import Errors def unquote(s):
class MimeParser(Parser):
# This is a copy of _parsebody from email.Parser, with a fix
# for message attachments. I couldn't find a smaller way to patch it
# in a subclass.
def _parsebody(self, container, fp, firstbodyline=None):
# Parse the body, but first split the payload on the content-type
# boundary if present.
boundary = container.get_boundary()
isdigest = (container.get_content_type() == 'multipart/digest')
# If there's a boundary, split the payload text into its constituent
# parts and parse each separately. Otherwise, just parse the rest of
# the body as a single message. Note: any exceptions raised in the
# recursive parse need to have their line numbers coerced.
if boundary:
preamble = epilogue = None
# Split into subparts. The first boundary we're looking for won't
# always have a leading newline since we're at the start of the
# body text, and there's not always a preamble before the first
# boundary.
separator = '--' + boundary
payload = fp.read()
if firstbodyline is not None:
payload = firstbodyline + '\n' + payload
# We use an RE here because boundaries can have trailing
# whitespace.
mo = re.search(
r'(?P<sep>' + re.escape(separator) + r')(?P<ws>[ \t]*)',
payload)
if not mo:
if self._strict:
raise Errors.BoundaryError(
"Couldn't find starting boundary: %s" % boundary)
container.set_payload(payload)
return
start = mo.start()
if start > 0:
# there's some pre-MIME boundary preamble
preamble = payload[0:start]
# Find out what kind of line endings we're using
start += len(mo.group('sep')) + len(mo.group('ws'))
mo = NLCRE.search(payload, start)
if mo:
start += len(mo.group(0))
# We create a compiled regexp first because we need to be able to
# specify the start position, and the module function doesn't
# support this signature. :(
cre = re.compile('(?P<sep>\r\n|\r|\n)' +
re.escape(separator) + '--')
mo = cre.search(payload, start)
if mo:
terminator = mo.start()
linesep = mo.group('sep')
if mo.end() < len(payload):
# There's some post-MIME boundary epilogue
epilogue = payload[mo.end():]
elif self._strict:
raise Errors.BoundaryError(
"Couldn't find terminating boundary: %s" % boundary)
else:
# Handle the case of no trailing boundary. Check that it ends
# in a blank line. Some cases (spamspamspam) don't even have
# that!
mo = re.search('(?P<sep>\r\n|\r|\n){2}$', payload)
if not mo:
mo = re.search('(?P<sep>\r\n|\r|\n)$', payload)
if not mo:
raise Errors.BoundaryError(
'No terminating boundary and no trailing empty line')
linesep = mo.group('sep')
terminator = len(payload)
# We split the textual payload on the boundary separator, which
# includes the trailing newline. If the container is a
# multipart/digest then the subparts are by default message/rfc822
# instead of text/plain. In that case, they'll have a optional
# block of MIME headers, then an empty line followed by the
# message headers.
parts = re.split(
linesep + re.escape(separator) + r'[ \t]*' + linesep,
payload[start:terminator])
for part in parts:
if isdigest:
if part.startswith(linesep):
# There's no header block so create an empty message
# object as the container, and lop off the newline so
# we can parse the sub-subobject
msgobj = self._class()
part = part[len(linesep):]
else:
parthdrs, part = part.split(linesep+linesep, 1)
# msgobj in this case is the "message/rfc822" container
msgobj = self.parsestr(parthdrs, headersonly=1)
# while submsgobj is the message itself
msgobj.set_default_type('message/rfc822')
maintype = msgobj.get_content_maintype()
if maintype in ('message', 'multipart'):
submsgobj = self.parsestr(part)
msgobj.attach(submsgobj)
else:
msgobj.set_payload(part)
else:
msgobj = self.parsestr(part)
container.preamble = preamble
container.epilogue = epilogue
container.attach(msgobj)
elif container.get_main_type() == 'multipart':
# Very bad. A message is a multipart with no boundary!
raise Errors.BoundaryError(
'multipart message with no defined boundary')
elif container.get_type() == 'message/delivery-status':
# This special kind of type contains blocks of headers separated
# by a blank line. We'll represent each header block as a
# separate Message object
blocks = []
while True:
blockmsg = self._class()
self._parseheaders(blockmsg, fp)
if not len(blockmsg):
# No more header blocks left
break
blocks.append(blockmsg)
container.set_payload(blocks)
elif container.get_main_type() == 'message':
# Create a container for the payload, but watch out for there not
# being any headers left
container.set_payload(fp.read())
fp = StringIO.StringIO(container.get_payload(decode=True))
try:
msg = self.parse(fp)
except Errors.HeaderParseError:
msg = self._class()
self._parsebody(msg, fp)
container.set_payload([msg])
else:
text = fp.read()
if firstbodyline is not None:
text = firstbodyline + '\n' + text
container.set_payload(text)
def unquote(str):
"""Remove quotes from a string.""" """Remove quotes from a string."""
if len(str) > 1: if len(s) > 1:
if str.startswith('"'): if s.startswith('"'):
if str.endswith('"'): if s.endswith('"'):
str = str[1:-1] s = s[1:-1]
else: # remove garbage after trailing quote else: # remove garbage after trailing quote
try: str = str[1:str[1:].index('"')+1] try: s = s[1:s[1:].index('"')+1]
except: return str except:
return str.replace('\\\\', '\\').replace('\\"', '"') return s
if str.startswith('<') and str.endswith('>'): return s.replace('\\\\', '\\').replace('\\"', '"')
return str[1:-1] if s.startswith('<') and s.endswith('>'):
return str return s[1:-1]
return s
from types import TupleType from types import TupleType
@@ -214,27 +138,11 @@ def _unquotevalue(value):
else: else:
return unquote(value) return unquote(value)
email.Message._unquotevalue = _unquotevalue #email.Message._unquotevalue = _unquotevalue
def _parseparam(str): from email.Message import _parseparam
plist = []
while str[:1] == ';':
str = str[1:]
end = str.find(';')
while end > 0 and (str.count('"',0,end) & 1):
end = str.find(';',end + 1)
if end < 0: end = len(str)
f = str[:end]
if '=' in f:
i = f.index('=')
f = f[:i].strip().lower() + \
'=' + f[i+1:].strip()
plist.append(f.strip())
str = str[end:]
return plist
# Enhance email.Message # Enhance email.Message
# - Fix getparam to parse attributes IE style
# - Provide a headerchange event for integration with Milter # - Provide a headerchange event for integration with Milter
# Headerchange attribute can be assigned a function to be called when # Headerchange attribute can be assigned a function to be called when
# changing headers. The signature is: # changing headers. The signature is:
@@ -245,64 +153,19 @@ class MimeMessage(Message):
"""Version of email.Message.Message compatible with old mime module """Version of email.Message.Message compatible with old mime module
""" """
def __init__(self,fp=None,seekable=1): def __init__(self,fp=None,seekable=1):
Message.__init__(self)
self.headerchange = None self.headerchange = None
self.submsg = None self.submsg = None
Message.__init__(self) self.modified = False
self.fp = fp
if fp:
parser = MimeParser(MimeMessage)
self.startofheaders = fp.tell()
parser._parseheaders(self,fp)
self.startofbody = fp.tell()
parser._parsebody(self,fp)
for part in self.walk():
part.modified = False
def rewindbody(self): def get_param(self, param, failobj=None, header='content-type', unquote=True):
return self.fp.seek(self.startofbody) val = Message.get_param(self,param,failobj,header,unquote)
if val != failobj and param == 'boundary' and unquote:
# unquote boundaries an extra time, test case testDefang5
return _unquotevalue(val)
return val
# override param parsing to handle quotes getfilename = Message.get_filename
def _get_params_preserve(self,failobj=None,header='content-type'):
"Return all parameter names and values. Use parser that handles quotes."
missing = []
value = self.get(header, missing)
if value is missing:
return failobj
params = []
for p in _parseparam(';' + value):
try:
name, val = p.split('=', 1)
name = name.strip()
val = val.strip()
except ValueError:
# Must have been a bare attribute
name = p.strip()
val = ''
params.append((name, val))
params = Utils.decode_params(params)
return params
def get_filename(self, failobj=None):
"""Return the filename associated with the payload if present.
The filename is extracted from the Content-Disposition header's
`filename' parameter, and it is unquoted.
"""
missing = []
filename = self.get_param('filename', missing, 'content-disposition')
if filename is missing:
return failobj
if isinstance(filename, TupleType):
# It's an RFC 2231 encoded parameter
newvalue = _unquotevalue(filename)
if newvalue[0]:
return unicode(newvalue[2], newvalue[0])
return unicode(newvalue[2])
else:
newvalue = _unquotevalue(filename.strip())
return newvalue
getfilename = get_filename
ismultipart = Message.is_multipart ismultipart = Message.is_multipart
getheaders = Message.get_all getheaders = Message.get_all
gettype = Message.get_content_type gettype = Message.get_content_type
@@ -313,11 +176,11 @@ class MimeMessage(Message):
def getname(self): def getname(self):
return self.get_param('name') return self.get_param('name')
def getnames(self): def getnames(self,scan_zip=False):
"""Return a list of (attr,name) pairs of attributes that IE might """Return a list of (attr,name) pairs of attributes that IE might
interpret as a name - and hence decide to execute this message.""" interpret as a name - and hence decide to execute this message."""
names = [] names = []
for attr,val in self.get_params([]): for attr,val in self._get_params_preserve([],'content-type'):
if isinstance(val, TupleType): if isinstance(val, TupleType):
# It's an RFC 2231 encoded parameter # It's an RFC 2231 encoded parameter
newvalue = _unquotevalue(val) newvalue = _unquotevalue(val)
@@ -328,12 +191,19 @@ class MimeMessage(Message):
else: else:
val = _unquotevalue(val.strip()) val = _unquotevalue(val.strip())
names.append((attr,val)) names.append((attr,val))
return names + [("filename",self.get_filename())] names += [("filename",self.get_filename())]
if scan_zip:
for key,name in tuple(names): # copy by converting to tuple
if name and name.lower().endswith('.zip'):
txt = self.get_payload(decode=True)
if txt.strip():
names += zipnames(txt)
return names
def ismodified(self): def ismodified(self):
"True if this message or a subpart has been modified." "True if this message or a subpart has been modified."
if not self.is_multipart(): if not self.is_multipart():
if self.submsg: if isinstance(self.submsg,Message):
return self.submsg.ismodified() return self.submsg.ismodified()
return self.modified return self.modified
if self.modified: return True if self.modified: return True
@@ -343,16 +213,22 @@ class MimeMessage(Message):
def dump(self,file,unixfrom=False): def dump(self,file,unixfrom=False):
"Write this message (and all subparts) to a file" "Write this message (and all subparts) to a file"
g = Generator(file) g = MimeGenerator(file)
g.flatten(self,unixfrom=unixfrom) g.flatten(self,unixfrom=unixfrom)
def as_string(self, unixfrom=False):
"Return the entire formatted message as a string."
fp = StringIO.StringIO()
self.dump(fp,unixfrom=unixfrom)
return fp.getvalue()
def getencoding(self): def getencoding(self):
return self.get('content-transfer-encoding',None) return self.get('content-transfer-encoding',None)
# Decode body to stream according to transfer encoding, return encoding name # Decode body to stream according to transfer encoding, return encoding name
def decode(self,filter): def decode(self,filt):
try: try:
filter.write(self.get_payload(decode=True)) filt.write(self.get_payload(decode=True))
except: except:
pass pass
return self.getencoding() return self.getencoding()
@@ -363,7 +239,7 @@ class MimeMessage(Message):
def __setitem__(self, name, value): def __setitem__(self, name, value):
rc = Message.__setitem__(self,name,value) rc = Message.__setitem__(self,name,value)
self.modified = True self.modified = True
if self.headerchange: self.headerchange(self,name,value) if self.headerchange: self.headerchange(self,name,str(value))
return rc return rc
def __delitem__(self, name): def __delitem__(self, name):
@@ -374,7 +250,7 @@ class MimeMessage(Message):
def get_payload(self,i=None,decode=False): def get_payload(self,i=None,decode=False):
msg = self.submsg msg = self.submsg
if msg and msg.ismodified(): if isinstance(msg,Message) and msg.ismodified():
self.set_payload([msg]) self.set_payload([msg])
return Message.get_payload(self,i,decode) return Message.get_payload(self,i,decode)
@@ -388,18 +264,27 @@ class MimeMessage(Message):
self.submsg = None self.submsg = None
def get_submsg(self): def get_submsg(self):
if self.get_content_type().lower() == 'message/rfc822': t = self.get_content_type().lower()
if t == 'message/rfc822' or t.startswith('multipart/'):
if not self.submsg: if not self.submsg:
txt = self.get_payload() txt = self.get_payload()
if type(txt) == str: if type(txt) == str:
txt = self.get_payload(decode=True) txt = self.get_payload(decode=True)
parser = MimeParser(MimeMessage) self.submsg = email.message_from_string(txt,MimeMessage)
self.submsg = parser.parsestr(txt) for part in self.submsg.walk():
part.modified = False
else: else:
self.submsg = txt[0] self.submsg = txt[0]
return self.submsg return self.submsg
return None return None
def message_from_file(fp):
msg = email.message_from_file(fp,MimeMessage)
for part in msg.walk():
part.modified = False
assert not msg.ismodified()
return msg
extlist = ''.join(""" extlist = ''.join("""
ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,inf,ins,isp,js, ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,inf,ins,isp,js,
jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc, jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc,
@@ -421,19 +306,27 @@ A copy of your original message was saved as '%s:%s'.
See your administrator. See your administrator.
""" """
def check_name(msg,savname=None,ckname=check_ext): def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
"Replace attachment with a warning if its name is suspicious." "Replace attachment with a warning if its name is suspicious."
for (key,name) in msg.getnames(): try:
badname = ckname(name) for key,name in msg.getnames(scan_zip):
if badname: badname = ckname(name)
hostname = socket.gethostname() if badname:
msg.set_payload(virus_msg % (badname,hostname,savname)) if key == 'zipname':
del msg["content-type"] badname = msg.get_filename()
del msg["content-disposition"] break
del msg["content-transfer-encoding"] else:
name = "WARNING.TXT" return Milter.CONTINUE
msg["Content-Type"] = "text/plain; name="+name except zipfile.BadZipfile:
break # a ZIP that is not a zip is very suspicious
badname = msg.get_filename()
hostname = socket.gethostname()
msg.set_payload(virus_msg % (badname,hostname,savname))
del msg["content-type"]
del msg["content-disposition"]
del msg["content-transfer-encoding"]
name = "WARNING.TXT"
msg["Content-Type"] = "text/plain; name="+name
return Milter.CONTINUE return Milter.CONTINUE
import email.Iterators import email.Iterators
@@ -444,7 +337,7 @@ msg MimeMessage
check function(MimeMessage): int check function(MimeMessage): int
Return CONTINUE, REJECT, ACCEPT Return CONTINUE, REJECT, ACCEPT
""" """
if msg.ismultipart() and not msg.get_content_type() == 'message/rfc822': if msg.is_multipart():
for i in msg.get_payload(): for i in msg.get_payload():
rc = check_attachments(i,check) rc = check_attachments(i,check)
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
@@ -453,28 +346,35 @@ check function(MimeMessage): int
# save call context for Python without nested_scopes # save call context for Python without nested_scopes
class _defang: class _defang:
def __init__(self,savname,check):
self._savname = savname def __init__(self,scan_html=True):
self._check = check self.scan_html = scan_html
self.scan_rfc822 = True
self.scan_html = True
def _chk_name(self,msg): def _chk_name(self,msg):
rc = check_name(msg,self._savname,self._check) rc = check_name(msg,self._savname,self._check,self.scan_zip)
if self.scan_html: if self.scan_html:
check_html(msg,self._savname) # remove scripts from HTML check_html(msg,self._savname) # remove scripts from HTML
if self.scan_rfc822: if self.scan_rfc822:
msg = msg.get_submsg() msg = msg.get_submsg()
if msg: return check_attachments(msg,self._chk_name) if isinstance(msg,Message):
return check_attachments(msg,self._chk_name)
return rc return rc
def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True,
scan_zip=False):
"""Compatible entry point.
Replace all attachments with dangerous names."""
self._savname = savname
self._check = check
self.scan_rfc822 = scan_rfc822
self.scan_zip = scan_zip
check_attachments(msg,self._chk_name)
if msg.ismodified():
return True
return False
# emulate old defang function # emulate old defang function
def defang(msg,savname=None,check=check_ext): defang = _defang()
"""Compatible entry point.
Replace all attachments with dangerous names."""
check_attachments(msg,_defang(savname,check)._chk_name)
if msg.ismodified():
return 1;
return 0
import sgmllib import sgmllib
@@ -571,7 +471,6 @@ class HTMLScriptFilter(SGMLFilter):
def handle_comment(self,comment): def handle_comment(self,comment):
if not self.ignoring: SGMLFilter.handle_comment(self,comment) if not self.ignoring: SGMLFilter.handle_comment(self,comment)
def check_html(msg,savname=None): def check_html(msg,savname=None):
"Remove scripts from HTML attachments." "Remove scripts from HTML attachments."
msgtype = msg.get_content_type().lower() msgtype = msg.get_content_type().lower()
@@ -582,14 +481,14 @@ def check_html(msg,savname=None):
msgtype = 'text/html' msgtype = 'text/html'
if msgtype == 'text/html': if msgtype == 'text/html':
out = StringIO.StringIO() out = StringIO.StringIO()
filter = HTMLScriptFilter(out) htmlfilter = HTMLScriptFilter(out)
try: try:
filter.write(msg.get_payload(decode=True)) htmlfilter.write(msg.get_payload(decode=True))
filter.close() htmlfilter.close()
#except sgmllib.SGMLParseError: #except sgmllib.SGMLParseError:
except: except:
#mimetools.copyliteral(msg.get_payload(),open('debug.out','w') #mimetools.copyliteral(msg.get_payload(),open('debug.out','w')
filter.close() htmlfilter.close()
hostname = socket.gethostname() hostname = socket.gethostname()
msg.set_payload( msg.set_payload(
"An HTML attachment could not be parsed. The original is saved as '%s:%s'" "An HTML attachment could not be parsed. The original is saved as '%s:%s'"
@@ -600,8 +499,25 @@ def check_html(msg,savname=None):
name = "WARNING.TXT" name = "WARNING.TXT"
msg["Content-Type"] = "text/plain; name="+name msg["Content-Type"] = "text/plain; name="+name
return Milter.CONTINUE return Milter.CONTINUE
if filter.modified: if htmlfilter.modified:
msg.set_payload(out) # remove embedded scripts msg.set_payload(out) # remove embedded scripts
del msg["content-transfer-encoding"] del msg["content-transfer-encoding"]
email.Encoders.encode_quopri(msg) email.Encoders.encode_quopri(msg)
return Milter.CONTINUE return Milter.CONTINUE
if __name__ == '__main__':
import sys
def _list_attach(msg):
t = msg.get_content_type()
p = msg.get_payload(decode=True)
print msg.get_filename(),msg.get_content_type(),type(p)
msg = msg.get_submsg()
if isinstance(msg,Message):
return check_attachments(msg,_list_attach)
return Milter.CONTINUE
for fname in sys.argv[1:]:
fp = open(fname)
msg = message_from_file(fp)
email.Iterators._structure(msg)
check_attachments(msg,_list_attach)
+125
View File
@@ -0,0 +1,125 @@
# EL 3,4,5 supported, set to 0 for Fedora
%if 0%{?el3} || 0%{?el4}
%define __python python2.4
%endif
%define libdir %{_libdir}/pymilter
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
%define pythonbase %(basename %{__python})
Summary: Python interface to sendmail milter API
Name: pymilter
Version: 0.9.1
Release: 1%{dist}
Source: http://downloads.sourceforge.net/pymilter/%{name}-%{version}.tar.gz
Patch: %{name}-smutil.patch
Patch1: %{name}-start.patch
License: GPLv2+
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html
Requires: %{pythonbase} >= 2.4, sendmail >= 8.13
%if 0%{?el3} || 0%{?el4}
# Need python2.4 specific pydns, not the version for system python
Requires: pydns
%else
# Needed for callbacks, not a core function but highly useful for milters
Requires: python-pydns
%endif
BuildRequires: ed, %{pythonbase}-devel >= 2.4, sendmail-devel >= 8.13
%description
This is a python extension module to enable python scripts to
attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV.
%prep
%setup -q
%patch -p0 -b .smutil
%patch1 -p0 -b .start
%build
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
%install
rm -rf $RPM_BUILD_ROOT
%{__python} setup.py install --root=$RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
mkdir -p $RPM_BUILD_ROOT%{libdir}
cp start.sh $RPM_BUILD_ROOT%{libdir}
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^datadir=/
c
datadir="%{_localstatedir}/log/milter"
.
/^piddir=/
c
piddir="%{_localstatedir}/run/milter"
.
/^libdir=/
c
libdir="%{libdir}"
.
/^python=/
c
python="%{__python}"
.
w
q
EOF
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
# start.sh is used by spfmilter and milter, and could be used by
# other milters using pymilter.
%files
%defattr(-,root,root,-)
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%{python_sitearch}/*
%{libdir}
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%clean
rm -rf $RPM_BUILD_ROOT
%changelog
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-4
- Stop using INSTALLED_FILES to make Fedora happy
- Remove config flag from start.sh glue
- Own /var/log/milter
- Use _localstatedir
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-2
- Changes to meet Fedora standards
* Mon Nov 24 2008 Stuart Gathman <stuart@bmsi.com> 0.9.0-1
- Split pymilter into its own CVS module
- Support chgfrom and addrcpt_par
- Support NS records in Milter.dns
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
- /var/run/milter directory must be owned by mail
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
- improved parsing into email and fullname (still 2 self test failures)
- implement no-DSN CBV, reduce full DSNs
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
- Use ifarch hack to build milter and milter-spf packages as noarch
- Remove spf dependency from dsn.py, add dns.py
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
- move AddrCache, parse_addr, iniplist to Milter package
- move parse_header to Milter.utils
- fix plock for missing source and can't change owner/group
- split out pymilter and pymilter-spf packages
- move milter apps to /usr/lib/pymilter
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
- SPF moved to pyspf RPM
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
- Support CBV timeout
+1 -1
View File
@@ -126,7 +126,7 @@ class sampleMilter(Milter.Milter):
def eom(self): def eom(self):
if not self.fp: return Milter.ACCEPT if not self.fp: return Milter.ACCEPT
self.fp.seek(0) self.fp.seek(0)
msg = mime.MimeMessage(self.fp) msg = mime.message_from_file(self.fp)
msg.headerchange = self._headerChange msg.headerchange = self._headerChange
if not mime.defang(msg,self.tempname): if not mime.defang(msg,self.tempname):
os.remove(self.tempname) os.remove(self.tempname)
+2 -1
View File
@@ -1,4 +1,5 @@
[bdist_rpm] [bdist_rpm]
python=python2 python=python2.4
doc_files=README NEWS TODO doc_files=README NEWS TODO
packager=Stuart D. Gathman <stuart@bmsi.com> packager=Stuart D. Gathman <stuart@bmsi.com>
release=1
+19 -7
View File
@@ -2,8 +2,12 @@ import os
import sys import sys
from distutils.core import setup, Extension from distutils.core import setup, Extension
# FIXME: on some versions of sendmail, smutil is renamed to sm # FIXME: on some versions of sendmail, smutil is renamed to sm.
libs = ["milter", "smutil"] # On slackware and debian, leave it out entirely. It depends
# on how libmilter was built by the sendmail package.
#libs = ["milter", "smutil"]
libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian
# patch distutils if it can't cope with the "classifiers" or # patch distutils if it can't cope with the "classifiers" or
# "download_url" keywords # "download_url" keywords
@@ -12,13 +16,14 @@ if sys.version < '2.2.3':
DistributionMetadata.classifiers = None DistributionMetadata.classifiers = None
DistributionMetadata.download_url = None DistributionMetadata.download_url = None
setup(name = "milter", version = "0.6.9", # NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "pymilter", version = '0.9.1',
description="Python interface to sendmail milter API", description="Python interface to sendmail milter API",
long_description="""\ long_description="""\
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
attach to sendmail's libmilter functionality. Additional python attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, and modules provide for navigating and modifying MIME parts, and
querying SPF records. sending DSNs or doing CBVs.
""", """,
author="Jim Niemira", author="Jim Niemira",
author_email="urmane@urmane.org", author_email="urmane@urmane.org",
@@ -26,9 +31,15 @@ querying SPF records.
maintainer_email="stuart@bmsi.com", maintainer_email="stuart@bmsi.com",
license="GPL", license="GPL",
url="http://www.bmsi.com/python/milter.html", url="http://www.bmsi.com/python/milter.html",
py_modules=["Milter","mime","spf"], py_modules=["mime"],
packages = ['Milter'],
ext_modules=[ ext_modules=[
Extension("milter", ["miltermodule.c"],libraries=libs), Extension("milter", ["miltermodule.c"],
library_dirs=libdirs,
libraries=libs,
# set MAX_ML_REPLY to 1 for sendmail < 8.13
define_macros = [ ('MAX_ML_REPLY',32) ]
),
], ],
keywords = ['sendmail','milter'], keywords = ['sendmail','milter'],
classifiers = [ classifiers = [
@@ -39,6 +50,7 @@ querying SPF records.
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python', 'Programming Language :: Python',
'Topic :: Communications :: Email :: Mail Transport Agents' 'Topic :: Communications :: Email :: Mail Transport Agents',
'Topic :: Communications :: Email :: Filters'
] ]
) )
-814
View File
@@ -1,814 +0,0 @@
#!/usr/bin/env python
"""SPF (Sender-Permitted From) implementation.
Copyright (c) 2003, Terence Way
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.
IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
For more information about SPF, a tool against email forgery, see
http://spf.pobox.com
For news, bugfixes, etc. visit the home page for this implementation at
http://www.wayforward.net/spf/
"""
# Changes:
# 9-dec-2003, v1.1, Meng Weng Wong added PTR code, THANK YOU
# 11-dec-2003, v1.2, ttw added macro expansion, exp=, and redirect=
# 13-dec-2003, v1.3, ttw added %{o} original domain macro,
# print spf result on command line, support default=,
# support localhost, follow DNS CNAMEs, cache DNS results
# during query, support Python 2.2 for Mac OS X
# 16-dec-2003, v1.4, ttw fixed include handling (include is a mechanism,
# complete with status results, so -include: should work.
# Expand macros AFTER looking for status characters ?-+
# so altavista.com SPF records work.
# 17-dec-2003, v1.5, ttw use socket.inet_aton() instead of DNS.addr2bin, so
# n, n.n, and n.n.n forms for IPv4 addresses work, and to
# ditch the annoying Python 2.4 FutureWarning
# 18-dec-2003, v1.6, Failures on Intel hardware: endianness. Use ! on
# struct.pack(), struct.unpack().
#
# Development taken over by Stuart Gathman <stuart@bmsi.com> since
# Terrence is not responding to email.
#
# $Log$
# Revision 1.10 2004/04/19 22:12:11 stuart
# Release 0.6.9
#
# Revision 1.9 2004/04/18 03:29:35 stuart
# Pass most tests except -local and -rcpt-to
#
# Revision 1.8 2004/04/17 22:17:55 stuart
# Header comment method.
#
# Revision 1.7 2004/04/17 18:22:48 stuart
# Support default explanation.
#
# Revision 1.6 2004/04/06 20:18:02 stuart
# Fix bug in include
#
# Revision 1.5 2004/04/05 22:29:46 stuart
# SPF best_guess,
#
# Revision 1.4 2004/03/25 03:27:34 stuart
# Support delegation of SPF records.
#
# Revision 1.3 2004/03/13 12:23:23 stuart
# Expanded result codes. Tolerate common method misspellings.
#
__author__ = "Terence Way"
__email__ = "terry@wayforward.net"
__version__ = "1.6: December 18, 2003"
MODULE = 'spf'
USAGE = """To check an incoming mail request:
% python spf.py {ip} {sender} {helo}
% python spf.py 69.55.226.139 tway@optsw.com mx1.wayforward.net
To test an SPF record:
% python spf.py "v=spf1..." {ip} {sender} {helo}
% python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.com a
To fetch an SPF record:
% python spf.py {domain}
% python spf.py wayforward.net
To test this script (and to output this usage message):
% python spf.py
"""
import re
import socket # for inet_ntoa() and inet_aton()
import struct # for pack() and unpack()
import time # for time()
import DNS # http://pydns.sourceforge.net
# 32-bit IPv4 address mask
MASK = 0xFFFFFFFFL
# Regular expression to look for modifiers
RE_MODIFIER = re.compile(r'^([a-zA-Z]+)=')
# Regular expression to find macro expansions
RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))')
# Regular expression to break up a macro expansion
RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)')
# Local parts and senders have their delimiters replaced with '.' during
# macro expansion
#
JOINERS = {'l': '.', 's': '.'}
RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail',
'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown',
'neutral': 'neutral', 'softfail': 'softfail',
'none': 'none', 'deny': 'fail' }
EXPLANATIONS = {'pass': 'sender SPF verified', 'fail': 'access denied',
'unknown': 'SPF unknown',
'softfail': 'domain in transition',
'neutral': 'access neither permitted nor denied',
'none': ''
}
# if set to a domain name, search _spf.domain namespace if no SPF record
# found in source domain.
DELEGATE = None
# support pre 2.2.1....
try:
bool, True, False = bool, True, False
except NameError:
False, True = 0, 1
def bool(x): return not not x
# ...pre 2.2.1
# standard default SPF record
DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr'
def check(i, s, h,local=None):
"""Test an incoming MAIL FROM:<s>, from a client with ip address i.
h is the HELO/EHLO domain name.
Returns (result, mta-status-code, explanation) where result in
['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ].
Example:
>>> check(i='127.0.0.1', s='terry@wayforward.net', h='localhost')
('pass', 250, 'local connections always pass')
#>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com')
"""
return query(i=i, s=s, h=h,local=local).check()
class query(object):
"""A query object keeps the relevant information about a single SPF
query:
i: ip address of SMTP client
s: sender declared in MAIL FROM:<>
l: local part of sender s
d: current domain, initially domain part of sender s
h: EHLO/HELO domain
v: 'in-addr' for IPv4 clients and 'ip6' for IPv6 clients
t: current timestamp
p: SMTP client domain name
o: domain part of sender s
This is also, by design, the same variables used in SPF macro
expansion.
Also keeps cache: DNS cache.
"""
def __init__(self, i, s, h,local=None):
self.i, self.s, self.h = i, s, h
self.l, self.o = split_email(s, h)
self.t = str(int(time.time()))
self.v = 'in-addr'
self.d = self.o
self.p = None
self.cache = {}
self.exps = dict(EXPLANATIONS)
self.local = local # local policy
def set_default_explanation(self,exp):
exps = self.exps
for i in 'softfail','fail','unknown':
exps[i] = exp
def getp(self):
if not self.p:
p = self.dns_ptr(self.i)
if len(p) > 0:
self.p = p[0]
else:
self.p = self.i
return self.p
def best_guess(self,spf=DEFAULT_SPF):
"""Return a best guess based on a default SPF record"""
return self.check(spf)
def check(self, spf=None):
"""
Returns (result, mta-status-code, explanation) where
result in ['fail', 'softfail', 'neutral' 'unknown', 'pass', 'error']
"""
if self.i.startswith('127.'):
return ('pass', 250, 'local connections always pass')
try:
if not spf:
spf = self.dns_spf(self.d)
if self.local and spf:
spf += ' ' + self.local
return self.check1(spf, self.d, 0)
except DNS.DNSError:
return ('error', 450, 'SPF DNS Error')
def check1(self, spf, domain, recursion):
# spf rfc: 3.7 Processing Limits
#
if recursion > 20:
self.prob = 'Mechanisms used too many DNS lookups'
return ('unknown', 250, 'SPF recursion limit exceeded')
try:
tmp, self.d = self.d, domain
return self.check0(spf, recursion)
finally:
self.d = tmp
def check0(self, spf, recursion):
"""Test this query information against SPF text.
Returns (result, mta-status-code, explanation) where
result in ['fail', 'unknown', 'pass', 'none']
"""
if not spf:
return ('none', 250, EXPLANATIONS['none'])
# split string by whitespace, drop the 'v=spf1'
#
spf = spf.split()[1:]
# copy of explanations to be modified by exp=
exps = self.exps
redirect = None
# no mechanisms at all cause unknown result, unless
# overridden with 'default=' modifier
#
default = 'neutral'
self.mech = [] # unknown mechanisms
# Look for modifiers
#
for m in spf:
m = RE_MODIFIER.split(m)[1:]
if len(m) != 2: continue
if m[0] == 'exp':
exps['fail'] = exps['unknown'] = \
self.get_explanation(m[1])
elif m[0] == 'redirect':
redirect = self.expand(m[1])
elif m[0] == 'default':
# default=- is the same as default=fail
default = RESULTS.get(m[1], default)
# spf rfc: 3.6 Unrecognized Mechanisms and Modifiers
# Look for mechanisms
#
for mech in spf:
if RE_MODIFIER.match(mech): continue
m, arg, cidrlength = parse_mechanism(mech, self.d)
# map '?' '+' or '-' to 'unknown' 'pass' or 'fail'
result = RESULTS.get(m[0])
if result:
# eat '?' '+' or '-'
m = m[1:]
else:
# default pass
result = 'pass'
if m in ['a', 'mx', 'ptr', 'exists', 'include']:
arg = self.expand(arg)
if m == 'include':
if arg != self.d:
res,code,txt = self.check1(self.dns_spf(arg),
arg, recursion + 1)
if res == 'pass':
break
if res in ('fail','neutral','softfail'):
continue
if res == 'none':
self.prob = \
'Could not find a valid SPF record'
res = 'unknown'
return res,code,txt
else:
self.prob = 'Required option is missing'
self.mech.append(mech)
return ('unknown', 250, 'missing SPF option')
elif m == 'all':
break
elif m == 'exists':
if len(self.dns_a(arg)) > 0:
break
elif m == 'a':
if cidrmatch(self.i, self.dns_a(arg),
cidrlength):
break
elif m == 'mx':
if cidrmatch(self.i, self.dns_mx(arg),
cidrlength):
break
elif m in ('ip4', 'ipv4') and arg != self.d:
if cidrmatch(self.i, [arg], cidrlength):
break
elif m in ('ptr', 'prt'):
if domainmatch(self.validated_ptrs(self.i),
arg):
break
else:
# unknown mechanisms cause immediate unknown
# abort results
self.mech.append(mech)
self.prob = 'Unknown mechanism found'
return ('unknown',250,'unknown SPF mechanism')
else:
# no matches
if redirect:
return self.check1(self.dns_spf(redirect),
redirect, recursion+1)
else:
result = default
if result == 'fail':
return (result, 550, exps[result])
else:
return (result, 250, exps[result])
def get_explanation(self, spec):
"""Expand an explanation."""
if spec:
return self.expand(''.join(self.dns_txt(self.expand(spec))))
else:
return 'explanation : Required option is missing'
def expand(self, str):
"""Do SPF RFC macro expansion.
Examples:
>>> q = query(s='strong-bad@email.example.com',
... h='mx.example.org', i='192.0.2.3')
>>> q.p = 'mx.example.org'
>>> q.expand('%{d}')
'email.example.com'
>>> q.expand('%{d4}')
'email.example.com'
>>> q.expand('%{d3}')
'email.example.com'
>>> q.expand('%{d2}')
'example.com'
>>> q.expand('%{d1}')
'com'
>>> q.expand('%{p}')
'mx.example.org'
>>> q.expand('%{p2}')
'example.org'
>>> q.expand('%{dr}')
'com.example.email'
>>> q.expand('%{d2r}')
'example.email'
>>> q.expand('%{l}')
'strong-bad'
>>> q.expand('%{l-}')
'strong.bad'
>>> q.expand('%{lr}')
'strong-bad'
>>> q.expand('%{lr-}')
'bad.strong'
>>> q.expand('%{l1r-}')
'strong'
>>> q.expand('%{ir}.%{v}._spf.%{d2}')
'3.2.0.192.in-addr._spf.example.com'
>>> q.expand('%{lr-}.lp._spf.%{d2}')
'bad.strong.lp._spf.example.com'
>>> q.expand('%{lr-}.lp.%{ir}.%{v}._spf.%{d2}')
'bad.strong.lp.3.2.0.192.in-addr._spf.example.com'
>>> q.expand('%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}')
'3.2.0.192.in-addr.strong.lp._spf.example.com'
>>> q.expand('%{p2}.trusted-domains.example.net')
'example.org.trusted-domains.example.net'
>>> q.expand('%{p2}.trusted-domains.example.net')
'example.org.trusted-domains.example.net'
"""
end = 0
result = ''
for i in RE_CHAR.finditer(str):
result += str[end:i.start()]
macro = str[i.start():i.end()]
if macro == '%%':
result += '%'
elif macro == '%_':
result += ' '
elif macro == '%-':
result += '%20'
else:
letter = macro[2].lower()
if letter == 'p':
self.getp()
expansion = getattr(self, letter, '')
if expansion:
result += expand_one(expansion,
macro[3:-1],
JOINERS.get(letter))
end = i.end()
return result + str[end:]
def dns_spf(self, domain):
"""Get the SPF record recorded in DNS for a specific domain
name. Returns None if not found, or if more than one record
is found.
"""
a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')]
if not a and DELEGATE:
a = [t
for t in self.dns_txt(domain+'._spf.'+DELEGATE)
if t.startswith('v=spf1')
]
if len(a) == 1:
return a[0]
else:
return None
def dns_txt(self, domainname):
if domainname:
return [t for a in self.dns(domainname, 'TXT') for t in a]
return []
def dns_mx(self, domainname):
"""Get a list of IP addresses for all MX exchanges for a
domain name.
"""
return [a for mx in self.dns(domainname, 'MX') \
for a in self.dns_a(mx[1])]
def dns_a(self, domainname):
"""Get a list of IP addresses for a domainname."""
return self.dns(domainname, 'A')
def dns_aaaa(self, domainname):
"""Get a list of IPv6 addresses for a domainname."""
return self.dns(domainname, 'AAAA')
def validated_ptrs(self, i):
"""Figure out the validated PTR domain names for a given IP
address.
"""
return [p for p in self.dns_ptr(i) if i in self.dns_a(p)]
def dns_ptr(self, i):
"""Get a list of domain names for an IP address."""
return self.dns(reverse_dots(i) + ".in-addr.arpa", 'PTR')
def dns(self, name, qtype):
"""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)
"""
result = self.cache.get( (name, qtype) )
cname = None
if not result:
req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req()
for a in resp.answers:
# key k: ('wayforward.net', 'A'), value v
k, v = (a['name'], a['typename']), a['data']
if k == (name, 'CNAME'):
cname = v
self.cache.setdefault(k, []).append(v)
result = self.cache.get( (name, qtype), [])
if not result and cname:
result = self.dns(cname, qtype)
return result
def get_header(self,res,receiver):
if res in ('pass','fail'):
return '%s (%s: %s) client-ip=%s; envelope-from=%s; helo=%s;' % (
res,receiver,self.get_header_comment(res),self.i,
self.l + '@' + self.o, self.h)
if res == 'unknown':
return '%s (%s: %s)' % (' '.join([res] + self.mech),
receiver,self.get_header_comment(res))
return '%s (%s: %s)' % (res,receiver,self.get_header_comment(res))
def get_header_comment(self,res):
"""Return comment for Received-SPF header.
"""
sender = self.o
if res == 'pass':
if self.i.startswith('127.'):
return "localhost is always allowed."
else: return \
"domain of %s designates %s as permitted sender" \
% (sender,self.i)
elif res == 'softfail': return \
"transitioning domain of %s does not designate %s as permitted sender" \
% (sender,self.i)
elif res == 'neutral': return \
"%s is neither permitted nor denied by domain of %s" \
% (self.i,sender)
elif res == 'none': return \
"%s is neither permitted nor denied by domain of %s" \
% (self.i,sender)
#"%s does not designate permitted sender hosts" % sender
elif res == 'unknown': return \
"error in processing during lookup of domain of %s: %s" \
% (sender, self.prob)
elif res == 'error': return \
"error in processing during lookup of %s" % sender
elif res == 'fail': return \
"domain of %s does not designate %s as permitted sender" \
% (sender,self.i)
raise ValueError("invalid SPF result for header comment: "+res)
def split_email(s, h):
"""Given a sender email s and a HELO domain h, create a valid tuple
(l, d) local-part and domain-part.
Examples:
>>> split_email('', 'wayforward.net')
('postmaster', 'wayforward.net')
>>> split_email('foo.com', 'wayforward.net')
('postmaster', 'foo.com')
>>> split_email('terry@wayforward.net', 'optsw.com')
('terry', 'wayforward.net')
"""
if not s:
return 'postmaster', h
else:
parts = s.split('@', 1)
if len(parts) == 2:
return tuple(parts)
else:
return 'postmaster', s
def parse_mechanism(str, d):
"""Breaks A, MX, IP4, and PTR mechanisms into a (name, domain,
cidr) tuple. The domain portion defaults to d if not present,
the cidr defaults to 32 if not present.
Examples:
>>> parse_mechanism('a', 'foo.com')
('a', 'foo.com', 32)
>>> parse_mechanism('a:bar.com', 'foo.com')
('a', 'bar.com', 32)
>>> parse_mechanism('a/24', 'foo.com')
('a', 'foo.com', 24)
>>> parse_mechanism('a:bar.com/16', 'foo.com')
('a', 'bar.com', 16)
"""
a = str.split('/')
if len(a) == 2:
a, port = a[0], int(a[1])
else:
a, port = str, 32
b = a.split(':')
if len(b) == 2:
return b[0], b[1], port
else:
return a, d, port
def reverse_dots(name):
"""Reverse dotted IP addresses or domain names.
Example:
>>> reverse_dots('192.168.0.145')
'145.0.168.192'
>>> reverse_dots('email.example.com')
'com.example.email'
"""
a = name.split('.')
a.reverse()
return '.'.join(a)
def domainmatch(ptrs, domainsuffix):
"""grep for a given domain suffix against a list of validated PTR
domain names.
Examples:
>>> domainmatch(['FOO.COM'], 'foo.com')
1
>>> domainmatch(['moo.foo.com'], 'FOO.COM')
1
>>> domainmatch(['moo.bar.com'], 'foo.com')
0
"""
domainsuffix = domainsuffix.lower()
for ptr in ptrs:
ptr = ptr.lower()
if ptr == domainsuffix or ptr.endswith('.' + domainsuffix):
return True
return False
def cidrmatch(i, ipaddrs, cidr_length = 32):
"""Match an IP address against a list of other IP addresses.
Examples:
>>> cidrmatch('192.168.0.45', ['192.168.0.44', '192.168.0.45'])
1
>>> cidrmatch('192.168.0.43', ['192.168.0.44', '192.168.0.45'])
0
>>> cidrmatch('192.168.0.43', ['192.168.0.44', '192.168.0.45'], 24)
1
"""
c = cidr(i, cidr_length)
for ip in ipaddrs:
if cidr(ip, cidr_length) == c:
return True
return False
def cidr(i, n):
"""Convert an IP address string with a CIDR mask into a 32-bit
integer.
i must be a string of numbers 0..255 separated by dots '.'::
pre: forall([0 <= int(p) < 256 for p in i.split('.')])
n is a number of bits to mask::
pre: 0 <= n <= 32
Examples:
>>> bin2addr(cidr('192.168.5.45', 32))
'192.168.5.45'
>>> bin2addr(cidr('192.168.5.45', 24))
'192.168.5.0'
>>> bin2addr(cidr('192.168.0.45', 8))
'192.0.0.0'
"""
return ~(MASK >> n) & MASK & addr2bin(i)
def addr2bin(str):
"""Convert a string IPv4 address into an unsigned integer.
Examples::
>>> addr2bin('127.0.0.1')
2130706433L
>>> addr2bin('127.0.0.1') == socket.INADDR_LOOPBACK
1
>>> addr2bin('255.255.255.254')
4294967294L
>>> addr2bin('192.168.0.1')
3232235521L
Unlike DNS.addr2bin, the n, n.n, and n.n.n forms for IP addresses
are handled as well::
>>> addr2bin('10.65536')
167837696L
>>> 10 * (2 ** 24) + 65536
167837696
>>> addr2bin('10.93.512')
173867520L
>>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512
173867520
"""
return struct.unpack("!L", socket.inet_aton(str))[0]
def bin2addr(addr):
"""Convert a numeric IPv4 address into string n.n.n.n form.
Examples::
>>> bin2addr(socket.INADDR_LOOPBACK)
'127.0.0.1'
>>> bin2addr(socket.INADDR_ANY)
'0.0.0.0'
>>> bin2addr(socket.INADDR_NONE)
'255.255.255.255'
"""
return socket.inet_ntoa(struct.pack("!L", addr))
def expand_one(expansion, str, joiner):
if not str:
return expansion
len, reverse, delimiters = RE_ARGS.split(str)[1:4]
if not delimiters:
delimiters = '.'
expansion = split(expansion, delimiters, joiner)
if reverse: expansion.reverse()
if len: expansion = expansion[-int(len)*2+1:]
return ''.join(expansion)
def split(str, delimiters, joiner=None):
"""Split a string into pieces by a set of delimiter characters. The
resulting list is delimited by joiner, or the original delimiter if
joiner is not specified.
Examples:
>>> split('192.168.0.45', '.')
['192', '.', '168', '.', '0', '.', '45']
>>> split('terry@wayforward.net', '@.')
['terry', '@', 'wayforward', '.', 'net']
>>> split('terry@wayforward.net', '@.', '.')
['terry', '.', 'wayforward', '.', 'net']
"""
result, element = [], ''
for c in str:
if c in delimiters:
result.append(element)
element = ''
if joiner:
result.append(joiner)
else:
result.append(c)
else:
element += c
result.append(element)
return result
def _test():
import doctest, spf
return doctest.testmod(spf)
DNS.DiscoverNameServers() # Fails on Mac OS X? Add domain to /etc/resolv.conf
if __name__ == '__main__':
import sys
if len(sys.argv) == 1:
print USAGE
_test()
elif len(sys.argv) == 2:
q = query(i='127.0.0.1', s='localhost', h='unknown')
print q.dns_spf(sys.argv[1])
elif len(sys.argv) == 4:
print check(i=sys.argv[1], s=sys.argv[2], h=sys.argv[3])
elif len(sys.argv) == 5:
i, s, h = sys.argv[2:]
q = query(i=i, s=s, h=h)
print q.check(sys.argv[1])
else:
print USAGE
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/python2.3
# $Log$
# Revision 2.3 2004/04/19 22:12:11 stuart
# Release 0.6.9
#
# Revision 2.2 2004/04/18 03:29:35 stuart
# Pass most tests except -local and -rcpt-to
#
# Revision 2.1 2004/04/08 18:41:15 stuart
# Reject numeric hello names
#
# Driver for SPF test system
import spf
import sys
from optparse import OptionParser
class PerlOptionParser(OptionParser):
def _process_args (self, largs, rargs, values):
"""_process_args(largs : [string],
rargs : [string],
values : Values)
Process command-line arguments and populate 'values', consuming
options and arguments from 'rargs'. If 'allow_interspersed_args' is
false, stop at the first non-option argument. If true, accumulate any
interspersed non-option arguments in 'largs'.
"""
while rargs:
arg = rargs[0]
# We handle bare "--" explicitly, and bare "-" is handled by the
# standard arg handler since the short arg case ensures that the
# len of the opt string is greater than 1.
if arg == "--":
del rargs[0]
return
elif arg[0:2] == "--":
# process a single long option (possibly with value(s))
self._process_long_opt(rargs, values)
elif arg[:1] == "-" and len(arg) > 1:
# process a single perl style long option
rargs[0] = '-' + arg
self._process_long_opt(rargs, values)
elif self.allow_interspersed_args:
largs.append(arg)
del rargs[0]
else:
return
def format(q):
res,code,txt = q.check()
print res
if res in ('pass','neutral','unknown'): print
else: print txt
print 'spfquery:',q.get_header_comment(res)
print 'Received-SPF:',q.get_header(res,'spfquery')
def main(argv):
parser = PerlOptionParser()
parser.add_option("--file",dest="file")
parser.add_option("--ip",dest="ip")
parser.add_option("--sender",dest="sender")
parser.add_option("--helo",dest="hello_name")
parser.add_option("--local",dest="local_policy")
parser.add_option("--rcpt-to",dest="rcpt")
parser.add_option("--default-explanation",dest="explanation")
parser.add_option("--sanitize",type="int",dest="sanitize")
parser.add_option("--debug",type="int",dest="debug")
opts,args = parser.parse_args(argv)
if opts.ip:
q = spf.query(opts.ip,opts.sender,opts.hello_name,local=opts.local_policy)
if opts.explanation:
q.set_default_explanation(opts.explanation)
format(q)
if opts.file:
if opts.file == '0':
fp = sys.stdin
else:
fp = open(opts.file,'r')
for ln in fp:
ip,sender,helo,rcpt = ln.split(None,3)
q = spf.query(ip,sender,helo,local=opts.local_policy)
if opts.explanation:
q.set_default_explanation(opts.explanation)
format(q)
fp.close()
if __name__ == "__main__":
import sys
main(sys.argv[1:])
Executable
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
appname="$1"
script="${2:-${appname}}"
datadir="/var/log/milter"
piddir="/var/run/milter"
libdir="/usr/lib/pymilter"
python="python2.4"
exec >>${datadir}/${appname}.log 2>&1
if test -s ${datadir}/${script}.py; then
cd ${datadir} # use version in log dir if it exists for debugging
else
cd ${libdir}
fi
${python} ${script}.py &
echo $! >${piddir}/${appname}.pid
+2 -2
View File
@@ -1,14 +1,14 @@
import unittest import unittest
import testbms
import testmime import testmime
import testsample import testsample
import testutils
import os import os
def suite(): def suite():
s = unittest.TestSuite() s = unittest.TestSuite()
s.addTest(testbms.suite())
s.addTest(testmime.suite()) s.addTest(testmime.suite())
s.addTest(testsample.suite()) s.addTest(testsample.suite())
s.addTest(testutils.suite())
return s return s
if __name__ == '__main__': if __name__ == '__main__':
+128
View File
@@ -0,0 +1,128 @@
From leec@windowsshop.com Fri Sep 10 11:48:25 2004
Message-ID: <4141CDD4.7040305@windowsshop.com>
Date: Fri, 10 Sep 2004 11:52:52 -0400
From: Lee Connor <leec@windowsshop.com>
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.4) Gecko/20030624 Netscape/7.1 (ax)
X-Accept-Language: en-us, en
MIME-Version: 1.0
To: Cleo Matthews-Conley <cleom@windowsshop.com>,
Tony Collini <tonyc@windowsshop.com>,
John Higinbothom <johnh@windowsshop.com>
CC: Rich Higgins <richh@windowsshop.com>
Subject: [Fwd: [Fwd: Customer Concerns]]
Content-Type: multipart/mixed;
boundary="------------020209070802060007090105"
This is a multi-part message in MIME format.
--------------020209070802060007090105
Content-Type: text/plain; charset=us-ascii; format=flowed
Content-Transfer-Encoding: 7bit
Cleo - please review attached feedback from Sales team.......I recall at
an early meeting after we moved in you and Tony (and maybe 1 or 2
others) were going to develop a voice mail procedure or instruction
sheet for all staff. It looks like we really need this to get what we
are looking for from the system. Please let me know when you can produce
this and give a draft to the managers here for review.
Thanks,
Lee
--------------020209070802060007090105
Content-Type: message/rfc822;
name="[Fwd: Customer Concerns]"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="[Fwd: Customer Concerns]"
Return-Path: <richh@windowsshop.com>
Received: from windowsshop.com (pc147.windowsshop.com [192.168.100.147] (may be forged))
by lord.windowsshop.com (8.12.10/8.12.10) with ESMTP id i89KCClX003425
for <leec@windowsshop.com>; Thu, 9 Sep 2004 16:12:12 -0400
Message-ID: <4140B851.3020501@windowsshop.com>
Date: Thu, 09 Sep 2004 16:08:49 -0400
From: Rich <richh@windowsshop.com>
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.0.2) Gecko/20021120 Netscape/7.01
X-Accept-Language: en-us, en
MIME-Version: 1.0
To: Lee Connor <leec@windowsshop.com>
Subject: [Fwd: Customer Concerns]
Content-Type: multipart/mixed;
boundary="------------030301030706020401010801"
X-DSpam-Score: 0.000000
This is a multi-part message in MIME format.
--------------030301030706020401010801
Content-Type: text/plain; charset=us-ascii; format=flowed
Content-Transfer-Encoding: 7bit
Lee - do you want me to do anything else with this?
Rich
<!DSPAM:FEE4D3278234264874834386>
--------------030301030706020401010801
Content-Type: message/rfc822; name="Customer Concerns";
boundary="===============0045392615=="
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="Customer Concerns"
Return-Path: <joes@windowsshop.com>
Received: from joes (pc148.windowsshop.com [192.168.100.148] (may be forged))
by lord.windowsshop.com (8.12.10/8.12.10) with SMTP id i89K9BlX003262
for <richh@windowsshop.com>; Thu, 9 Sep 2004 16:09:11 -0400
From: "Joe Schmuck" <joes@windowsshop.com>
To: <richh@windowsshop.com>
Subject: Customer Concerns
Date: Thu, 9 Sep 2004 16:08:26 -0400
Message-ID: <OFEPKHCCLPIECLFBLDHBAEAECAAA.joes@windowsshop.com>
MIME-Version: 1.0
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
X-Priority: 3 (Normal)
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
Importance: Normal
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1106
X-DSpam-Score: 0.000000
Rich:
Following is a summary of concerns from customers regarding internal
communications within WS:
- Not all employees have activated their voice mail - when this is the
case, the system will automatically cut you off
- When employees are out of the office, phones are not forwarded to a back
up, ie manager
- Reception has no record of employee attendance, and therefore will
forward call to individual requested - see point 2
- Reception directs calls to incorrect individuals
- When entering voice mail, if you press '0', system does not default to
operator, but puts you back into individual voice mail
- Reception phone demeanor has no 'pep'
Thanks
Joe
---
Outgoing mail is certified Virus Free.
Checked by AVG anti-virus system (http://www.grisoft.com).
Version: 6.0.752 / Virus Database: 503 - Release Date: 9/3/2004
<!DSPAM:FEE4D05F1332634871908793>
--===============0045392615==--
--------------030301030706020401010801--
--------------020209070802060007090105--
+51
View File
@@ -0,0 +1,51 @@
From paulp@go2net.com Wed Jun 1 22:35:12 2005
Return-Path: <paulp@go2net.com>
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
Received: from 127.0.0.1 ([220.117.92.241])
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
SUBJECT: urgent
FROM: paulp@go2net.com
TO: stuart@bmsi.com
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="--------bound--"
X-DSpam-Score: 0.081200
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
Status: RO
X-Status:
X-Keywords: NonJunk
----------bound--
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Hi
Sorry, I forgot to send an important
document to you in that last email. I had an important phone call.
Please checkout attached doc file when you have a moment.
Best Regards
<!DSPAM:1043AE6B6492860536935410>
----------bound--
Content-Type: application/x-msdownload; name="zip.zip"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="zip.zip"
UEsDBAoAAAAAADVVwjLaV2nEGgAAABoAAAAzABUAemlwLmRvYyAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAuZXhlVVQJAAOmGp9CphqfQlV4BACGA2UAVGhpcyBw
cm9ncmFtIHdhcyBhIHZpcnVzLgpQSwECFwMKAAAAAAA1VcIy2ldpxBoAAAAaAAAAMwANAAAA
AAABAAAAtIEAAAAAemlwLmRvYyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAuZXhlVVQFAAOmGp9CVXgAAFBLBQYAAAAAAQABAG4AAACAAAAAAAA=
----------bound--
----------bound----
+49
View File
@@ -0,0 +1,49 @@
From paulp@go2net.com Wed Jun 1 22:35:12 2005
Return-Path: <paulp@go2net.com>
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
Received: from 127.0.0.1 ([220.117.92.241])
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
SUBJECT: urgent
FROM: paulp@go2net.com
TO: stuart@bmsi.com
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="--------bound--"
X-DSpam-Score: 0.081200
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
Status: RO
X-Status:
X-Keywords: NonJunk
----------bound--
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Hi
Sorry, I forgot to send an important
document to you in that last email. I had an important phone call.
Please checkout attached doc file when you have a moment.
Best Regards
<!DSPAM:1043AE6B6492860536935410>
----------bound--
Content-Type: application/octet-stream;
name="Readme.zip"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename="Readme.zip"
----------bound--
----------bound----
+51
View File
@@ -0,0 +1,51 @@
From paulp@go2net.com Wed Jun 1 22:35:12 2005
Return-Path: <paulp@go2net.com>
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
Received: from 127.0.0.1 ([220.117.92.241])
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
SUBJECT: urgent
FROM: paulp@go2net.com
TO: stuart@bmsi.com
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="--------bound--"
X-DSpam-Score: 0.081200
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
Status: RO
X-Status:
X-Keywords: NonJunk
----------bound--
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Hi
Sorry, I forgot to send an important
document to you in that last email. I had an important phone call.
Please checkout attached doc file when you have a moment.
Best Regards
<!DSPAM:1043AE6B6492860536935410>
----------bound--
Content-Type: application/x-msdownload; name="zip.zip"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="zip.zip"
USsDBAoBAAAAADVVwjLaV2nEGgAAABoAAAAzABUAemlwLmRvYyAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAuZXhlVVQJAAOmGp9CphqfQlV4BACGA2UAVGhpcyBw
cm9ncmFtIHdhcyBhIHZpcnVzLgpQSwECFwMKAAAAAAA1VcIy2ldpxBoAAAAaAAAAMwANAAAA
AAABAAAAtIEAAAAAemlwLmRvYyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAuZXhlVVQFAAOmGp9CVXgAAFBLBQYAAAAAAQABAG4AAACAAAAAAAA=
----------bound--
----------bound----
+47
View File
@@ -0,0 +1,47 @@
From ttaie1@thfalcon.com Thu Jun 16 10:23:13 2005
Received: from thfalcon.com (unknown [202.90.113.150])
by thfalcon.com (Postfix) with ESMTP id 32F0DD819C
for <stuart@bmsi.com>; Thu, 16 Jun 2005 15:42:08 +0700 (ICT)
From: ttaie1@thfalcon.com
To: stuart@bmsi.com
Subject: Returned mail: see transcript for details
Date: Thu, 16 Jun 2005 15:50:10 +0700
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_NextPart_000_0014_E4E04420.5619685C"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2600.0000
X-MIMEOLE: Produced By Microsoft MimeOLE V6.00.2600.0000
Message-Id: <20050616084208.32F0DD819C@thfalcon.com>
Received-SPF: pass (mail.bmsi.com: guessing: domain of thfalcon.com designates 203.147.3.44 as permitted sender) client-ip=203.147.3.44; envelope-from=ttaie1@thfalcon.com; helo=thfalcon.com;
This is a multi-part message in MIME format.
------=_NextPart_000_0014_E4E04420.5619685C
Content-Type: text/plain;
charset=us-ascii
Content-Transfer-Encoding: 7bit
Message could not be delivered
------=_NextPart_000_0014_E4E04420.5619685C
Content-Type: application/octet-stream;
name="stuart@bmsi.com.zip"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="stuart@bmsi.com.zip"
UEsDBAoAAAAAAM6r0DL7SfbCBAEAAAQBAAAFABUAdC56aXBVVAkAA7MnskK4J7JCVXgEAIYD
ZQBQSwMECgAAAAAANVXCMtpXacQaAAAAGgAAADMAFQB6aXAuZG9jICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgIC5leGVVVAkAA6Yan0KmGp9CVXgEAIYDZQBUaGlz
IHByb2dyYW0gd2FzIGEgdmlydXMuClBLAQIXAwoAAAAAADVVwjLaV2nEGgAAABoAAAAzAA0A
AAAAAAEAAAC0gQAAAAB6aXAuZG9jICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgIC5leGVVVAUAA6Yan0JVeAAAUEsFBgAAAAABAAEAbgAAAIAAAAAAAFBLAQIXAwoA
AAAAAM6r0DL7SfbCBAEAAAQBAAAFAA0AAAAAAAAAAAC0gQAAAAB0LnppcFVUBQADsyeyQlV4
AABQSwUGAAAAAAEAAQBAAAAAPAEAAAAA
------=_NextPart_000_0014_E4E04420.5619685C--
-290
View File
@@ -1,290 +0,0 @@
import unittest
import Milter
import bms
import mime
import rfc822
import StringIO
#import pdb
class TestMilter(bms.bmsMilter):
def __init__(self):
bms.bmsMilter.__init__(self)
self.logfp = open("test/milter.log","a")
self._delrcpt = [] # record deleted rcpts for testing
self._addrcpt = [] # record added rcpts for testing
def log(self,*msg):
for i in msg: print >>self.logfp, i,
print >>self.logfp
def getsymval(self,name):
if name == 'j': return 'test.milter.org'
return bms.bmsMilter.getsymval(self,name)
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self.bodyreplaced = 1
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 = 1
def addheader(self,field,value):
if not self._body:
raise IOError,"addheader not called from eom()"
self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value
self.headerschanged = 1
def delrcpt(self,rcpt):
if not self._body:
raise IOError,"delrcpt not called from eom()"
self._delrcpt.append(rcpt)
def addrcpt(self,rcpt):
if not self._body:
raise IOError,"addrcpt not called from eom()"
self._addrcpt.append(rcpt)
def setreply(self,rcode,xcode,msg):
self.reply = (rcode,xcode,msg)
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com"):
self._body = None
self.bodyreplaced = 0
self.headerschanged = 0
self.reply = None
msg = rfc822.Message(fp)
rc = self.envfrom('<%s>'%sender)
if rc != Milter.CONTINUE: return rc
rc = self.envrcpt('<%s>'%rcpt)
if rc != Milter.CONTINUE: return rc
line = None
for h in msg.headers:
if h[:1].isspace():
line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
if len(s) > 1: val = s[1].strip()
else: val = ''
rc = self.header(s[0],val)
if rc != Milter.CONTINUE: return rc
line = h
if line:
s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
while 1:
buf = fp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = StringIO.StringIO()
rc = self.eom()
if self.bodyreplaced:
body = self._body.getvalue()
else:
msg.rewindbody()
body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body)
return rc
def feedMsg(self,fname,sender="spam@adv.com",rcpt="victim@lamb.com"):
fp = open('test/'+fname,'r')
rc = self.feedFile(fp,sender,rcpt)
fp.close()
return rc
def connect(self,host='localhost'):
self._body = None
self.bodyreplaced = 0
rc = bms.bmsMilter.connect(self,host,1,('1.2.3.4',1234))
if rc != Milter.CONTINUE and rc != Milter.ACCEPT:
self.close()
return rc
rc = self.hello('spamrelay')
if rc != Milter.CONTINUE:
self.close()
return rc
class BMSMilterTestCase(unittest.TestCase):
def testDefang(self,fname='virus1'):
milter = TestMilter()
rc = milter.connect('testDefang')
self.assertEqual(rc,Milter.CONTINUE)
rc = milter.feedMsg(fname)
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
fp.seek(0)
msg = mime.MimeMessage(fp)
str = msg.get_payload(1).get_payload()
milter.log(str)
milter.close()
# test some spams that crashed our parser
def testParse(self,fname='spam7'):
milter = TestMilter()
milter.connect('testParse')
rc = milter.feedMsg(fname)
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue())
milter.connect('pro-send.com')
rc = milter.feedMsg('spam8')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg('bounce')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg('bounce1')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
milter.close()
def testDefang2(self):
milter = TestMilter()
milter.connect('testDefang2')
rc = milter.feedMsg('samp1')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg("virus3")
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body
open("test/virus3.tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
rc = milter.feedMsg("virus6")
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failUnless(milter.headerschanged,"Message headers not adjusted")
fp = milter._body
open("test/virus6.tstout","w").write(fp.getvalue())
milter.close()
def testDefang3(self):
milter = TestMilter()
milter.connect('testDefang3')
# test script removal on complex HTML attachment
rc = milter.feedMsg('amazon')
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body
open("test/amazon.tstout","w").write(fp.getvalue())
# test defanging Klez virus
rc = milter.feedMsg("virus13")
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body
open("test/virus13.tstout","w").write(fp.getvalue())
# test script removal on quoted-printable HTML attachment
# sgmllib can't handle the <![if cond]> syntax
rc = milter.feedMsg('spam44')
self.assertEqual(rc,Milter.ACCEPT)
self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/spam44.tstout","w").write(fp.getvalue())
milter.close()
def testRFC822(self):
milter = TestMilter()
milter.connect('testRFC822')
# test encoded rfc822 attachment
#pdb.set_trace()
rc = milter.feedMsg('test8')
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
#self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/test8.tstout","w").write(fp.getvalue())
rc = milter.feedMsg('virus7')
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
#self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/virus7.tstout","w").write(fp.getvalue())
def testSmartAlias(self):
milter = TestMilter()
milter.connect('testSmartAlias')
# test smart alias feature
key = ('foo@bar.com','baz@bat.com')
bms.smart_alias[key] = ['ham@eggs.com']
rc = milter.feedMsg('test8',key[0],key[1])
self.assertEqual(rc,Milter.ACCEPT)
self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failUnless(milter._delrcpt == ['<baz@bat.com>'])
self.failUnless(milter._addrcpt == ['<ham@eggs.com>'])
def testBadBoundary(self):
milter = TestMilter()
milter.connect('testBadBoundary')
# test rfc822 attachment with invalid boundaries
#pdb.set_trace()
rc = milter.feedMsg('bound')
self.assertEqual(rc,Milter.REJECT)
self.assertEqual(milter.reply[0],'554')
#self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/bound.tstout","w").write(fp.getvalue())
def testCompoundFilename(self):
milter = TestMilter()
milter.connect('testCompoundFilename')
# test rfc822 attachment with invalid boundaries
#pdb.set_trace()
rc = milter.feedMsg('test1')
self.assertEqual(rc,Milter.ACCEPT)
#self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failIf(milter.bodyreplaced,"Message body replaced")
fp = milter._body
open("test/test1.tstout","w").write(fp.getvalue())
# def testReject(self):
# "Test content based spam rejection."
# milter = TestMilter()
# milter.connect('gogo-china.com')
# rc = milter.feedMsg('big5');
# self.failUnless(rc == Milter.REJECT)
# milter.close();
def suite(): return unittest.makeSuite(BMSMilterTestCase,'test')
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
for fname in sys.argv[1:]:
milter = TestMilter()
milter.connect('main')
fp = open(fname,'r')
rc = milter.feedFile(fp)
fp = milter._body
sys.stdout.write(fp.getvalue())
else:
unittest.main()
+71 -14
View File
@@ -1,8 +1,32 @@
# $Log$
# Revision 1.3 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
# Revision 1.2 2005/06/02 15:00:17 customdesigned
# Configure banned extensions. Scan zipfile option with test case.
#
# Revision 1.1.1.2 2005/05/31 18:23:49 customdesigned
# Development changes since 0.7.2
#
# Revision 1.23 2005/02/11 18:34:14 stuart
# Handle garbage after quote in boundary.
#
# Revision 1.22 2005/02/10 01:10:59 stuart
# Fixed MimeMessage.ismodified()
#
# Revision 1.21 2005/02/10 00:56:49 stuart
# Runs with python2.4. Defang not working correctly - more work needed.
#
# Revision 1.20 2004/11/20 16:38:17 stuart
# Add rcs log
#
import unittest import unittest
import mime import mime
import socket import socket
import StringIO import StringIO
import email import email
import sys
from email import Errors
samp1_txt1 = """Dear Agent 1 samp1_txt1 = """Dear Agent 1
I hope you can read this. Whenever you write label it P.B.S kids. I hope you can read this. Whenever you write label it P.B.S kids.
@@ -24,23 +48,40 @@ class MimeTestCase(unittest.TestCase):
self.failUnless(plist[0] == 'name="Jim&amp;amp;Girlz.jpg"') self.failUnless(plist[0] == 'name="Jim&amp;amp;Girlz.jpg"')
def testParse(self,fname='samp1'): def testParse(self,fname='samp1'):
msg = mime.MimeMessage(open('test/'+fname,"r")) msg = mime.message_from_file(open('test/'+fname,"r"))
self.failUnless(msg.ismultipart()) self.failUnless(msg.ismultipart())
parts = msg.get_payload() parts = msg.get_payload()
self.failUnless(len(parts) == 2) self.failUnless(len(parts) == 2)
txt1 = parts[0].get_payload() txt1 = parts[0].get_payload()
self.failUnless(txt1.rstrip() == samp1_txt1,txt1) self.failUnless(txt1.rstrip() == samp1_txt1,txt1)
msg = mime.message_from_file(open('test/missingboundary',"r"))
# should get no exception as long as we don't try to parse
# message attachments
mime.defang(msg,scan_rfc822=False)
msg.dump(open('test/missingboundary.out','w'))
msg = mime.message_from_file(open('test/missingboundary',"r"))
try:
mime.defang(msg)
# python 2.4 doesn't get exceptions on missing boundaries, and
# if message is modified, output is readable by mail clients
if sys.hexversion < 0x02040000:
self.fail('should get boundary error parsing bad rfc822 attachment')
except Errors.BoundaryError:
pass
def testDefang(self,vname='virus1',part=1, def testDefang(self,vname='virus1',part=1,
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'): fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
msg = mime.MimeMessage(open('test/'+vname,"r")) msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg) mime.defang(msg,scan_zip=True)
self.failUnless(msg.ismodified(),"virus not removed")
oname = vname + '.out' oname = vname + '.out'
msg.dump(open('test/'+oname,"w")) msg.dump(open('test/'+oname,"w"))
msg = mime.MimeMessage(open('test/'+oname,"r")) msg = mime.message_from_file(open('test/'+oname,"r"))
parts = msg.get_payload() txt2 = msg.get_payload()
txt2 = parts[part].get_payload() if type(txt2) == list:
self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2) txt2 = txt2[part].get_payload()
self.failUnless(
txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
def testDefang3(self): def testDefang3(self):
self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif') self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif')
@@ -55,11 +96,11 @@ class MimeTestCase(unittest.TestCase):
# virus6 has no parts - the virus is directly inline # virus6 has no parts - the virus is directly inline
def testDefang6(self,vname="virus6",fname='FAX20.exe'): def testDefang6(self,vname="virus6",fname='FAX20.exe'):
msg = mime.MimeMessage(open('test/'+vname,"r")) msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg) mime.defang(msg)
oname = vname + '.out' oname = vname + '.out'
msg.dump(open('test/'+oname,"w")) msg.dump(open('test/'+oname,"w"))
msg = mime.MimeMessage(open('test/'+oname,"r")) msg = mime.message_from_file(open('test/'+oname,"r"))
self.failIf(msg.ismultipart()) self.failIf(msg.ismultipart())
txt2 = msg.get_payload() txt2 = msg.get_payload()
self.failUnless(txt2 == mime.virus_msg % \ self.failUnless(txt2 == mime.virus_msg % \
@@ -68,11 +109,11 @@ class MimeTestCase(unittest.TestCase):
# honey virus has a sneaky ASP payload which is parsed correctly # honey virus has a sneaky ASP payload which is parsed correctly
# by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1 # by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1
def testDefang7(self,vname="honey",fname='story[1].scr'): def testDefang7(self,vname="honey",fname='story[1].scr'):
msg = mime.MimeMessage(open('test/'+vname,"r")) msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg) mime.defang(msg)
oname = vname + '.out' oname = vname + '.out'
msg.dump(open('test/'+oname,"w")) msg.dump(open('test/'+oname,"w"))
msg = mime.MimeMessage(open('test/'+oname,"r")) msg = mime.message_from_file(open('test/'+oname,"r"))
parts = msg.get_payload() parts = msg.get_payload()
txt2 = parts[1].get_payload() txt2 = parts[1].get_payload()
txt3 = parts[2].get_payload() txt3 = parts[2].get_payload()
@@ -83,13 +124,28 @@ class MimeTestCase(unittest.TestCase):
('story[1].asp',hostname,None),txt3) ('story[1].asp',hostname,None),txt3)
def testParse2(self,fname="spam7"): def testParse2(self,fname="spam7"):
msg = mime.MimeMessage(open('test/'+fname,"r")) msg = mime.message_from_file(open('test/'+fname,"r"))
self.failUnless(msg.ismultipart()) self.failUnless(msg.ismultipart())
parts = msg.get_payload() parts = msg.get_payload()
self.failUnless(len(parts) == 2) self.failUnless(len(parts) == 2)
name = parts[1].getname() name = parts[1].getname()
self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name) self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name)
def testZip(self,vname="zip1",fname='zip.zip'):
self.testDefang(vname,1,'zip.zip')
# test scan_zip flag
msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg,scan_zip=False)
self.failIf(msg.ismodified())
# test ignoring empty zip (often found in DSNs)
msg = mime.message_from_file(open('test/zip2','r'))
mime.defang(msg,scan_zip=True)
self.failIf(msg.ismodified())
# test corrupt zip (often an EXE named as a ZIP)
self.testDefang('zip3',1,'zip.zip')
# test zip within zip
self.testDefang('ziploop',1,'stuart@bmsi.com.zip')
def testHTML(self,fname=""): def testHTML(self,fname=""):
result = StringIO.StringIO() result = StringIO.StringIO()
filter = mime.HTMLScriptFilter(result) filter = mime.HTMLScriptFilter(result)
@@ -106,10 +162,11 @@ class MimeTestCase(unittest.TestCase):
def suite(): return unittest.makeSuite(MimeTestCase,'test') def suite(): return unittest.makeSuite(MimeTestCase,'test')
if __name__ == '__main__': if __name__ == '__main__':
import sys
if len(sys.argv) < 2: if len(sys.argv) < 2:
unittest.main() unittest.main()
else: else:
for fname in sys.argv[1:]: for fname in sys.argv[1:]:
fp = open(fname,'r') fp = open(fname,'r')
msg = mime.MimeMessage(fp) msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True)
print msg.as_string()
+6 -6
View File
@@ -17,7 +17,7 @@ class TestMilter(sample.sampleMilter):
def replacebody(self,chunk): def replacebody(self,chunk):
if self._body: if self._body:
self._body.write(chunk) self._body.write(chunk)
self.bodyreplaced = 1 self.bodyreplaced = True
else: else:
raise IOError,"replacebody not called from eom()" raise IOError,"replacebody not called from eom()"
@@ -29,16 +29,16 @@ class TestMilter(sample.sampleMilter):
del self._msg[field] del self._msg[field]
else: else:
self._msg[field] = value self._msg[field] = value
self.headerschanged = 1 self.headerschanged = True
def addheader(self,field,value): def addheader(self,field,value):
self.log('addheader: %s=%s' % (field,value)) self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value self._msg[field] = value
self.headerschanged = 1 self.headerschanged = True
def feedMsg(self,fname): def feedMsg(self,fname):
self._body = None self._body = None
self.bodyreplaced = 0 self.bodyreplaced = False
self.headerschanged = 0 self.headerschanged = 0
fp = open('test/'+fname,'r') fp = open('test/'+fname,'r')
msg = rfc822.Message(fp) msg = rfc822.Message(fp)
@@ -85,7 +85,7 @@ class TestMilter(sample.sampleMilter):
def connect(self,host='localhost'): def connect(self,host='localhost'):
self._body = None self._body = None
self.bodyreplaced = 0 self.bodyreplaced = False
rc = sample.sampleMilter.connect(self,host,1,0) rc = sample.sampleMilter.connect(self,host,1,0)
if rc != Milter.CONTINUE and rc != Milter.ACCEPT: if rc != Milter.CONTINUE and rc != Milter.ACCEPT:
self.close() self.close()
@@ -108,7 +108,7 @@ class BMSMilterTestCase(unittest.TestCase):
open('test/'+fname+".tstout","w").write(fp.getvalue()) open('test/'+fname+".tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
fp.seek(0) fp.seek(0)
msg = mime.MimeMessage(fp) msg = mime.message_from_file(fp)
s = msg.get_payload(1).get_payload() s = msg.get_payload(1).get_payload()
milter.log(s) milter.log(s)
milter.close() milter.close()
+48
View File
@@ -0,0 +1,48 @@
import unittest
import doctest
import os
import Milter.utils
from Milter.cache import AddrCache
from Milter.dynip import is_dynip
class AddrCacheTestCase(unittest.TestCase):
def setUp(self):
self.fname = 'test.dat'
def tearDown(self):
os.remove(self.fname)
def testAdd(self):
cache = AddrCache(fname=self.fname)
cache['foo@bar.com'] = None
cache.addperm('baz@bar.com')
cache['temp@bar.com'] = 'testing'
self.failUnless(cache.has_key('foo@bar.com'))
self.failUnless(not cache.has_key('hello@bar.com'))
self.failUnless('baz@bar.com' in cache)
self.assertEquals(cache['temp@bar.com'],'testing')
s = open(self.fname).readlines()
self.failUnless(len(s) == 2)
self.failUnless(s[0].startswith('foo@bar.com '))
self.assertEquals(s[1].strip(),'baz@bar.com')
# check that new result overrides old
cache['temp@bar.com'] = None
self.failUnless(not cache['temp@bar.com'])
def testDomain(self):
fp = open(self.fname,'w')
print >>fp,'spammer.com'
fp.close()
cache = AddrCache(fname=self.fname)
cache.load(self.fname,30)
self.failUnless('spammer.com' in cache)
def suite():
s = unittest.makeSuite(AddrCacheTestCase,'test')
s.addTest(doctest.DocTestSuite(Milter.utils))
s.addTest(doctest.DocTestSuite(Milter.dynip))
return s
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())