Compare commits

..

139 Commits

Author SHA1 Message Date
cvs2svn 1c53a7b6fb This commit was manufactured by cvs2svn to create tag 'milter-0_8_8'.
Sprout from master 2007-07-25 19:04:44 UTC Stuart Gathman <stuart@gathman.org> 'Fixes from test on EL5.'
Cherrypick from bmsi 2005-05-31 18:23:49 UTC Stuart Gathman <stuart@gathman.org> 'Development changes since 0.7.2':
    rejects.py
    rhsbl.m4
    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
2007-07-25 19:04: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
50 changed files with 3662 additions and 2776 deletions
+4 -1
View File
@@ -5,8 +5,11 @@ 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):
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 Scott Kitterman
+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.
+29 -11
View File
@@ -1,3 +1,10 @@
On Sun, 11 Feb 2007, Rick Saul wrote:
> Stuart I was planning to move to centos4.4 in a couple of weeks anyway...
> Your advice of where to go from here.
Oh - you are asking for a howto.
Step one. Which DSPAM is right for you? Step one. Which DSPAM is right for you?
The DSPAM project makes dspam part of the LDA (Local Delivery Agent). The DSPAM project makes dspam part of the LDA (Local Delivery Agent).
@@ -28,39 +35,42 @@ wish to install pydspam.
For basic pymilter you'll need: For basic pymilter you'll need:
python-2.4 python-2.4
milter-0.8.2 (the RH9 rpm should work on Fedora Core - let me know) milter-0.8.7
sendmail-8.13.x (with milter support enabled) sendmail-8.13.x (with milter support enabled)
and for SPF you'll need: and for SPF you'll need:
pydns-2.3.0-2.4 pydns-2.3.0-2.4
pyspf-2.0.3-2.py24
and for SRS you'll need: and for SRS you'll need:
pysrs-0.30.9-1.py24 pysrs-0.30.11-1.py24
I'm pretty sure you will want to have SPF and SRS available. I'm pretty sure you will want to have SPF and SRS available.
Step three. Activate basic milter. Step three. Activate basic milter.
Activate the basic milter by editing /etc/mail/sendmail.mc and adding: Activate the basic milter and pysrs by editing /etc/mail/sendmail.mc and adding:
define(`NO_SRS_FILE',`/etc/mail/no-srs-mailers')dnl
dnl define(`NO_SRS_FROM_LOCAL')dnl
HACK(`pysrs',`/var/run/milter/pysrs')dnl
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m') INPUT_MAIL_FILTER(`pythonfilter', `S=local:/var/run/milter/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
You can then "make sendmail.cf" and restart sendmail. You can then "make sendmail.cf" and restart sendmail.
Start milter and pysrs with "service milter start", "service pysrs start".
Tail /var/log/milter/milter.log while SMTP clients connect to your Tail /var/log/milter/milter.log while SMTP clients connect to your
sendmail instance. This should show you what the milter is doing. sendmail instance. This should show you what the milter is doing.
By default, milter-0.8.2 rejects on SPF fail, except for listed domains By default, milter-0.8.7 rejects on SPF fail.
(that are known to be broken). Some admins don't like that, and 0.8.3 will use
the /etc/mail/access database to configure SPF responses. For now,
if you don't like SPF, you can disable spf by replacing "import spf"
with "spf = None" around line 285 in /var/log/milter/bms.py.
Step four. Tweaking the basic config. Step four. Tweaking the basic config.
Most pymilter configuration is in /etc/mail/pymilter.cfg. Most pymilter configuration is in /etc/mail/pymilter.cfg. To activate
changes, "service milter restart".
By default, milter scans attachments for executable extensions. You can By default, milter scans attachments for executable extensions. You can
turn this off by setting banned_exts to the empty list. There are options turn this off by setting banned_exts to the empty list. There are options
@@ -76,7 +86,9 @@ should also run pymilter with similar policies. (But this isn't
needed for initial testing.) needed for initial testing.)
Configure internal_connect with subnets of your internal SMTP clients. Configure internal_connect with subnets of your internal SMTP clients.
Internal connections skip SPF testing and other policies. Internal connections skip SPF testing and other policies. You will
likely need to set this to allow outgoing mail if you have
an SPF policy already.
Configure internal_domains with domains used by your internal SMTP clients. Configure internal_domains with domains used by your internal SMTP clients.
If they attempt to use any other domain, the attempt is blocked and the If they attempt to use any other domain, the attempt is blocked and the
@@ -116,7 +128,7 @@ The CBV policy requires a valid HELO name. If the EHLO name is
RFC2822 compliant, then a DSN is sent to the alleged sender. The RFC2822 compliant, then a DSN is sent to the alleged sender. The
template for the DSN is selected according to the SPF result: template for the DSN is selected according to the SPF result:
Fail: softfail.txt Fail: fail.txt
SoftFail: softfail.txt SoftFail: softfail.txt
Neutral: neutral.txt Neutral: neutral.txt
PermError: permerror.txt PermError: permerror.txt
@@ -134,3 +146,9 @@ SRS config
pydspam config pydspam config
wiretap config wiretap config
--
Stuart D. Gathman <stuart@bmsi.com>
Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flammis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.
+8 -1
View File
@@ -4,19 +4,26 @@ include NEWS
include HOWTO include HOWTO
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 testutils.py
include testbms.py include testbms.py
include testdspam.py
include rejects.py include rejects.py
include report.py
include bms.py include bms.py
include spf.py include spf.py
include cid2spf.py include cid2spf.py
include spfquery.py include spfquery.py
include test.py include test.py
include sample.py include sample.py
include milter-template.py
include spfmilter.py
include spfmilter.rc
include spfmilter.cfg
include test/* include test/*
include doc/*
include Milter/*.py include Milter/*.py
include *.spec include *.spec
include start.sh include start.sh
+5 -5
View File
@@ -9,14 +9,14 @@ 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, setbacklog, settimeout, \ 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: from milter import QUARANTINE try: from milter import QUARANTINE
except: pass except: pass
__version__ = '0.8.3' __version__ = '0.8.5'
_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
@@ -106,8 +106,8 @@ class Milter:
return self.__ctx.setreply(rcode,xcode,msg,*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)
+143
View File
@@ -0,0 +1,143 @@
# 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.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)
l = time.strptime(ts,AddrCache.time_format)
t = time.mktime(l)
if t < too_old:
changed = True
continue
cache[rcpt.lower()] = (t,None)
except:
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_key(self,sender):
"True if sender is cached and has not expired."
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]
try:
user,host = sender.split('@',1)
return self.has_key(host)
except ValueError:
pass
except KeyError:
try:
user,host = sender.split('@',1)
return self.has_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)
+59
View File
@@ -0,0 +1,59 @@
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
+48 -101
View File
@@ -4,110 +4,44 @@
# Send DSNs, do call back verification, # Send DSNs, do call back verification,
# and generate DSN messages from a template # and generate DSN messages from a template
# $Log$
# 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 smtplib
import spf import spf
import socket import socket
from email.Message import Message from email.Message import Message
import Milter import Milter
import time
nospf_msg = """Subject: Critical mail server configuration error def send_dsn(mailfrom,receiver,msg=None,timeout=600):
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Someone at IP address %(connectip)s sent an email claiming
to be from %(sender)s.
If that wasn't you, then your domain, %(sender_domain)s,
was forged - i.e. used without your knowlege or authorization by
someone attempting to steal your mail identity. This is a very
serious problem, and you need to provide authentication for your
SMTP (email) servers to prevent criminals from forging your
domain. The simplest step is usually to publish an SPF record
with your Sender Policy.
For more information, see: http://spfhelp.net
I hate to annoy you with a DSN (Delivery Status
Notification) from a possibly forged email, but since you
have not published a sender policy, there is no other way
of bringing this to your attention.
If it *was* you that sent the email, then your email domain
or configuration is in error. If you don't know anything
about mail servers, then pass this on to your SMTP (mail)
server administrator. We have accepted the email anyway, in
case it is important, but we couldn't find anything about
the mail submitter at %(connectip)s to distinguish it from a
zombie (compromised/infected computer - usually a Windows
PC). There was no PTR record for its IP address (PTR names
that contain the IP address don't count). RFC2821 requires
that your hello name be a FQN (Fully Qualified domain Name,
i.e. at least one dot) that resolves to the IP address of
the mail sender. In addition, just like for PTR, we don't
accept a helo name that contains the IP, since this doesn't
help to identify you. The hello name you used,
%(heloname)s, was invalid.
Furthermore, there was no SPF record for the sending domain
%(sender_domain)s. We even tried to find its IP in any A or
MX records for your domain, but that failed also. We really
should reject mail from anonymous mail clients, but in case
it is important, we are accepting it anyway.
We are sending you this message to alert you to the fact that
Either - Someone is forging your domain.
Or - You have problems with your email configuration.
Or - Possibly both.
If you need further assistance, please do not hesitate to
contact me again.
Kind regards,
postmaster@%(receiver)s
"""
softfail_msg = """Subject: SPF softfail (POSSIBLE FORGERY)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
"""
def send_dsn(mailfrom,receiver,msg=None):
"""Send DSN. If msg is None, do callback verification. """Send DSN. If msg is None, do callback verification.
Mailfrom is original sender we are sending DSN or CBV to. Mailfrom is original sender we are sending DSN or CBV to.
Receiver is the MTA sending the DSN. Receiver is the MTA sending the DSN.
Return None for success or (code,msg) for failure.""" Return None for success or (code,msg) for failure."""
user,domain = mailfrom.split('@') user,domain = mailfrom.split('@')
q = spf.query(None,None,None) try:
mxlist = q.dns(domain,'MX') q = spf.query(None,None,None)
mxlist = q.dns(domain,'MX')
except spf.TempError:
return (450,'DNS Timeout: %s MX'%domain) # temp error
if not mxlist: if not mxlist:
mxlist = (0,domain), # fallback to A record when no MX mxlist = (0,domain), # fallback to A record when no MX
else: else:
mxlist.sort() mxlist.sort()
smtp = smtplib.SMTP() smtp = smtplib.SMTP()
toolate = time.time() + timeout
for prior,host in mxlist: for prior,host in mxlist:
try: try:
smtp.connect(host) smtp.connect(host)
@@ -145,11 +79,17 @@ def send_dsn(mailfrom,receiver,msg=None):
pass # any other error, try next MX pass # any other error, try next MX
except socket.error: except socket.error:
pass # MX didn't accept connections, try next one pass # MX didn't accept connections, try next one
except socket.timeout:
pass # MX too slow, try next one
smtp.close() 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 return (450,'No MX servers available') # temp error
def create_msg(q,rcptlist,origmsg=None,template=None): def create_msg(q,rcptlist,origmsg=None,template=None):
"Create a DSN message from a template. Template must be '\n' separated." "Create a DSN message from a template. Template must be '\n' separated."
if not template:
return None
heloname = q.h heloname = q.h
sender = q.s sender = q.s
connectip = q.i connectip = q.i
@@ -166,30 +106,37 @@ def create_msg(q,rcptlist,origmsg=None,template=None):
msg = Message() msg = Message()
msg.add_header('To',sender)
msg.add_header('From','postmaster@%s'%receiver)
msg.add_header('Auto-Submitted','auto-generated (configuration error)')
msg.add_header('X-Mailer','PyMilter-'+Milter.__version__) msg.add_header('X-Mailer','PyMilter-'+Milter.__version__)
msg.set_type('text/plain') msg.set_type('text/plain')
if not template: hdrs,body = template.split('\n\n',1)
if spf_result and spf_result.startswith('softfail'):
template = softfail_msg
else:
template = nospf_msg
hdrs,body = template.split('\n',1)
for ln in hdrs.splitlines(): for ln in hdrs.splitlines():
name,val = ln.split(':',1) name,val = ln.split(':',1)
msg.add_header(name,(val % locals()).strip()) msg.add_header(name,(val % locals()).strip())
msg.set_payload(body % locals()) msg.set_payload(body % locals())
# add headers if missing from old template
if 'to' not in msg:
msg.add_header('To',sender)
if 'from' not in msg:
msg.add_header('From','postmaster@%s'%receiver)
if 'auto-submitted' not in msg:
msg.add_header('Auto-Submitted','auto-generated')
return msg return msg
if __name__ == '__main__': if __name__ == '__main__':
q = spf.query('192.168.9.50', q = spf.query('192.168.9.50',
'SRS0=pmeHL=RH=bmsi.com=stuart@bmsi.com', 'SRS0=pmeHL=RH==stuart@example.com',
'bmsred.bmsi.com',receiver='mail.bmsi.com') 'red.example.com',receiver='mail.example.com')
msg = create_msg(q,['charlie@jsconnor.com'],None,None) 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 msg.as_string()
# print send_dsn(f,msg.as_string()) # print send_dsn(f,msg.as_string())
print send_dsn(q.s,'mail.bmsi.com',msg.as_string()) # print send_dsn(q.s,'mail.example.com',msg.as_string())
+3 -1
View File
@@ -44,6 +44,8 @@ def is_dynip(host,addr):
True True
>>> is_dynip('[1.2.3.4]','1.2.3.4') >>> is_dynip('[1.2.3.4]','1.2.3.4')
True True
>>> is_dynip('c-71-63-151-151.hsd1.mn.comcast.net','71.63.151.151')
True
""" """
if host.startswith('[') and host.endswith(']'): if host.startswith('[') and host.endswith(']'):
return True return True
@@ -54,7 +56,7 @@ def is_dynip(host,addr):
h = host h = host
m = ip3.findall(host) m = ip3.findall(host)
if m: if m:
g = map(int,m) g = map(int,m)[:4]
ia3 = (ia[1:],ia[:3]) ia3 = (ia[1:],ia[:3])
if g[-3:] in ia3: return True if g[-3:] in ia3: return True
if g[0] == ia[3] and g[1:3] == ia[:2]: return True if g[0] == ia[3] and g[1:3] == ia[:2]: return True
+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
+85
View File
@@ -0,0 +1,85 @@
import re
import struct
import socket
import email.Errors
from fnmatch import fnmatchcase
from email.Header import decode_header
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 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
+40 -1
View File
@@ -1,5 +1,44 @@
Here is a history of user visible changes to Python milter. Here is a history of user visible 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. 0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
spf_accept_fail option for braindead SPF senders spf_accept_fail option for braindead SPF senders
(treats fail like softfail) (treats fail like softfail)
+13 -3
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,8 +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: 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')
+176 -14
View File
@@ -1,24 +1,140 @@
Don't match dynamic ptr in bestguess.
When content filtering is not installed, reject BLACKLISTed MFROM
immediately. There is no use waiting until EOM.
Configuration is problematic when handling incoming, but not outgoing mail.
The problem comes when alice@example.com sends mail to bill@example.com,
and we are the MX for example.com, but alice is sending from some other
MTA. The mail is flagged external, so we don't list example.com in
internal_domains (or we would get "spam from self"). But, if we try to do a
CBV, we get "fraudulent MX", because the MX is ourself! So we need to
avoid doing CBV on such domains. Currently, we try to make sure the SPF
policies don't do CBV.
We now don't check internal domains for incoming mail if there is an
SPF record.
On the other hand, if alice is sending internally, or with SMTP AUTH, she
*does* need the domain to be in internal_domains. The solution to that
is to use the new SMTP AUTH access configuration to specify which domains
can be used by smtp AUTH (by user if desired).
It would be cleaner if CBV would know which domains we have agreed to
be MX for. Some ideas for external connections:
a) check access file for To:example.com RELAY
b) check mailertable
c) check mx_domains config list
d) if there is an SPF record, don't check internal_domains
(let SPF block unauthorized machines)
But that still doesn't handle the roaming user, who won't use SMTP
AUTH, but sends through some hotel MTA. Maybe we don't want to support
him?
When setting up pydspam, both sender and rcpt must resolve to dspam users
for falsepositive recognition. Usually, this means adding
honeypot@mail.example.com to alias list for honeypot in pymilter.cfg.
This needs to be documented. I was caught by it setting up a new site.
Add signature (x-sig=AB7485f=TS) to Received-SPF, so it can be used
to blacklist sources of delayed DSNs.
rcpt-addr may let us know when a recipient is unknown. That should count
against reputation.
Need to use wildcards in blacklist.log: *.madcowsrecord.net
Need to exclude emails like !*-admin@example.com in whitelist_sender.
Need to exclude robot users from autowhitelist. Don't want to have to
list all users, so implement something like !*-admin@bmsi.com,@bmsi.com.
GOSSiP feedback from user training is ignored because UMIS has already been
removed from queue. Maybe keep UMIS in queue, and add method to
alter last feedback for ID.
Generate DSNs according to RFC 3464
Get temperror policy from access file.
Reporting explanation for failure should show source if sender
provided explanation.
Bug in Auto-whitelist. Recent Auto-whitelist doesn't override expired entry.
SPF permerror diagnostics should include corrected mechanism.
Delay SPF check until RCPT TO. Cache result to avoid repeating
for multiple RCPT. This avoids overhead for invalid RCPT, and
allows for per RCPT local policy.
Check SPF for outgoing mail (including local policy for internal addresses).
This could also solve the second part of the mail from relay problem below.
Whitelisted senders from trusted relay get PROBATION. Need to extracted
SPF result from headers - and in the case of mail internal to relay
(e.g. bmsi.com), supply 'pass' result.
For selected domains, check rcpts via CBV before accepting mail. Cache
results. This will kick out dictonary attacks against a mail domain
behind a gateway sooner.
Add auto-blacklisted senders to blacklist.log with timestamp.
Add emails blacklisted via CBV so that they are remembered across milter
restarts.
Make all dictionaries work like honeypot. Do not train as ham unless
whitelisted. Train on blacklisted messages, or spam feedback. This
can be called Train On Error. Should be possible to startup
with training on everything to get dictionary built fast, then switch
to train on error to minimize labor.
Allow unsigned DSNs from selected domains (that don't accept signed MFROM,
e.g. verizon.net).
Allow verified hostnames for trusted_relay. E.g. HELO name that
passes SPF.
Table of sendmail macros for documentation.
When do we get two hello calls? STARTTLS is one reason.
Option: accept mail from auto-whitelisted senders even with spf-fail,
but do not update dspam. This can be done for individual senders or domains
using the access file.
pysrs: SRS doesn't get applied to proper recipients when there are
multiple recipients. This requires debugging cf scripts - yuk.
auto_whitelist false_positives from quarantine - perhaps only when
user selects special button (use special header to communicate
that from dspamcgi.py to milter.)
Use send_dsn.log for blacklist also. AddrCache needs localpart
wildcard (e.g. empty localpart).
Quarantined mail is missing headers modified/added by milter after
checking dspam.
Send DSN for permerror before processing extended result. An additional
DSN may be sent based on extended result. Send permerror DSN to
postmaster@sending_domain.
Rescind whitelist for banned extensions, in case sender is infected.
Train honeypot on error only.
Find rfc2822 policy for MFROM quoting. Find rfc2822 policy for MFROM quoting.
Use /etc/mail/access for domain specific SPF policies. Support explicit errors for SPF policy in access file:
SPF-Fail: REJECT
SPF-Softfail: OK
SPF-Neutral: OK
SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS" SPF-Neutral:aol.com ERROR:"550 AOL mail must get SPF PASS"
Defer TEMPERROR in SPF evaluation - give precedence to security Defer TEMPERROR in SPF evaluation - give precedence to security
(only defer for PASS mechanisms). (only defer for PASS mechanisms).
Option to add Received-SPF header, but never reject on SPF.
I think the above will handle this.
Create null config that does nothing - except maybe add Received-SPF Create null config that does nothing - except maybe add Received-SPF
headers. Many admins would like to turn features on one at a time. headers. Many admins would like to turn features on one at a time.
Auto whitelist based on outgoing email - perhaps with magic subject
or recipient prefix.
Can't output messages with malformed rfc822 attachments. Can't output messages with malformed rfc822 attachments.
Move milter,Milter,mime,spf modules to pymilter Move milter,Milter,mime,spf modules to pymilter
@@ -33,7 +149,8 @@ check spam keywords with character classes, e.g.
Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS
forwarder accounts, and a util provides a special local alias for the forwarder accounts, and a util provides a special local alias for the
user to give to the forwarder. Alias only works for mail from that user to give to the forwarder. (Or user just adds arbitrary alias
unique to that forwarder to a database.) Alias only works for mail from that
forwarder. Milter gets forwarder domain from alias and uses it to forwarder. Milter gets forwarder domain from alias and uses it to
SPF check forwarder. SPF check forwarder.
@@ -43,8 +160,6 @@ a separate process. However, a significant amount of memory is wasted
for each additional Python VM, and communication between milters for each additional Python VM, and communication between milters
is cumbersome (e.g., adding mail headers, writing external files). is cumbersome (e.g., adding mail headers, writing external files).
Backup copies for outgoing/incoming mail.
Copy incoming wiretap mail, even though sendmail alias works perfectly Copy incoming wiretap mail, even though sendmail alias works perfectly
for the purpose, to avoid having to change two configs for a wiretap. for the purpose, to avoid having to change two configs for a wiretap.
@@ -62,3 +177,50 @@ embarrass yourself), and also removing Received headers with hidepath.
Need a test module to feed sample messages to a milter though a live 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, sendmail and SMTP. The mockup currently used is probably not very accurate,
and doesn't test the threading code. and doesn't test the threading code.
DONE Require signed MFROM for all incoming bounces when signing all outgoing
mail - except from trusted relays.
DONE Added Message-ID header to DSN with SRS signed sender. When seen on
incoming rfc ignorant failure message, blacklist sender.
DONE Option to add Received-SPF header, but never reject on SPF.
I think the above will handle this.
DONE Received-SPF header field should show identity that was checked.
DONE When training with spam, REJECT after data so that mistakenly blacklisted
senders at least get an error.
DONE Milter won't start when it can't change permissions on *.lock to match
*.log. Should maybe ignore that error - the effect will be to set
the permissions to default.
DONE Milter won't start when a whitelist/blacklist file is missing.
DONE Delayed failure detection should parse From header to find email address.
DONE When bms.py can't find templates, it passes None to dsn.create_msg(),
which uses local variable as backup, which no longer exist. Do plain
CBV in that case instead.
DONE Find and use X-GOSSiP: header for SPAM: and FP: submissions. Would need
to keep tags longer.
DONE Parse incoming 3464 DSNs for "Action: failed" to recognize delayed
failures. This works regardless of Subject.
DONE Reports PROBATION even when rejecting message (works, but confusing in
log).
DONE Delayed_failure detection needs to handle multi-line header fields.
Also, delayed_failure should be recognized when addressed to
postmaster@helodomain
DONE DSN for Permerror shows 'None' for error under some condition.
DONE Allow blacklisted emails as well as domains in blacklist.log. Use same
data structure as autowhitelist.log.
DONE Backup copies for outgoing/incoming mail.
+1069 -784
View File
File diff suppressed because it is too large Load Diff
-153
View File
@@ -1,153 +0,0 @@
#!/usr/bin/python2.3
# Convert a MS Caller-ID entry (XML) to a SPF entry
#
# (c) 2004 by Ernesto Baschny
# (c) 2004 Python version by Stuart Gathman
#
# Date: 2004-02-25
# Version: 1.0
#
# Usage:
# ./cid2spf.pl "<ep xmlns='http://ms.net/1'>...</ep>"
#
# Note that the 'include' directives will also have to be checked and
# "translated". Future versions of this script might be able to get a
# domain name as an argument and "crawl" the DNS for the necessary
# information.
#
# A complete reverse translation (SPF -> CID) might be impossible, since
# there are no way to handle:
# - PTR and EXISTS mechanism
# - MX mechanism with an different domain as argument
# - macros
#
# References:
# http://www.microsoft.com/mscorp/twc/privacy/spam_callerid.mspx
# http://spf.pobox.com/
#
# Known bugs:
# - Currently it won't handle the exclusions provided in the A and R
# tags (prefix '!'). They will show up "as-is" in the SPF record
# - I really haven't read the MS-CID specs in-depth, so there are probably
# other bugs too :)
#
# Ernesto Baschny <ernst@baschny.de>
#
import xml.sax
import spf
# -------------------------------------------------------------------------
class CIDParser(xml.sax.ContentHandler):
"Convert a MS Caller-ID entry (XML) to a SPF entry"
def __init__(self,q=None):
self.spf = []
self.action = '-all'
self.has_servers = None
self.spf_entry = None
if q:
self.spf_query = q
else:
self.spf_query = spf.query(i='127.0.0.1', s='localhost', h='unknown')
def startElement(self,tag,attr):
if tag == 'm':
if self.has_servers != None and not self.has_servers:
raise ValueError(
"Declared <noMailServers\> and later <m>, this CID entry is not valid."
)
self.has_servers = True
elif tag == 'noMailServers':
if self.has_servers:
raise ValueError(
"Declared <m> and later <noMailServers\>, this CID entry is not valid."
)
self.has_servers = False
elif tag == 'ep':
if attr.has_key('testing') and attr.getValue('testing') == 'true':
# A CID with 'testing' found:
# From the MS-specs:
# "Documents in which such attribute is present with a true
# value SHOULD be entirely ignored (one should act as if the
# document were absent)"
# From the SPF-specs:
# "Neutral (?): The SPF client MUST proceed as if a domain did
# not publish SPF data."
# So we set SPF action to "neutral":
self.action = '?all'
elif tag == 'mx':
# The empty MX-tag, same as SPF's MX-mechanism
self.spf.append('mx')
self.tag = tag
def characters(self,text):
tag = self.tag
# Remove starting and trailing spaces from text:
text = text.strip()
if tag == 'a' or tag == 'r':
# The A and R tags from MS-CID are both handled by the
# ipv4/6-mechanisms from SPF:
if text.find(':') < 0:
mechanism = 'ip4'
else:
mechanism = 'ip6'
self.spf.append(mechanism + ':' + text)
elif tag == 'indirect':
# MS-CID's indirect is "sort of" the include from SPF:
# Not really true, because the <indirect> tag from MS-CID also
# provides a fallback in case the included domain doesn't provide
# _ep-records: The inbound MX-servers of the included domains
# are added to the list of allowed outgoing mailservers for the
# domain that declared the _ep-record with the <indirect> tag.
# In SPF you would use the 'mx:domain' to handle this, but this
# wouldn't depend on referred domain having or not SPF-records.
cid_xml = self.cid_txt(text)
if cid_xml:
p = CIDParser()
xml.sax.parseString(cid_xml,p)
if p.has_servers != False:
self.spf += p.spf
else:
self.spf.append('mx:' + text)
def cid_txt(self,domain):
q = self.spf_query
domain='_ep.' + domain
a = q.dns_txt(domain)
if not a: return None
if a[0].lower().startswith('<ep ') and a[-1].lower().endswith('</ep>'):
return ''.join(a)
return None
def endElement(self,tag):
if tag == 'ep':
# This is the end... assemble what we've got
spf_entry = ['v=spf1']
if self.has_servers != False:
spf_entry += self.spf
spf_entry.append(self.action)
self.spf_entry = ' '.join(spf_entry)
def spf_txt(self,cid_xml):
if not cid_xml.startswith('<'):
cid_xml = self.cid_txt(cid_xml)
if not cid_xml: return None
# Parse the beast. Any XML-problem will be reported by xlm.sax
self.spf_entry = None
xml.sax.parseString(cid_xml,self)
return self.spf_entry
if __name__ == '__main__':
import sys
if len(sys.argv) < 2:
print >>sys.stderr, \
"""Usage: %s "<ep xmlns='http://ms.net/1'>...</ep>" """ % sys.argv[0]
sys.exit(1)
cid_xml = sys.argv[1]
p = CIDParser()
print p.spf_txt(cid_xml)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+181
View File
@@ -0,0 +1,181 @@
Title: Recent Changes
<h2> Recent Changes </h2>
<h3> 0.8.7 </h3>
The spf module has been moved to the
<a href="http://cheeseshop.python.org/pypi/pyspf">pyspf</a> package.
Download <a href="http://sourceforge.net/project/showfiles.php?group_id=139894&package_id=191419">here</a>.
<h3> 0.8.6 </h3>
Python milter has been moved to
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
project</a> for development and release downloads.
<h3> 0.8.5 </h3>
Release 0.8.5 fixes some build bugs reported by Stephen Figgins. It
fixes many small things, like not auto-whitelisting recipients of
outgoing mail when the subject contains "autoreply:". There is a
simple trusted forwarder implementation. If you have more than
2 or so forwarders, we will need a way to "compile" SPF records into an
IP set and TTL for it to be efficient (like libspf2 does).
<h3> GOSSiP </h3>
An alpha release of <a href="pygossip.html">pygossip</a> has been commited to
CVS, module pygossip. A version of the bms.py milter has been commited to CVS
which supports calling GOSSiP to track domain reputation in a local database.
<h3> New website design </h3>
Hey, I'm no artist, so I just used the
<a href="http://ht2html.sourceforge.net/">ht2html</a> package
by <a href="http://barry.wooz.org/">Barry Warsaw</a>. The mascot
is by <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>,
or maybe his wife. I chose Maxwell's daemon because it tirelessly
and invisibly sorts molecules, just as milters sort mail.
Christian has also provided a fun
<a href="http://alphard.ethz.ch/hafner/PPS/PPS2002/Maxwell/simulation.htm">
simulation</a> that lets you try your hand at sorting molecules.
<h3> 0.8.4 </h3>
Release 0.8.4 makes configuring SPF policy via access.db actually work.
The honeypot idea is enhanced by auto-whitelisting recipients of
email sent from selected domains. Whitelisted messages are then used
to train the honeypot. This makes the honeypot screener entirely self
training. The smfi_progress() API is now automatically supported when present.
An optional idx parameter to milter.addheader() invokes smfi_insheader().
<h3> 0.8.3 </h3>
Release 0.8.3 uses the standard logging module, and supports configuring
more detailed SPF policy via the sendmail access map. SMTP AUTH connections
are considered INTERNAL. Preventing forgery between internal domains is
just a matter of specifying the user-domain map - I'll define something
for the next version. We now send DSNs when mail is quarantined (rejecting
if DSN fails) and for SPF syntax errors (PermError). There is an
experimental option to add a Sender header when it is missing and the From
domain doesn't match the MAIL FROM domain. Next release, we may start
renaming and replacing an existing Sender header when neither it nor the
From domain matches MAIL FROM. Since bogus MAIL FROMs are rejected
(to varying degrees depending on the configured SPF policy), and
both Sender and From and displayed by default in many email clients,
this provides some phishing protection without rejecting mail based
on headers.
<h3> 0.8.2 </h3>
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
in line with the newly official RFC. It adds
<a href="http://ses.codeshare.ca/">SES</a>
support (the original SES without body hash) for pysrs-0.30.10, and honeypot
support for pydspam-1.1.9. There is a new method in the base milter module.
milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or
TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
<h3> 0.8.0 </h3>
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
release. It supports Python-2.4, and provides an option to accept mail
that gets an SPF softfail or fails the 3 strikes rule, provided the
alleged sender accepts a DSN explaining the problem. Python-2.3 is
no longer supported by the reworked mime.py module, although API changes
could be backported. There are too many incompatible changes to the
python email package.
<h3> Older Releases </h3>
Release 0.7.2 tightens the authentication screws with a "3 strikes and
you're out" policy. A sender must have a valid PTR, HELO, or SPF record
to send email. Specific senders can be whitelisted using the
"delegate" option in the spf configuration section by adding a
default SPF record for them. The PTR and HELO are required
by RFC anyway, so this is not an unreasonable requirement.
There is now a coherent policy for an SPF softfail result. A softfail
is accepted if there is a valid PTR or HELO, or if the domain
is listed in the "accept_softfail" option of the spf configuration section.
A neutral result is accepted by default if there is a valid PTR or
HELO, (and the SPF record was not guessed), unless the domain is listed in the
"reject_neutral" option. Common forms of PTR records for dynamic IPs are
recognized, and do not count as a valid PTR. This does not prevent anyone
from sending mail from a dynamic IP - they just need to configure a
valid HELO name or publish an SPF record.
<p>
As SPF adoption continues to rise, forged spam is not getting through. So
spammers are publishing their SPF records as predicted. The 0.7.2 RPM
now provides the <code>rhsbl</code> sendmail hack so that spammer domains
can be blacklisted. With the RPM installed, add a line like the following
to your <code>sendmail.mc</code>.
<pre>
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
</pre>
<p>
Of course, spammers are now starting to register
throwaway domains. The next thing we need is a custom DNS server,
in Python, that
can recognize patterns. For instance, one spammer registers ded304.com,
ded305.com, ded306.com, etc. We also need the custom DNS server to
let SPF classic clients check SES (which will be part of pysrs).
The <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
framework provides a custom DNS server - but I
would like a smaller implementation for our use.
<p>
The RPM for release 0.7.0 moves the config file and socket locations to
/etc/mail and /var/run/milter respectively. We now parse Microsoft CID records
- but only hotmail.com uses them. They seem to have applied for a patent on
the brilliant idea of examining the mail headers to see who the message is
from. We aren't doing that here, so not to worry - but I am not a lawyer, so
if you are worried, change spf.py around line 626 to return None instead of
calling CIDParser(). There is a new option to reject mail with no PTR
and no SPF.
<p>
Microsoft is pushing an anti-opensource license for their pending patent
along with their sender-ID proposal before the IETF.
It is royalty free - but requires anyone distributing a binary they've
compiled from source to sign a license agreement. The Apache Software
Foundation <a
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
the problem with sender-ID</a>, and Debian <a
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
software in general</a> and the <a
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
particular</a>, Python milter will not be able to implement sender-ID in its
current form. This was, no doubt, Microsoft's intent all along.
<p>
Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I
recommend ignoring it and continuing to implement and improve SPF until a
working and unencumbered proposal for RFC2822 headers surfaces.
<p>
<a href="http://openspf.com">
<img src="SPF.gif" align=left alt="SPF logo"></a>
Release 0.6.6 adds support for <a href="http://openspf.com/">SPF</a>,
a protocol to prevent forging of the envelope from address.
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
The included spf.py module is an updated version of the original 1.6
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
The updated version tracks the draft RFC and test suite.
<p>
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
dspam bayes filter project</a> and <a href="dspam.html">
packaged it for python</a>.
Release 0.6.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers.
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
+55
View File
@@ -0,0 +1,55 @@
Title: Credits
<h1> CREDITS </h1>
<a href="mailto:Jim Niemira <urmane@urmane.org>">Jim Niemira</a>
wrote the original C module and some quick
and dirty python to use it.
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">Stuart D. Gathman</a>
took that kludge and added threading and context objects to it, wrote a proper
OO wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a
real, usable Python extension.
<h2>Other contributors (in random order):</h2>
<dl>
<dt> <a href="http://alphard.ethz.ch/hafner/lebl.htm">Christian Hafner</a>
<dd>for the pymilter mascot image of
<a href="http://maxwelld.netfirms.com/">
Maxwell's daemon</a>
<dt>Stephen Figgins
<dd>for reporting problems building with sendmail-8.12, and when
building milter.so for the first time.
<dt>Dave MacQuigg
<dd>for noticing that smfi_insheader wasn't supported, and creating
a template to help first time pymilter users create their own milter.
<dt>Terence Way
<dd>for providing a Python port of SPF
<dt>Scott Kitterman
<dd>for doing lots of testing and debugging of SPF against draft standard,
and for putting up a <a href="http://www.kitterman.com/spf/validate.html">
web page that validates SPF</a> records using spf.py
<dt>Alexander Kourakos
<dd>for plugging several memory leaks
<dt>George Graf at Vienna University of Economics and Business Administration
<dd>for handling None passed to setreply and chgheader.
<dt>Deron Meranda
<dd>for IPv6 patches
<dt>Jason Erikson
<dd>for handling NULL hostaddr in connect callback.
<dt>John Draper
<dd>for porting Python milter to OpenBSD, and starting to work on tutorials
then pointing out that it would be easier to just write the MTA in Python.
<dt>Eric S. Johansson
<dd>for helpful design discussions while working on camram
<dt>Alex Savguira
<dd>for finding bugs with international headers and
suggesting the scan_zip option.
<dt><a href="http://www.bmsi.com">Business Management Systems</a>
<dd>for hosting the website, and providing paying clients who need milter
service so I can work on it as part of my day job.
</dl>
If I have left anybody out, send me a reminder:
<a href="mailto:Stuart Gathman <stuart@bmsi.com>">stuart@bmsi.com</a>
+113 -18
View File
@@ -1,13 +1,25 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> Title: Python Milter FAQ
<html>
<head>
<title>Python Milter FAQ</title>
</head><body>
<h1> Python Milter <a name=faq>FAQ</a> </h1> <h1> Python Milter <a name=faq>FAQ</a> </h1>
<menu>
<li> <a href="#compiling">Compiling Python Milter</a>
<li> <a href="#running">Running Python Milter</a>
<li> <a href="#spf">Using SPF</a>
<li> <a href="#srs">Using SRS</a>
</menu>
<ol> <ol>
<h3> Compiling Python Milter </h3>
<h3> <a name="compiling">Compiling Python Milter </a> </h3>
<li> Q. I have tried to download the current milter code and my virus scan
traps several viruses in the download.
<p> A. The milter source includes a number of deactivated viruses in
the test directory. All but the first and last lines of the base64
encoded virus data has been removed. I suppose I should randomize
the first and last lines as well, since pymilter just deletes executables,
and doesn't look for signatures.
<li> Q. I have installed sendmail from source, but Python milter won't <li> Q. I have installed sendmail from source, but Python milter won't
compile. compile.
<p> A. Even though libmilter is officially supported in sendmail-8.12, <p> A. Even though libmilter is officially supported in sendmail-8.12,
@@ -22,10 +34,25 @@ shows you how to install libmilter with a separate invocation of make.
<li> Q. Why is mfapi.h not found when I try to compile Python milter on <li> Q. Why is mfapi.h not found when I try to compile Python milter on
RedHat 7.2? RedHat 7.2?
<p> A. RedHat forgot to include the header in the RPM. See the <p> A. RedHat forgot to include the header in the RPM. See the
<a href="milter.html#rh72">RedHat 7.2 requirements</a>. <a href="requirements.html#rh72">RedHat 7.2 requirements</a>.
<p> <p>
<li> Q. Python milter compiles ok, but I get an error like this when
I try to import the milter module:
<pre>
ImportError: /usr/lib/python2.4/site-packages/milter.so: undefined symbol: smfi_setmlreply
</pre>
<p> A. Your libmilter.a is from sendmail-8.12 or earlier. You need
sendmail-8.13 or later to support setmlreply. You can disable
setmlreply by changing setup.py. Change:
<pre>
define_macros = [ ('MAX_ML_REPLY',32) ]
</pre>
in setup.py to
<pre>
define_macros = [ ('MAX_ML_REPLY',1) ]
</pre>
<h3> Running Python Milter </h3> <h3> <a name="running">Running Python Milter </a></h3>
<li> Q. The sample.py milter prints a message, then just sits there. <li> Q. The sample.py milter prints a message, then just sits there.
<pre> <pre>
@@ -103,13 +130,14 @@ If you are running bms.py, then the block_chinese option in
<li> Q. Why does sendmail coredump with milters on OpenBSD? <li> Q. Why does sendmail coredump with milters on OpenBSD?
<p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD. <p> A. Sendmail has a problem with unix sockets on old versions of OpenBSD.
Use an internet domain socket instead. For example, in OpenBSD users report that this problem has been fixed, so upgrading
OpenBSD will fix this. Otherwise, you can
use an internet domain socket instead. For example, in
<code>sendmail.cf</code> use <code>sendmail.cf</code> use
<pre> <pre>
Xpythonfilter, S=inet:1234@localhost Xpythonfilter, S=inet:1234@localhost
</pre> </pre>
and change sample.py accordingly. and change sample.py accordingly.
<p> OpenBSD users report that this problem has been fixed.
<p> <p>
<li> Q. How can I change the bounce message for an invalid recipient? <li> Q. How can I change the bounce message for an invalid recipient?
@@ -143,10 +171,15 @@ Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
</pre> </pre>
<li> Q. There is a Python traceback in the log file! What happened to <li> Q. There is a Python traceback in the log file! What happened to
my email? my email?
<p> A. When the milter fails with an untrapped exception, a TEMPFAIL <p> A. By default, when the milter fails with an untrapped exception, a
result (451) is returned to the sender. The sender will then retry every TEMPFAIL result (451) is returned to the sender. The sender will then retry
hour or so for several days. Hopefully, someone will notice the every hour or so for several days. Hopefully, someone will notice the
traceback, and workaround or fix the problem. traceback, and workaround or fix the problem. Beginning with milter-0.8.2,
you can call <code>milter.set_exception_policy(milter.CONTINUE)</code>
to cause an untrapped exception to continue processing with the
next callback or milter instead. For
completeness, you can also set the exception policy to
<code>milter.REJECT</code>.
<li> Q. I read some notes such as "Check valid domains allowed by internal <li> Q. I read some notes such as "Check valid domains allowed by internal
senders to detect PCs infected with spam trojans." but could not senders to detect PCs infected with spam trojans." but could not
@@ -169,10 +202,16 @@ The <code>internal_domains</code> option is simplistic, it assumes all
valid senders of the domains are internal. SPF provides a much more general valid senders of the domains are internal. SPF provides a much more general
check of IP and MAIL FROM for external email. Pymilter should soon check of IP and MAIL FROM for external email. Pymilter should soon
have a local policy feature for more general checking of internal mail. have a local policy feature for more general checking of internal mail.
<li> Q. <code>mail_archive</code> isn't working. Or I don't understand how
it's suppose to work. I have
<code>mail_archive = /var/mail/mail_archive</code>
in <code>pymilter.cfg</code> but nothing ever gets dumped into
<code>/var/mail/mail_archive</code>.
<p> A. The 'mail' user needs to have write access. Permission failures
should be logged as a traceback in milter.log if it doesn't.
<h3> Using SPF </h3> <h3> <a name="spf">Using SPF </a></h3>
<a name="spf">
<li> Q. So how do I use the SPF support? The sample.py milter doesn't seem <li> Q. So how do I use the SPF support? The sample.py milter doesn't seem
to use it. to use it.
<p> A. The bms.py milter supports spf. The RedHat RPMs will set almost <p> A. The bms.py milter supports spf. The RedHat RPMs will set almost
@@ -192,7 +231,63 @@ everything up for you. For other systems:
logfiles and a simple cron script using <code>find</code> to clean logfiles and a simple cron script using <code>find</code> to clean
<code>tempdir</code>. <code>tempdir</code>.
</ol> </ol>
In CVS, there is <code>spfmilter.py</code>. Run that as a service,
and it does just SPF. It uses the sendmail <code>access</code>
file to configure SPF responses just like <code>bms.py</code>, but
supports only REJECT and OK.
<li> Q. The SPF DSN is sent at least once for domains that don't publish a SPF.
How do I stop this behavior?
<p> A. The SPF response is controlled by <code>/etc/mail/access</code>
(actually the file you specify with <code>access_file</code> in
the <code>[spf]</code> section of <code>pymilter.cfg</code>).
Responses are OK, CBV, and REJECT. CBV sends the DSN.
<p>
You can change the defaults. For instance, I have:
<pre>
SPF-None: REJECT
SPF-Neutral: CBV
SPF-Softfail: CBV
SPF-Permerror: CBV
</pre>
I have best_guess = 1, so SPF none is converted to PASS/NEUTRAL for policy
lookup, and 3 strikes (no PTR, no HELO, no SPF) becomes "SPF NONE" for local
policy purposes (the Received-SPF header always shows the official SPF
result.)
<p>
You can change the default for specific domains:
<pre>
# these guys aren't going to pay attention to CBVs anyway...
SPF-None:cia.gov REJECT
SPF-None:fbi.gov REJECT
SPF-Neutral:aol.com REJECT
SPF-Softfail:ebay.com REJECT
</pre>
<h3> <a name="srs">Using SRS </a></h3>
<li> Q. The SRS part doesn't seem to work as whenever I try to start
<code>/etc/init.d/pysrs</code>, I get this in
<code>/var/log/milter/pysrs.log</code>:
<pre>
ConfigParser.NoOptionError: No option 'fwdomain' in section: 'srs'
</pre>
<p> A. You need to specify the forward domain - i.e. the domain you want
SRS to rewrite stuff too.
<p>
For instance, I have:
<pre>
# sample SRS configuration
[srs]
secret = don't you wish
maxage = 8
hashlength = 5
;database=/var/log/milter/srs.db
fwdomain = bmsi.com
sign=bmsi.com,mail.bmsi.com,gathman.org
srs=bmsaix.bmsi.com,bmsred.bmsi.com,stl.gathman.org,bampa.gathman.org
</pre>
The <code>sign</code> is for local domains which are signed.
The <code>srs</code> list is for other domains which you are relaying,
and which need to have SRS checked/undone for bounces.
</ol> </ol>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<!-- -*- html -*- -->
<h3>Subsections</h3>
<li><a href="milter.html">Introduction</a>
<li><a href="changes.html">Changes</a>
<li><a href="requirements.html">Requirements</a>
<li><a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download</a>
<li><a href="faq.html">FAQ</a>
<li><a href="policy.html">Policies</a>
<li><a href="logmsgs.html">Log&nbsp;Messages</a>
<li><a href="http://bmsi.com/mailman/listinfo/pymilter">Mailing&nbsp;List</a>
<li><a href="credits.html">CREDITS</a>
<li><a href="http://sourceforge.net"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=139894&amp;type=1" width="88" height="31" border="0" alt="SourceForge.net Logo" /></a>
<h3>Links</h3>
<li><a href="http://www.milter.org/milter_api/api.html">C&nbsp;API</a>
<li><a href="http://www.milter.org/">Milter.Org</a>
<li><a href="http://www.python.org/">Python.Org</a>
<li><a href="http://www.sendmail.org/">Sendmail.Org</a>
<li><a href="http://www.openspf.org/">SPF</a>
<li><a href="pysrs.html">pysrs</a>
<li><a href="http://cheeseshop.python.org/pypi/pyspf">pyspf</a>
<li><a href="http://bmsi.com/python/pygossip.html">pygossip</a>
<li><a href="http://bmsi.com/python/dspam.html">pydspam</a>
<li><a href="http://bmsi.com/libdspam/dspam.html">libdspam</a>
+91
View File
@@ -0,0 +1,91 @@
Title: Python Milter Log Documentation
<style>
DT { font-weight: bolder; padding-top: 1em }
</style>
<h1> Milter Log Documentation </h1>
The milter log from the bms.py application has a variety of "tags" in it that
indicate what it did.
<dl>
<dt> DSPAM: honeypot SCREENED
<dd> message was quarantined to the honeypot quarantine
<dt> REJECT: hello SPF: fail 550 access denied
<dt> REJECT: hello SPF: softfail 550 domain in transition
<dt> REJECT: hello SPF: neutral 550 access neither permitted nor denied
<dd> message was rejected because there was an SPF policy for the
HELO name, and it did not pass.
<dt> CBV: sender-17-44662668-643@bluepenmagic.com
<dd> we performed a call back verification
<dt> dspam
<dd> dspam identifier was added to the message
<dt> REJECT: spam from self: jsconnor.com
<dd> message was reject because HELO was us (jsconnor.com)
<dt> INNOC: richh
<dd> message was used to update richh's dspam dictionary
<dt> HONEYPOT: pooh@bwicorp.com
<dd> message was sent to a honeypot address (pooh@bwicorp.com), the
message was added to the honeypot dspam dictionary as spam
<dt> REJECT: numeric hello name: 63.217.19.146
<dd> message was rejected because helo name was invalid (numeric)
<dt> eom
<dd> message was successfully received
<dt> TEMPFAIL: CBV: 450 No MX servers available
<dd> we tried to do a call back verification but could not look up
MX record, we told the sender to try again later
<dt> CBV: info@emailpizzahut.com (cached)
<dd> call back verification was needed, we had already done it recently
<dt> abort after 0 body chars
<dd> sender hung up on us
<dt> REJECT: SPF fail 550 SPF fail: see
http://openspf.com/why.html?sender=m.hendersonxk@163.net&ip=213.47.161.100
<dd> message was reject because its sender's spf policy said to
<dt> REJECT: Subject: Cialis - No prescription needed!
<dd> message was rejected because its subject contained a bad expression
<dt> REJECT: zombie PC at 192.168.3.37 sending MAIL FROM seajdr@amritind.com
<dd> message was rejected because the connect ip was internal, but the
sender was not. This is usually because a Windows PC is infected with
malware.
<dt> X-Guessed-SPF: pass
<dd> When the SPF result is NONE, we guess a result based on the generic
SPF policy "v=spf1 a/24 mx/24 ptr".
<dt> DSPAM: tonyc tonyc@example.com
<dd> message was sent to tonyc@example.com and it was identified as spam
and placed in the tonyc dspam quarantine
<dt> REJECT: CBV: 550 calvinalstonis@ix.netcom.com...User unknown
<dt> REJECT: CBV: 553 sorry, that domain isn't in my list
<dt> REJECT: CBV: 554 delivery error: dd This user doesn't have an account
<dd> message was rejected because call back verification gave us a fatal
error
<dt> Auto-Whitelist: user@example.com
<dd> recipient has been added to auto_whitelist.log because the message
was sent from an internal IP and the recipient is not internal.
<dt> WHITELIST user@example.com
<dd> message is whitelisted because sender appears in auto_whitelist.log
<dt> BLACKLIST user@example.com
<dd> message is blacklisted because sender appears in blacklist.log or
failed a CBV test.
<dt> TRAINSPAM: honeypot X-Dspam-Score: 0.002278
<dd> message was used to train screener dictionary as spam
<dt> TRAIN: honeypot X-Dspam-Score: 0.980203
<dd> message was used to train screener dictionary as ham
</dl>
<br>
+56 -259
View File
@@ -1,33 +1,29 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> Title: Python Milters
<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/"> <P ALIGN="CENTER"><A HREF="http://www.anybrowser.org/campaign/">
<IMG SRC="/art/brain1.gif" <IMG SRC="http://bmsi.com/art/brain1.gif"
ALT="Viewable With Any Browser" BORDER="0"></A> ALT="Viewable With Any Browser" BORDER="0"></A>
<img src="/art/banner_4.gif" width="468" height="60" border="0" <img src="http://bmsi.com/art/banner_4.gif" width="468" height="60" border="0"
usemap="#banner_4" alt="Your vote?"> usemap="#banner_4" alt="Your vote?">
<map name="banner_4"> <map name="banner_4">
<area shape="rect" coords="330,25,426,59" <area shape="rect" coords="330,25,426,59"
href="http://education-survey.org/" alt="I Disagree"> href="http://education-survey.org/" alt="I Disagree">
<area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree"> <area shape="rect" coords="234,28,304,57" href="http://www.honestEd.com/" alt="I Agree">
</map> </map>
</P> </P>
<img src="Maxwells.gif" alt="Maxwell's Daemon: pymilter mascot" align=left>
<h1 align=center>Sendmail Milters in Python</h1> <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> <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"> and <a href="mailto:%73%74%75%61%72%74%40%62%6D%73%69%2E%63%6F%6D">
Stuart D. Gathman</a><br> Stuart D. Gathman</a><br>
This web page is written by Stuart D. Gathman<br>and<br>sponsored by 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> <a href="http://www.bmsi.com">Business Management Systems, Inc.</a> <br>
Last updated Aug 28, 2005</h4> Last updated Mar 30, 2007</h4>
See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> | See the <a href="faq.html">FAQ</a> | <a href="http://sourceforge.net/project/showfiles.php?group_id=139894">Download now</a> |
<a href="/mailman/listinfo/pymilter">Subscribe to mailing list</a> | <a href="http://bmsi.com/mailman/listinfo/pymilter">Subscribe to mailing list</a> |
<a href="#overview">Overview</a> | <a href="#overview">Overview</a> |
<a href="/python/dspam.html">pydspam</a> | <a href="/python/dspam.html">pydspam</a> |
<a href="/libdspam/dspam.html">libdspam</a> <a href="/libdspam/dspam.html">libdspam</a>
@@ -45,159 +41,6 @@ separation features to enhance security. Even better, sendmail 8.13
supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more supports socket maps, which makes <a href="pysrs.html">pysrs</a> much more
efficient and secure. I recommend upgrading. efficient and secure. I recommend upgrading.
<h2> Recent Changes </h2>
Python milter is being moved to
<a href="http://sourceforge.net/projects/pymilter/">pymilter Sourceforge
project</a> for development and release downloads.
<p>
Release 0.8.2 has changes to <a href="http://openspf.net">SPF</a> to bring it
in line with the newly official RFC. It adds
<a href="http://ses.codeshare.ca/">SES</a>
support (the original SES without body hash) for pysrs-0.30.10, and honeypot
support for pydspam-1.1.9. There is a new method in the base milter module.
milter.set_exception_policy(i) lets you choose a policy of CONTINUE, REJECT, or
TEMPFAIL (default) for untrapped exceptions encountered in a milter callback.
<p>
Release 0.8.0 is the first <a href="http://sourceforge.net/">Sourceforge</a>
release. It supports Python-2.4, and provides an option to accept mail
that gets an SPF softfail or fails the 3 strikes rule, provided the
alleged sender accepts a DSN explaining the problem. Python-2.3 is
no longer supported by the reworked mime.py module, although API changes
could be backported. There are too many incompatible changes to the
python email package.
<p>
Release 0.7.2 tightens the authentication screws with a "3 strikes and
you're out" policy. A sender must have a valid PTR, HELO, or SPF record
to send email. Specific senders can be whitelisted using the
"delegate" option in the spf configuration section by adding a
default SPF record for them. The PTR and HELO are required
by RFC anyway, so this is not an unreasonable requirement.
There is now a coherent policy for an SPF softfail result. A softfail
is accepted if there is a valid PTR or HELO, or if the domain
is listed in the "accept_softfail" option of the spf configuration section.
A neutral result is accepted by default if there is a valid PTR or
HELO, (and the SPF record was not guessed), unless the domain is listed in the
"reject_neutral" option. Common forms of PTR records for dynamic IPs are
recognized, and do not count as a valid PTR. This does not prevent anyone
from sending mail from a dynamic IP - they just need to configure a
valid HELO name or publish an SPF record.
<p>
As SPF adoption continues to rise, forged spam is not getting through. So
spammers are publishing their SPF records as predicted. The 0.7.2 RPM
now provides the <code>rhsbl</code> sendmail hack so that spammer domains
can be blacklisted. With the RPM installed, add a line like the following
to your <code>sendmail.mc</code>.
<pre>
HACK(rhsbl,`blackholes.example.com',"550 Rejected: " $&{RHS} " has been spamming our customers.")dnl
</pre>
<p>
Of course, spammers are now starting to register
throwaway domains. The next thing we need is a custom DNS server,
in Python, that
can recognize patterns. For instance, one spammer registers ded304.com,
ded305.com, ded306.com, etc. We also need the custom DNS server to
let SPF classic clients check SES (which will be part of pysrs).
The <a href="http://twistedmatrix.com/products/twisted">Twisted Python</a>
framework provides a custom DNS server - but I
would like a smaller implementation for our use.
<p>
The RPM for release 0.7.0 moves the config file and socket locations to
/etc/mail and /var/run/milter respectively. We now parse Microsoft CID records
- but only hotmail.com uses them. They seem to have applied for a patent on
the brilliant idea of examining the mail headers to see who the message is
from. We aren't doing that here, so not to worry - but I am not a lawyer, so
if you are worried, change spf.py around line 626 to return None instead of
calling CIDParser(). There is a new option to reject mail with no PTR
and no SPF.
<p>
Microsoft is pushing an anti-opensource license for their pending patent
along with their sender-ID proposal before the IETF.
It is royalty free - but requires anyone distributing a binary they've
compiled from source to sign a license agreement. The Apache Software
Foundation <a
href="http://www.apache.org/foundation/docs/sender-id-position.html"> explains
the problem with sender-ID</a>, and Debian <a
href="http://www.debian.org/News/2004/20040904">concurs</a>. Since
the <a href="http://download.microsoft.com/download/4/3/9/439b024b-09fd-44ee-8ff0-10e834004c36/senderid_FAQ.PDF">Microsoft license</a> is
<a href="http://www.circleid.com/article/732_0_1_0_C/">incompatible with free
software in general</a> and the <a
href="http://www.imc.org/ietf-mxcomp/mail-archive/msg03678.html">GPL in
particular</a>, Python milter will not be able to implement sender-ID in its
current form. This was, no doubt, Microsoft's intent all along.
<p>
Sender-ID attempts to do for RFC2822 headers what SPF does for RFC2821 headers.
Unlike SPF, it has never been tried, and is encumbered by a stupid patent. I
recommend ignoring it and continuing to implement and improve SPF until a
working and unencumbered proposal for RFC2822 headers surfaces.
<p>
<a href="http://openspf.com">
<img src="SPF.gif" align=left alt="SPF logo"></a>
Release 0.6.6 adds support for <a href="http://openspf.com/">SPF</a>,
a protocol to prevent forging of the envelope from address.
SPF support requires <a href="http://pydns.sourceforge.net/">pydns</a>.
The included spf.py module is an updated version of the original 1.6
version at <a href="http://www.wayforward.net/spf/">wayforward.net</a>.
The updated version tracks the draft RFC and test suite.
<p>
The FAQ addresses <a href="faq.html#spf">how to get started with SPF</a>.
<p>
Release 0.6.1 adds a full milter based dspam application.
<p>
I have selected the <a href="http://www.nuclearelephant.com/projects/dspam/">
dspam bayes filter project</a> and <a href="dspam.html">
packaged it for python</a>.
Release 0.6.0 offers a simple application of dspam I call "header triage",
which rejects messages with spammy headers.
To use header triage, you must have <a href="dspam.html">DSPAM</a> installed,
and select a dictionary that is well moderated by someone who gets
lots of spam. That dictionary can be used to block spam that is
obvious from the headers (e.g. X-Mailer and Subject) before it ties
up any more resources. I have yet to see any false positives from this
approach (check the milter log), but if there are, the sender will
get a REJECT with the message "Your message looks spammy."
<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><a name=overview>Overview</a></h3> <h3><a name=overview>Overview</a></h3>
This package provides a robust toolkit for Python <a This package provides a robust toolkit for Python <a
@@ -245,6 +88,8 @@ content filtering. SPF checking
requires <a href="http://pydns.sourceforge.net/"> requires <a href="http://pydns.sourceforge.net/">
pydns</a>. Configuration documentation is currently included as comments pydns</a>. Configuration documentation is currently included as comments
in the <a href="milter.cfg">sample config file</a> for the bms.py milter. in the <a href="milter.cfg">sample config file</a> for the bms.py milter.
See also the <a href="HOWTO">HOWTO</a> and <a href="logmsgs.html">
Milter Log Message Tags</a>.
<p> <p>
Python milter is under GPL. The authors can probably be convinced to Python milter is under GPL. The authors can probably be convinced to
change this to LGPL if needed. change this to LGPL if needed.
@@ -376,8 +221,14 @@ me if you successfully install milter on a system not mentioned below.
<td>0.5.5</td><tr> <td>0.5.5</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.13.1</td> <td>RedHat 7.3</td><td>gcc-2.96</td><td>2.3.3</td><td>8.13.1</td>
<td>0.7.2</td><tr> <td>0.7.2</td><tr>
<td>RedHat 7.3</td><td>gcc-2.96</td><td>2.4.1</td><td>8.13.5</td>
<td>0.8.4</td><tr>
<td>RedHat 8.0</td><td>gcc-3.2</td><td>2.2.1</td><td>8.12.6</td> <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>0.5.2</td><tr>
<td>RedHat 9.0</td><td>gcc-3.2.2</td><td>2.4.1</td><td>8.13.1</td>
<td>0.8.2</td><tr>
<td>RedHat EL3</td><td>gcc-3.2.3</td><td>2.4.1</td><td>8.13.5</td>
<td>0.8.4</td><tr>
<td>Debian Linux</td><td>gcc-2.95.2</td><td>2.1.1</td><td>8.12.0</td> <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>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>Debian Linux</td><td>gcc-3.2.2</td><td>2.2.2</td><td>8.12.7</td>
@@ -388,8 +239,8 @@ me if you successfully install milter on a system not mentioned below.
<td>0.3.4</td><tr> <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>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>0.4.2</td><tr>
<td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.2.3</td><td>8.13.1</td> <td>AIX-4.1.5</td><td>gcc-2.95.2</td><td>2.4.1</td><td>8.13.1</td>
<td>0.7.1</td><tr> <td>0.8.4</td><tr>
<td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td> <td>Slackware 7.1</td><td>?</td><td>?</td><td>8.12.1</td>
<td>0.3.8</td><tr> <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>Slackware 9.0</td><td>gcc-3.2.2</td><td>2.2.3</td><td>8.12.9</td>
@@ -403,108 +254,54 @@ me if you successfully install milter on a system not mentioned below.
<td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td> <td>FreeBSD</td><td>gcc-2.95.3</td><td>2.2.2</td><td>?</td>
<td>0.5.5</td><tr> <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>FreeBSD 4.4</td><td>gcc-2.95.3</td><td>?</td><td>8.12.10</td>
<td>0.6.6</td><tr> <td>0.6.6</td>
</table> </table>
<h3> Requirements </h3> <h2> Enough Already! </h2>
<menu> Nearly a dozen people have emailed me begging for a feature to copy
<li> While the miltermodule will work with python 1.5, you probably outgoing and/or incoming mail to a backup directory by user. Ok, it
want to use python 2.0 or better. The python code uses a number of looks like this is a most requested feature for 0.5.6. In the meantime,
python 2 features. here are some things to consider:
<li> Python must be configured with thread support. This is because <ul>
sendmail's libmilter requires thread support. <li> If you want to equivalent of a Bcc added to each message, this
<li> You must compile sendmail with libmilter enabled. In versions of is very easy to do in the python code for bms.py. See below.
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and <li> If you want to copy to a file in a directory (thus avoiding having to
is not installed by default. set up aliases), this is slightly more involved. The bms.py milter already
Sendmail 8.12 still does not enable libmilter by default. You must copies the message to a temporary file for use in replacing the message body
explicitly select the "MILTER" option when compiling. when banned attachments are found. You have to open a file, and copy the
<li> Python milter has been tested against sendmail-8.11 and sendmail-8.12. Mesage object to it in eom().
<li> Python milter must be compiled for the specific version of sendmail <li> Finally, you are probably aware that most email clients already
it will run with. (Since the result is dynamically loaded, there could keep a copy of outgoing mail? Presumably there is a good reason for
conceivably be multiple versions available and selected at startup - but keeping another copy on the server.
that will have to wait.) This situation may only exist for sendmail </ul>
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> <p>
Sendmail-8.12 renames To Bcc a message, call <code>self.add_recipient(rcpt)</code> in envfrom after
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system determining whether you want to copy (e.g. whether the sender is local). For
shared library. Therefore, I rename libsm.a back to libsmutil.a for example,
AIX. This presents a problem for setup.py. <pre>
def envfrom(...
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3> ...
if len(t) == 2:
If you are running Redhat 7.2, the distributed version of sendmail self.rejectvirus = t[1] in reject_virus_from
now enables libmilter by default. RedHat 7.2 bundles if t[0] in wiretap_users.get(t[1],()):
the development libraries with the main sendmail package, so self.add_recipient(wiretap_dest)
there is no sendmail-devel package. However, they forgot to include the if t[1] == 'mydomain.com':
headers! So you'll have to get the SRPM and modify it. I suggest self.add_recipient('&lt;copy-%s&gt;' % t[0])
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> </pre>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
<p> <p>
If you do modify the SRPM, I suggest renaming libsmutil.a To make this a generic feature requires thinking about how the configuration
to libsm.a - just like sendmail-8.12 will. If you manually install would look. Feel free to make specific suggestions about config file
mfapi.h or don't rename libsmutil.a, you'll entries. Be sure to handle both Bcc and file copies, and designating what
need to force <code>libs = ["milter", "smutil"]</code> in setup.py. mail should be copied. How should "outgoing" be defined? Implementing it is
<p> easy once the configuration is designed.
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> <hr>
<p> <p>
<a href="http://validator.w3.org/check/referer"> <a href="http://validator.w3.org/check/referer">
<img border=0 src="/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a> <img border=0 src="http://bmsi.com/vh32.png" alt=" [ Valid HTML 3.2! ] " height=31 width=88></a>
<a href="http://www.redhat.com"> <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> <img src="http://bmsi.com/art/powered_by.gif" width="88" height="31" alt=" [ Powered By Red Hat Linux ] " border="0"></a>
</p> </p>
</body></html>
+9 -9
View File
@@ -1,11 +1,12 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> Title: Python Milter Mail Policy
<html>
<head>
<title>Python Milter Mail Policy </title>
</head><body>
<h1> Python Milter Mail Policy </h1> <h1> Python Milter Mail Policy </h1>
These are the policies implemented by the <code>bms.py</code> milter
application. The milter and Milter modules do not implement any policies
by themselves. Eventually, I'll get the bms.py milter moved to its
own package.
<h3> Classify connection </h3> <h3> Classify connection </h3>
When the SMTP client connects, the connection IP address is When the SMTP client connects, the connection IP address is
@@ -72,7 +73,9 @@ The wiretap feature can screen and/or monitor mail to/from certain
users. If the MAIL FROM is being wiretapped, the recipients are users. If the MAIL FROM is being wiretapped, the recipients are
altered accordingly. altered accordingly.
<h4> SPF check </h4> <!--table-stop-->
<h2> SPF check </h2>
Finally, the MAIL FROM, connect IP, and HELO name are checked against Finally, the MAIL FROM, connect IP, and HELO name are checked against
any SPF records published via DNS for the alleged sender (MAIL FROM). any SPF records published via DNS for the alleged sender (MAIL FROM).
@@ -232,6 +235,3 @@ should retry the message at a later time.
</td></tr> </td></tr>
</table> </table>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+99
View File
@@ -0,0 +1,99 @@
Title: Requirements
<h2> Requirements </h2>
<menu>
<li> While the miltermodule will work with python 1.5, you probably
want to use python 2.0 or better. The python code uses a number of
python 2 features. The email support requires python 2.4.
<li> Python must be configured with thread support. This is because
pymilter uses sendmail's libmilter which requires thread support.
<li> You must compile sendmail with libmilter enabled. In versions of
sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and
is not installed by default.
Sendmail 8.12 still does not enable libmilter by default. You must
explicitly select the "MILTER" option when compiling.
<li> When compiling Python milter against sendmail versions earlier than
8.13, you must set MAX_ML_REPLY to 1 in setup.py. There is no way to tell from
the libmilter includes that smfi_setmlreply is not supported.
<li> You probably want to use sendmail-8.13, since that supports multi-line
SMTP error descriptions and SOCKETMAP. You want SOCKETMAP for use with
pysrs.
<li> Python milter has been tested against sendmail-8.11 through sendmail-8.13.
<li> Python milter must be compiled for the specific version of sendmail
it will run with. (Since the result is dynamically loaded, there could
conceivably be multiple versions available and selected at startup - but
that will have to wait.) This situation may only exist for sendmail
versions prior to 8.12. The protocol seems designed for backward
compatibility - and 8.12 is the first official milter release.
<li> Mea Culpa! After reading the Python Style guide, I realize that
my Python code is not up to snuff. Apparently mixed tabs and spaces
are anathema to those using Windows editors, where tabs can be expanded using
any arbitrary algorithm. Other than that, my
intuition matched Guido's pretty well - although I like to indent by 2
rather than 4. I will arrange to have tabs expanded to spaces when
exporting new versions. Until then, beware!
</menu>
<h3> <a name="aix4"> AIX 4.1.5 Requirements </a> </h3>
To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files
for <a href="/aix/sendmail.spec">sendmail-8.11.5</a>
or <a href="/aix/sendmail12.spec">sendmail-8.12.3</a>. If you have
not already set it up, I use a <a href="/aix/aix.spec">dummy RPM package</a>
to represent the stuff that comes with AIX. You might also want
my <a href="/aix/python.spec">python-2.1.1</a> spec file for AIX. It
does not include Tk or curses modules, sorry. If y'all trust me, you can
download rpms for AIX 4.x from my <a href="/aix">AIX RPM directory</a>.
<p>
Sendmail-8.12 renames
libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system
shared library. Therefore, I rename libsm.a back to libsmutil.a for
AIX. This presents a problem for setup.py.
<h3> <a name="rh72"> RedHat 7.2 Requirements </a> </h3>
If you are running Redhat 7.2, the distributed version of sendmail
now enables libmilter by default. RedHat 7.2 bundles
the development libraries with the main sendmail package, so
there is no sendmail-devel package. However, they forgot to include the
headers! So you'll have to get the SRPM and modify it. I suggest
moving the static libs to a devel package and adding the headers. If
this is too much trouble, you can get the <a href="mfapi.h">mfapi.h</a>
header for sendmail-8.6.11 from here and manually install it as
<code>/usr/include/libmilter/mfapi.h</code>.
<p>
If you do modify the SRPM, I suggest renaming libsmutil.a
to libsm.a - just like sendmail-8.12 will. If you manually install
mfapi.h or don't rename libsmutil.a, you'll
need to force <code>libs = ["milter", "smutil"]</code> in setup.py.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
<h3> <a name="rh62"> Redhat 6.2 Requirements </a> </h3>
If you are running Redhat 6.2, the distributed version of sendmail
does not enable libmilter. You can download the Redhat 7.2 sendmail.spec
modified to compile on RedHat 6.2:
<a href="http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec">
sendmail-rhmilter.spec</a>. The <a
href="ftp://updates.redhat.com/7.0/en/os/SRPMS/sendmail-8.11.6-1.7.0.src.rpm">
SRPM for sendmail-8.11.6</a> is available from
<a href="http://www.redhat.com">Redhat</a> under
<a href="http://www.redhat.com/support/errata/RHSA-2001-106.html">
Errata for RH6.2</a>. But that doesn't include the latest security
patches since RH6.2 is no longer supported.
<p>
If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2
from my <a href="http://www.bmsi.com/linux/rh62">linux downloads</a> directory.
The lastest RPMs were built by taking a RH7.2 SRPMS and removing some
RPM features from the spec file that RH6.2 doesn't support, then
recompiling on RH6.2. You can check this by installing the RH7.2 SRPM,
then diffing my sendmail.spec with theirs. Then run
"rpm -bb sendmail-rhmilter.spec" when you are satisfied.
<p>
If you have installed python2, and want
python-milter to use python2, add <code>python=python2</code> to setup.cfg
and build with <code>python2 setup.py bdist_rpm</code>.
You'll need to install the sendmail-devel package to compile milter.
+155
View File
@@ -0,0 +1,155 @@
## 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
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('@')
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()
+48 -11
View File
@@ -6,9 +6,10 @@ tempdir = /var/log/milter/save
# how long to wait for a response from sendmail before giving up # how long to wait for a response from sendmail before giving up
;timeout=600 ;timeout=600
log_headers = 0 log_headers = 0
# connection ips and hostnames are matched against this glob style list # Connection ips and hostnames are matched against this glob style list
# to recognize internal senders. # to recognize internal senders. You probably need to change this.
;internal_connect = 192.168.*.* # The default is a good guess to try and prevent newbie frustration.
internal_connect = 192.168.0.0/16,127.*
# mail that is not an internal_connect and claims to be from an # mail that is not an internal_connect and claims to be from an
# internal domain is rejected. Furthermore, internal mail that # internal domain is rejected. Furthermore, internal mail that
@@ -17,12 +18,15 @@ log_headers = 0
# flexible. However, SPF is not currently checked for outgoing # flexible. However, SPF is not currently checked for outgoing
# (internal_connect) mail because it doesn't yet handle authorizing # (internal_connect) mail because it doesn't yet handle authorizing
# internal IPs locally. # internal IPs locally.
;internal_domains = mycorp.com ;internal_domains = mycorp.com,localhost.localdomain
# connections from a trusted relay can trust the first Received header # connections from a trusted relay can trust the first Received header
# SPF checks are bypassed for internal connections and trusted relays. # SPF checks are bypassed for internal connections and trusted relays.
;trusted_relay = 1.2.3.4, 66.12.34.56 ;trusted_relay = 1.2.3.4, 66.12.34.56
# Relaying to these domains is allowed from internal connections only.
;private_relay = mycorp.com
# Reject external senders with hello names no legit external sender would use. # Reject external senders with hello names no legit external sender would use.
# SPF will do this also, but listing your own domain and mailserver here # SPF will do this also, but listing your own domain and mailserver here
# will save some DNS lookups when rejecting certain viruses. # will save some DNS lookups when rejecting certain viruses.
@@ -31,17 +35,20 @@ log_headers = 0
# Reject mail for domains mentioned unless user is mentioned here also # Reject mail for domains mentioned unless user is mentioned here also
;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com ;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
# Treat localparts in milter.cfg as case-insensitive
case_sensitive_localpart = true
# features intended to filter or block incoming mail # features intended to filter or block incoming mail
[defang] [defang]
# do virus scanning on attached messages also # do virus scanning on attached messages also
scan_rfc822 = 1 scan_rfc822 = 0
# do virus scanning on attached zipfiles also # do virus scanning on attached zipfiles also
scan_zip = 0 scan_zip = 0
# Comment out scripts in HTML attachments. Can be CPU intensive. # Comment out scripts in HTML attachments. Can be CPU intensive.
scan_html = 0 scan_html = 0
# reject messages with asian fonts because we can't read them # reject messages with asian fonts because we can't read them
block_chinese = 1 block_chinese = 0
# list users who hate forwarded mail # list users who hate forwarded mail
;block_forward = egghead@mycorp.com, busybee@mycorp.com ;block_forward = egghead@mycorp.com, busybee@mycorp.com
# reject mail with these case insensitive strings in the subject # reject mail with these case insensitive strings in the subject
@@ -75,7 +82,7 @@ reject_spoofed = 0
# refuses mail from user names commonly abused in that way. # refuses mail from user names commonly abused in that way.
;banned_users = postmaster, mailer-daemon, clamav ;banned_users = postmaster, mailer-daemon, clamav
# See http://spf.pobox.com for more info on SPF. # See http://www.openspf.com for more info on SPF.
[spf] [spf]
# namespace where SPF records can be supplied for domains without one # namespace where SPF records can be supplied for domains without one
# records are searched for under _spf.domain.com # records are searched for under _spf.domain.com
@@ -99,6 +106,11 @@ reject_spoofed = 0
# doesn't match MAIL FROM. Outlook and other email clients will then display # doesn't match MAIL FROM. Outlook and other email clients will then display
# something like: "Sent by sender@domain.com on behalf of from@example.com" # something like: "Sent by sender@domain.com on behalf of from@example.com"
;supply_sender = 0 ;supply_sender = 0
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
# features intended to clean up outgoing mail # features intended to clean up outgoing mail
[scrub] [scrub]
@@ -120,11 +132,15 @@ blind = 1
# discard outgoing mail without alerting sender # discard outgoing mail without alerting sender
# can be used in conjunction with wiretap to censor outgoing mail # can be used in conjunction with wiretap to censor outgoing mail
;discard_users = canned@bigcorp.com ;discard_users = canned@bigcorp.com
# archive copies all delivered mail to a file
;mail_archive = /var/log/mail_archive
# #
# smart aliases trigger on both sender and recipient # smart aliases trigger on both sender and recipient
# alias = sender, recipient[, destination]
# #
;smart_alias = copycust,walter,spy1,spy2 [smart_alias]
# multiple wiretap monitors # multiple wiretap monitors. Smart aliases are applied after wiretap.
;spy1 = disloyal@bigcorp.com,spy@bigcorp.com ;spy1 = disloyal@bigcorp.com,spy@bigcorp.com
;spy2 = bigmouth@bigcorp.com,spy@bigcorp.com ;spy2 = bigmouth@bigcorp.com,spy@bigcorp.com
# mail from client@clientcorp.com to sue@bigcorp.com is redirected to # mail from client@clientcorp.com to sue@bigcorp.com is redirected to
@@ -137,7 +153,7 @@ blind = 1
;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com, ;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com,
; walter@bigcorp.com ; walter@bigcorp.com
;bulk = soruce@telex.com,bob@jsconnor.com ;bulk = soruce@telex.com,bob@jsconnor.com
;bulk = soruce@telex.com,larry@jsconnor.com ;bulk1 = soruce@telex.com,larry@jsconnor.com,bulk
# See http://bmsi.com/python/dspam.html # See http://bmsi.com/python/dspam.html
[dspam] [dspam]
@@ -146,7 +162,13 @@ blind = 1
# only EXTERNAL messages are dspam filtered # only EXTERNAL messages are dspam filtered
;dspam_dict=/var/lib/dspam/moderator.dict ;dspam_dict=/var/lib/dspam/moderator.dict
# Opt-opt recipients from dspam screening and header triage # Recipients of mail sent from these senders are added to the auto_whitelist.
# Auto_whitelisted senders with an SPF PASS are never rejected by dspam, and
# messages from auto_whitelisted senders will be used to train screener
# dictionaries as innocent mail.
;whitelist_senders = @mycorp.com
# Opt-out recipients entirely from dspam screening and header triage
;dspam_exempt=getitall@mycorp.com ;dspam_exempt=getitall@mycorp.com
# Do not scan mail (ostensibly) from these senders # Do not scan mail (ostensibly) from these senders
;dspam_whitelist=getitall@sender.com ;dspam_whitelist=getitall@sender.com
@@ -178,3 +200,18 @@ blind = 1
# delivered. # delivered.
;dspam_screener=david,goliath ;dspam_screener=david,goliath
# The dspam CGI can also be used: logins must match dspam users # The dspam CGI can also be used: logins must match dspam users
# Optional pygossip interface
#
# GOSSiP tracks reputation of domain:qualifier pairs. For instance,
# the reputation of example.com:SPF is tracked separately from
# example.com:neutral. Currently qualifiers are
# SPF,neutral,softfail,fail,permerror,GUESS,HELO
[gossip]
# Use a dedicated GOSSiP server. If not specified, a local database
# will be used.
;server=host:11900
# If a local database is used, also consult these GOSSiP servers about
# domains. Peer reputation is also tracked as to how often they
# agree with us, and weighted accordingly.
;peers=host1:port,host2
+7 -3
View File
@@ -5,10 +5,10 @@
# chkconfig: 2345 80 30 # chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail. # description: Milter is a process that filters messages sent through sendmail.
# processname: milter # processname: milter
# config: /var/log/milter/bms.py # config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid # pidfile: /var/run/milter/milter.pid
python="python2.3" python="python2.4"
pidof() { pidof() {
set - "" set - ""
@@ -32,7 +32,11 @@ start() {
# Start daemons. # Start daemons.
echo -n "Starting $prog: " echo -n "Starting $prog: "
daemon --check milter --user mail /var/log/milter/start.sh if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /var/log/milter/start.sh milter bms
RETVAL=$? RETVAL=$?
echo echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
+7 -3
View File
@@ -5,10 +5,10 @@
# chkconfig: 2345 80 30 # chkconfig: 2345 80 30
# description: Milter is a process that filters messages sent through sendmail. # description: Milter is a process that filters messages sent through sendmail.
# processname: milter # processname: milter
# config: /var/log/milter/bms.py # config: /etc/mail/pymilter.cfg
# pidfile: /var/run/milter/milter.pid # pidfile: /var/run/milter/milter.pid
python="python2.3" python="python2.4"
pidof() { pidof() {
set - "" set - ""
@@ -32,7 +32,11 @@ start() {
# Start daemons. # Start daemons.
echo -n "Starting $prog: " echo -n "Starting $prog: "
daemon --check milter --user mail /var/log/milter/start.sh if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /var/log/milter/start.sh milter bms
RETVAL=$? RETVAL=$?
echo echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter
+159 -37
View File
@@ -1,30 +1,33 @@
%define name milter %define name pymilter
%define version 0.8.3 %define version 0.8.8
%define release 1.RH7 %define release 1
# what version of RH are we building for? # what version of RH are we building for?
%define redhat9 0 %define redhat7 0
%define redhat7 1
%define redhat6 0
# Options for Redhat version 6.x: # Options for Redhat version 6.x:
# rpm -ba|--rebuild --define "rh6 1" # rpm -ba|--rebuild --define "rh7 1"
%{?rh6:%define redhat7 0} %{?rh7:%define redhat7 1}
%{?rh6:%define redhat6 1}
# some systems dont have initrddir defined # some systems dont have initrddir defined
%{?_initrddir:%define _initrddir /etc/rc.d/init.d} %{?_initrddir:%define _initrddir /etc/rc.d/init.d}
%if %{redhat9} %if %{redhat7}
%define sysvinit milter.rc # Redhat 7.x and earlier (multiple ps lines per thread)
%else # Redhat 7.x and earlier (multiple ps lines per thread)
%define sysvinit milter.rc7 %define sysvinit milter.rc7
%else
%define sysvinit milter.rc
%endif %endif
# RH9, other systems (single ps line per process) # RH9, other systems (single ps line per process)
%ifos Linux %ifos Linux
%define python python2.4 %define python python
%else %else
%define python python %define python python
%endif %endif
%ifos aix4.1
%define libdir /var/log/milter
%else
%define libdir /usr/lib/pymilter
%endif
Summary: Python interface to sendmail milter API Summary: Python interface to sendmail milter API
Name: %{name} Name: %{name}
@@ -32,39 +35,67 @@ Version: %{version}
Release: %{release} Release: %{release}
Source: %{name}-%{version}.tar.gz Source: %{name}-%{version}.tar.gz
#Patch: %{name}-%{version}.patch #Patch: %{name}-%{version}.patch
Copyright: GPL License: GPL
Group: Development/Libraries Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-buildroot BuildRoot: %{_tmppath}/%{name}-buildroot
Prefix: %{_prefix} Prefix: %{_prefix}
Vendor: Stuart D. Gathman <stuart@bmsi.com> Vendor: Stuart D. Gathman <stuart@bmsi.com>
Packager: Stuart D. Gathman <stuart@bmsi.com> Packager: Stuart D. Gathman <stuart@bmsi.com>
Url: http://www.bmsi.com/python/milter.html Url: http://www.bmsi.com/python/milter.html
Requires: %{python} >= 2.4, sendmail >= 8.12.10 Requires: %{python} >= 2.4, sendmail >= 8.13
%ifos Linux %ifos Linux
Requires: chkconfig Requires: chkconfig
%endif %endif
BuildRequires: %{python}-devel , sendmail-devel >= 8.12.10 BuildRequires: %{python}-devel >= 2.4, sendmail-devel >= 8.13
%description %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. modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV.
%package -n milter
Group: Applications/System
Summary: BMS spam and reputation milter
Requires: pyspf >= 2.0.4
%description -n milter
A complex but effective spam filtering, SPF checking, and reputation tracking
mail application. It uses pydspam if installed for bayesian filtering.
%package spf
Group: Applications/System
Summary: BMS spam and reputation milter
Requires: pyspf >= 2.0.4
%description spf
A simple mail filter to add Received-SPF headers and reject forged mail.
Rejection policy is configured via sendmail access file.
%prep %prep
%setup %setup
#%patch -p1 #patch -p0 -b .bms
%build %build
env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build %if %{redhat7}
LDFLAGS="-s"
%else # Redhat builds debug packages after 7.3
LDFLAGS="-g"
%endif
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{python} setup.py build
%install %install
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES %{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
grep '.pyc$' INSTALLED_FILES | sed -e 's/c$/o/' >>INSTALLED_FILES
mkdir -p $RPM_BUILD_ROOT/var/log/milter mkdir -p $RPM_BUILD_ROOT/var/log/milter
mkdir -p $RPM_BUILD_ROOT/etc/mail mkdir -p $RPM_BUILD_ROOT/etc/mail
mkdir $RPM_BUILD_ROOT/var/log/milter/save mkdir $RPM_BUILD_ROOT/var/log/milter/save
cp bms.py *.txt $RPM_BUILD_ROOT/var/log/milter mkdir -p $RPM_BUILD_ROOT%{libdir}
cp *.txt $RPM_BUILD_ROOT/var/log/milter
cp bms.py spfmilter.py $RPM_BUILD_ROOT%{libdir}
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
cp spfmilter.cfg $RPM_BUILD_ROOT/etc/mail
# logfile rotation # logfile rotation
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
@@ -73,6 +104,11 @@ cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF'
copytruncate copytruncate
compress compress
} }
/var/log/milter/banned_ips {
rotate 3
daily
copytruncate
}
EOF EOF
# purge saved defanged message copies # purge saved defanged message copies
@@ -86,11 +122,13 @@ cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF'
#!/bin/sh #!/bin/sh
find /var/log/milter/save -mtime +7 | xargs $R rm find /var/log/milter/save -mtime +7 | xargs $R rm
# work around memory leak
/etc/init.d/milter condrestart
EOF EOF
chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
%ifos aix4.1 %ifos aix4.1
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF' cat >$RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
#!/bin/sh #!/bin/sh
cd /var/log/milter cd /var/log/milter
# uncomment to enable sgmlop if installed # uncomment to enable sgmlop if installed
@@ -98,15 +136,10 @@ cd /var/log/milter
exec /usr/local/bin/python bms.py >>milter.log 2>&1 exec /usr/local/bin/python bms.py >>milter.log 2>&1
EOF EOF
%else %else
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF' cp start.sh $RPM_BUILD_ROOT%{libdir}
#!/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 mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
cp spfmilter.rc $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF' ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
/^python=/ /^python=/
c c
@@ -115,8 +148,24 @@ python="%{python}"
w w
q q
EOF EOF
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter <<'EOF'
/^python=/
c
python="%{python}"
.
w
q
EOF
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^python=/
c
python="%{python}"
.
w
q
EOF
%endif %endif
chmod a+x $RPM_BUILD_ROOT/var/log/milter/start.sh chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
mkdir -p $RPM_BUILD_ROOT/var/run/milter mkdir -p $RPM_BUILD_ROOT/var/run/milter
mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
@@ -124,21 +173,29 @@ cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
%ifos aix4.1 %ifos aix4.1
%post %post
mkssys -s milter -p /var/log/milter/start.sh -u 25 -S -n 15 -f 9 -G mail || : mkssys -s milter -p %{libdir}/start.sh -u 25 -S -n 15 -f 9 -G mail || :
%preun %preun
if [ $1 = 0 ]; then if [ $1 = 0 ]; then
rmssys -s milter || : rmssys -s milter || :
fi fi
%else %else
%post %post -n milter
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf" #echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
/sbin/chkconfig --add milter /sbin/chkconfig --add milter
%preun %preun -n milter
if [ $1 = 0 ]; then if [ $1 = 0 ]; then
/sbin/chkconfig --del milter /sbin/chkconfig --del milter
fi fi
%post spf
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
/sbin/chkconfig --add spfmilter
%preun spf
if [ $1 = 0 ]; then
/sbin/chkconfig --del spfmilter
fi
%endif %endif
%clean %clean
@@ -146,7 +203,11 @@ rm -rf $RPM_BUILD_ROOT
%files -f INSTALLED_FILES %files -f INSTALLED_FILES
%defattr(-,root,root) %defattr(-,root,root)
%doc README HOWTO NEWS TODO CREDITS sample.py %doc README HOWTO ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%config %{libdir}/start.sh
%files -n milter
%defattr(-,root,root)
/etc/logrotate.d/milter /etc/logrotate.d/milter
/etc/cron.daily/milter /etc/cron.daily/milter
%ifos aix4.1 %ifos aix4.1
@@ -156,20 +217,81 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,mail,mail) %defattr(-,mail,mail)
%endif %endif
%dir /var/log/milter %dir /var/log/milter
%dir /var/run/milter
%dir /var/log/milter/save %dir /var/log/milter/save
%config /var/log/milter/start.sh %config %{libdir}/bms.py
%config /var/log/milter/bms.py %{libdir}/bms.py?
%config(noreplace) /var/log/milter/strike3.txt %config(noreplace) /var/log/milter/strike3.txt
%config(noreplace) /var/log/milter/softfail.txt %config(noreplace) /var/log/milter/softfail.txt
%config(noreplace) /var/log/milter/fail.txt
%config(noreplace) /var/log/milter/neutral.txt %config(noreplace) /var/log/milter/neutral.txt
%config(noreplace) /var/log/milter/quarantine.txt %config(noreplace) /var/log/milter/quarantine.txt
%config(noreplace) /var/log/milter/permerror.txt %config(noreplace) /var/log/milter/permerror.txt
%config(noreplace) /etc/mail/pymilter.cfg %config(noreplace) /etc/mail/pymilter.cfg
/usr/share/sendmail-cf/hack/rhsbl.m4 /usr/share/sendmail-cf/hack/rhsbl.m4
%files spf
%defattr(-,root,root)
%dir /var/log/milter
%{libdir}/spfmilter.py*
%config(noreplace) /etc/mail/spfmilter.cfg
/etc/rc.d/init.d/spfmilter
%changelog %changelog
* Fri Jul 15 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-1 * Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
- move AddrCache, parse_addr, iniplist to Milter package
- move parse_header to Milter.utils
- fix plock for missing source and can't change owner/group
- add sample spfmilter.py milter
- private_relay config option
- persist delayed DSN blacklisting
- handle gossip server restart without disabling gossip
- split out pymilter and pymilter-spf packages
- move milter apps to /usr/lib/pymilter
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
- More lame bounce heuristics
- SPF moved to pyspf RPM
- wiretap archive option
- Do plain CBV if missing template
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
- Support CBV timeout
- Support fail template, headers in templates
- Create GOSSiP record only when connection will procede to DATA.
- More SPF lax heuristics
- Don't require SPF pass for white/black listing mail from trusted relay.
- Support localpart wildcard for white and black lists.
* Thu Feb 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-1
- Delay reject of unsigned RCPT for postmaster and abuse only
- Fix dsn reporting of hard permerror
- Resolve FIXME for wrap_close in miltermodule.c
- Add Message-ID to DSNs
- Use signed Message-ID in delayed reject to blacklist senders
- Auto-train via blacklist and auto-whitelist
- Don't check userlist for signed MFROM
- Accept but skip DSPAM and training for whitelisted senders without SPF PASS
- Report GC stats
- Support CIDR matching for IP lists
- Support pysrs sign feature
- Support localpart specific SPF policy in access file
* Thu Dec 29 2005 Stuart Gathman <stuart@bmsi.com> 0.8.5-1
- Simple trusted_forwarder implementation.
- Fix access_file neutral policy
- Move Received-SPF header to beginning of headers
- Supply keyword info for all results in Received-SPF header.
- Move guessed SPF result to separate header
- Activate smfi_insheader only when SMFIR_INSHEADER defined
- Handle NULL MX in spf.py
- in-process GOSSiP server support (to be extended later)
- Expire CBV cache and renew auto-whitelist entries
* Fri Oct 21 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-2
- Don't supply sender when MFROM is subdomain of header from/sender.
- Don't send quarantine DSN for DSNs
- Skip dspam for replies/DSNs to signed MFROM
* Thu Oct 20 2005 Stuart Gathman <stuart@bmsi.com> 0.8.4-1
- Fix SPF policy via sendmail access map (case insensitive keys).
- Auto whitelist senders, train screener on whitelisted messages
- Optional idx parameter to addheader to invoke smfi_insheader
- Activate progress when SMFIR_PROGRESS defined
* Wed Oct 12 2005 Stuart Gathman <stuart@bmsi.com> 0.8.3-1
- Keep screened honeypot mail, but optionally discard honeypot only mail. - Keep screened honeypot mail, but optionally discard honeypot only mail.
- spf_accept_fail option for braindead SPF senders (treats fail like softfail) - spf_accept_fail option for braindead SPF senders (treats fail like softfail)
- Consider SMTP AUTH connections internal. - Consider SMTP AUTH connections internal.
+55 -37
View File
@@ -34,6 +34,18 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* 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 * Revision 1.5 2005/06/24 04:20:07 customdesigned
* Report context allocation error. * Report context allocation error.
* *
@@ -277,7 +289,7 @@ _find_context(PyObject *c) {
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)
@@ -285,23 +297,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;
@@ -535,13 +530,19 @@ milter_set_exception_policy(PyObject *self, PyObject *args) {
return NULL; 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);
switch (exception_policy) { switch (exception_policy) {
case SMFIS_REJECT: case SMFIS_REJECT:
smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure"); smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure");
@@ -552,7 +553,7 @@ static int _report_exception(milter_ContextObject *self) {
} }
return SMFIS_CONTINUE; return SMFIS_CONTINUE;
} }
PyEval_ReleaseThread(self->t); _release_thread(self->t);
return SMFIS_CONTINUE; return SMFIS_CONTINUE;
} }
@@ -571,7 +572,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;
} }
@@ -768,17 +769,23 @@ milter_wrap_close(SMFICTX *ctx) {
PyObject *cb = close_callback; PyObject *cb = close_callback;
milter_ContextObject *self = smfi_getpriv(ctx); milter_ContextObject *self = smfi_getpriv(ctx);
int r = SMFIS_CONTINUE; int r = SMFIS_CONTINUE;
if (self != NULL && cb != NULL && self->ctx == ctx) { if (self != NULL) {
PyObject *arglist; PyThreadState *t = self->t;
PyEval_AcquireThread(self->t); PyEval_AcquireThread(t);
arglist = Py_BuildValue("(O)", self); self->t = 0;
r = _generic_wrapper(self, cb, arglist); 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);
} }
/* FIXME: It is inefficient to have released the interp lock only to
acquire it again in _clear_context. We can tell _generic_return and
friends not to release the lock by, for instance, setting self->t to NULL.
However, first we make it work. */
_clear_context(ctx);
return r; return r;
} }
@@ -967,28 +974,39 @@ milter_setreply(PyObject *self, PyObject *args) {
} }
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
} }
static char milter_chgheader__doc__[] = static char milter_chgheader__doc__[] =
@@ -1143,7 +1161,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.";
@@ -1174,7 +1192,7 @@ static PyMethodDef context_methods[] = {
#ifdef SMFIF_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 #endif
{ NULL, NULL } { NULL, NULL }
+8 -3
View File
@@ -1,4 +1,7 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: SPF %(result)s (POSSIBLE FORGERY) Subject: SPF %(result)s (POSSIBLE FORGERY)
Auto-Submitted: auto-generated (sender verification)
This is an automatically generated Delivery Status Notification. This is an automatically generated Delivery Status Notification.
@@ -18,14 +21,16 @@ sent via an authorized SMTP server, but may still be legitimate. Since there
is no positive confirmation that the message is really from you, we have is no positive confirmation that the message is really from you, we have
to give it extra scrutiny - including verifying that the sender really to give it extra scrutiny - including verifying that the sender really
exists by sending you this DSN. We will remember this sender and not exists by sending you this DSN. We will remember this sender and not
bother you again for while. You can avoid this message entirely for bother you again for a while. You can avoid this message entirely for
legitimate mail by using an authorized SMTP server. Contact your mail legitimate mail by using an authorized SMTP server. Contact your mail
administrator and ask how to configure your email client to use an administrator and ask how to configure your email client to use an
authorized server. authorized server.
If you never sent the above message, then your domain has been forged. If you never sent the above message, then your domain has been forged.
Your mail admin needs to publish a strict SPF record so that I can reject Your mail admin needs to publish a strict SPF record so that I can reject
those forgeries instead of bugging you about them. those forgeries instead of bugging you about them.
See http://openspf.org for details.
If you need further assistance, please do not hesitate to contact me. If you need further assistance, please do not hesitate to contact me.
+4
View File
@@ -1,4 +1,7 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: Critical SPF configuration error Subject: Critical SPF configuration error
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification. This is an automatically generated Delivery Status Notification.
@@ -11,6 +14,7 @@ Delivery to the following recipients has been delayed.
%(rcpt)s %(rcpt)s
Subject: %(subject)s Subject: %(subject)s
Received-SPF: %(spf_result)s
Your spf record has a permanent error. The error was: Your spf record has a permanent error. The error was:
+16
View File
@@ -1,4 +1,7 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: DELIVERY STATUS (POSSIBLE SPAM) Subject: DELIVERY STATUS (POSSIBLE SPAM)
Auto-Submitted: auto-generated (content analysis)
This is an automatically generated Delivery Status Notification. This is an automatically generated Delivery Status Notification.
@@ -19,6 +22,19 @@ their quarantined mail and may notice your message. If your message is
important, please contact them via other means. You may also try sending important, please contact them via other means. You may also try sending
them a simple plain text message. them a simple plain text message.
If you never sent the above message, then your domain, %(sender_domain)s,
was forged - i.e. used without your knowlege or authorization by
someone attempting to steal your mail identity. This is a very
serious problem, and you need to provide authentication for your
SMTP (email) servers to prevent criminals from forging your
domain. The simplest step is usually to publish an SPF record
with your Sender Policy.
For more information, see: http://www.openspf.org
Your mail admin needs to publish a strict SPF record so that I can reject
those forgeries instead of bugging you with them.
If you need further assistance, please do not hesitate to contact me. If you need further assistance, please do not hesitate to contact me.
Kind regards, Kind regards,
+138
View File
@@ -0,0 +1,138 @@
# Analyze milter log to find abusers
import traceback
import sys
def parse_addr(a):
beg = a.find('<')
end = a.find('>')
if beg >= 0:
if end > beg: return a[beg+1:end]
return a
class Connection(object):
def __init__(self,dt,tm,id,ip=None,conn=None):
self.dt = dt
self.tm = tm
self.id = id
if ip:
_,self.host,self.ip = ip.split(None,2)
elif conn:
self.ip = conn.ip
self.host = conn.host
self.helo = conn.helo
self.subject = None
self.rcpt = []
self.mfrom = None
self.helo = None
self.innoc = []
self.whitelist = False
def connections(fp):
conndict = {}
termdict = {}
for line in fp:
if line.startswith('{'): continue
a = line.split(None,4)
if len(a) < 4: continue
dt,tm,id,op = a[:4]
if (id,op) == ('bms','milter'):
# FIXME: optionally yield all partial connections in conndict
conndict = {}
termdict = {}
continue
if id[0] == '[' and id[-1] == ']':
try:
key = int(id[1:-1])
except:
print >>sys.stderr,'bad id:',line.rstrip()
continue
else: continue
if op == 'connect':
ip = a[4].rstrip()
conn = Connection(dt,tm,id,ip=ip)
conndict[key] = conn
elif op in (
'DISCARD:','TAG:','CBV:','Large','No',
'NOTE:','From:','Sender:','TRAIN:'):
continue
else:
op = op.lower()
try:
conn = conndict[key]
except KeyError:
try:
conn = termdict[key]
del termdict[key]
conndict[key] = conn
except KeyError:
print >>sys.stderr,'key error:',line.rstrip()
continue
try:
if op == 'subject:':
if len(a) > 4:
conn.subject = a[4].rstrip()
elif op == 'innoc:':
conn.innoc.append(a[4].rstrip())
elif op == 'whitelist':
conn.whitelist = True
elif op == 'x-mailer:':
if len(a) > 4:
conn.mailer = a[4].rstrip()
elif op == 'x-guessed-spf:':
conn.spfguess = a[4]
elif op == 'received-spf:':
conn.spfres,conn.spfmsg = a[4].rstrip().split(None,1)
elif op == 'received:':
conn.received = a[4].rstrip()
elif op == 'temp':
_,conn.tempfile = a[4].rstrip().split(None,1)
elif op == 'srs':
_,conn.srsrcpt = a[4].rstrip().split(None,1)
elif op == 'mail':
_,conn.mfrom = a[4].rstrip().split(None,1)
elif op == 'rcpt':
_,rcpt = a[4].rstrip().split(None,1)
conn.rcpt.append(rcpt)
elif op == 'hello':
_,conn.helo = a[4].rstrip().split(None,1)
elif op in ('eom','dspam','abort'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('reject:','dspam:','tempfail:','reject','fail:','honeypot:'):
del conndict[key]
conn.enddt = dt
conn.endtm = tm
conn.result = op
conn.resmsg = a[4].rstrip()
yield conn
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
elif op in ('fp:','spam:'):
del conndict[key]
termdict[key] = Connection(conn.dt,conn.tm,conn.id,conn=conn)
else:
print >>sys.stderr,'unknown op:',line.rstrip()
except Exception:
print >>sys.stderr,'error:',line.rstrip()
traceback.print_exc()
if __name__ == '__main__':
import gzip
for fn in sys.argv[1:]:
if fn.endswith('.gz'):
fp = gzip.open(fn)
else:
fp = open(fn)
for conn in connections(fp):
if conn.rcpt and conn.mfrom:
for r in conn.rcpt:
if r.lower().find('iancarter') > 0: break
else:
if conn.mfrom.lower().find('iancarter') < 0: continue
print >>sys.stderr,conn.result,conn.dt,conn.tm,conn.id,conn.subject,parse_addr(conn.mfrom),
for a in conn.rcpt:
print parse_addr(a),
print
+7 -4
View File
@@ -1,9 +1,10 @@
import os import os
import sys import sys
from distutils.core import setup, Extension from distutils.core import setup, Extension
import Milter
# FIXME: on some versions of sendmail, smutil is renamed to sm # FIXME: on some versions of sendmail, smutil is renamed to sm
# on slackware and debian, leave it out entirely. It depends
# on how libmilter was built by the sendmail package.
libs = ["milter", "smutil"] libs = ["milter", "smutil"]
# patch distutils if it can't cope with the "classifiers" or # patch distutils if it can't cope with the "classifiers" or
@@ -13,13 +14,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 = Milter.__version__, # NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "pymilter", version = '0.8.8',
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",
@@ -27,11 +29,12 @@ 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=["mime","spf"], py_modules=["mime"],
packages = ['Milter'], packages = ['Milter'],
ext_modules=[ ext_modules=[
Extension("milter", ["miltermodule.c"], Extension("milter", ["miltermodule.c"],
libraries=libs, libraries=libs,
# set MAX_ML_REPLY to 1 for sendmail < 8.13
define_macros = [ ('MAX_ML_REPLY',32) ] define_macros = [ ('MAX_ML_REPLY',32) ]
), ),
], ],
+4 -1
View File
@@ -1,4 +1,7 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: SPF %(result)s (POSSIBLE FORGERY) Subject: SPF %(result)s (POSSIBLE FORGERY)
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification. This is an automatically generated Delivery Status Notification.
@@ -14,7 +17,7 @@ Subject: %(subject)s
Received-SPF: %(spf_result)s Received-SPF: %(spf_result)s
Your sender policy indicated that the above email was likely forged and that Your sender policy indicated that the above email was likely forged and that
feedback was desired. If you are sending from a foreign ISP, feedback was desired for debugging. If you are sending from a foreign ISP,
then you may need to follow your home ISPs instructions for configuring then you may need to follow your home ISPs instructions for configuring
your outgoing mail server. your outgoing mail server.
-1215
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[milter]
# The socket used to communicate with sendmail
socketname = /var/run/milter/spfmiltersock
# Name of the milter given to sendmail
name = pyspffilter
# Trusted relays such as secondary MXes that should not have SPF checked.
;trusted_relay =
# Internal networks that should not have SPF checked.
internal_connect = 127.0.0.1,192.168.0.0/16,10.0.0.0/8
# See http://www.openspf.com for more info on SPF.
[spf]
# Use sendmail access map or similar format for detailed spf policy.
# SPF entries in the access map will override defaults.
access_file = /etc/mail/access.db
# Connections that get an SPF pass for a pretend MAIL FROM of
# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM.
# This is for non-SRS forwarders. It is a simple implementation that
# is inefficient for more than a few entries.
;trusted_forwarder = careerbuilder.com
+253
View File
@@ -0,0 +1,253 @@
# A simple SPF milter.
# You must install pyspf for this to work.
# http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2007 Business Management Systems, Inc.
# This code is under GPL. See COPYING for details.
import sys
import Milter
import spf
import syslog
import anydbm
from Milter.config import MilterConfigParser
from Milter.utils import iniplist,parse_addr
syslog.openlog('spfmilter',0,syslog.LOG_MAIL)
class Config(object):
"Hold configuration options."
pass
def read_config(list):
"Return new config object."
cp = MilterConfigParser()
cp.read(list)
if cp.has_option('milter','datadir'):
os.chdir(cp.get('milter','datadir'))
conf = Config()
conf.socketname = cp.getdefault('milter','socketname', '/tmp/spfmiltersock')
conf.miltername = cp.getdefault('milter','name','pyspffilter')
conf.trusted_relay = cp.getlist('milter','trusted_relay')
conf.internal_connect = cp.getlist('milter','internal_connect')
conf.trusted_forwarder = cp.getlist('spf','trusted_relay')
conf.access_file = cp.getdefault('spf','access_file',None)
return conf
class SPFPolicy(object):
"Get SPF policy by result from sendmail style access file."
def __init__(self,sender,access_file=None):
self.sender = sender
self.domain = sender.split('@')[-1].lower()
if access_file:
try: acf = anydbm.open(access_file,'r')
except: acf = None
else: acf = None
self.acf = acf
def getPolicy(self,pfx):
acf = self.acf
if not acf: return None
try:
return acf[pfx + self.sender]
except KeyError:
try:
return acf[pfx + self.domain]
except KeyError:
try:
return acf[pfx]
except KeyError:
return None
class spfMilter(Milter.Milter):
"Milter to check SPF. Each connection gets its own instance."
def log(self,*msg):
syslog.syslog('[%d] %s' % (self.id,' '.join([str(m) for m in msg])))
def __init__(self):
self.mailfrom = None
self.id = Milter.uniqueID()
# we don't want config used to change during a connection
self.conf = config
# addheader can only be called from eom(). This accumulates added headers
# which can then be applied by alter_headers()
def add_header(self,name,val,idx=-1):
self.new_headers.append((name,val,idx))
self.log('%s: %s' % (name,val))
def connect(self,hostname,unused,hostaddr):
self.internal_connection = False
self.trusted_relay = False
self.hello_name = None
# sometimes people put extra space in sendmail config, so we strip
self.receiver = self.getsymval('j').strip()
if hostaddr and len(hostaddr) > 0:
ipaddr = hostaddr[0]
if iniplist(ipaddr,self.conf.internal_connect):
self.internal_connection = True
if iniplist(ipaddr,self.conf.trusted_relay):
self.trusted_relay = True
else: ipaddr = ''
self.connectip = ipaddr
if self.internal_connection:
connecttype = 'INTERNAL'
else:
connecttype = 'EXTERNAL'
if self.trusted_relay:
connecttype += ' TRUSTED'
self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype))
return Milter.CONTINUE
def hello(self,hostname):
self.hello_name = hostname
self.log("hello from %s" % hostname)
return Milter.CONTINUE
# multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message.
def envfrom(self,f,*str):
self.log("mail from",f,str)
if not self.hello_name:
self.log('REJECT: missing HELO')
self.setreply('550','5.7.1',"It's polite to say helo first.")
return Milter.REJECT
self.mailfrom = f
self.new_headers = []
t = parse_addr(f)
if len(t) == 2: t[1] = t[1].lower()
self.canon_from = '@'.join(t)
if not (self.internal_connection or self.trusted_relay) and self.connectip:
rc = self.check_spf()
if rc != Milter.CONTINUE: return rc
return Milter.CONTINUE
def envrcpt(self,f,*str):
return Milter.CONTINUE
def header(self,name,hval):
return Milter.CONTINUE
def eoh(self):
return Milter.CONTINUE
def eom(self):
for name,val,idx in self.new_headers:
try:
self.addheader(name,val,idx)
except:
self.addheader(name,val) # older sendmail can't insheader
return Milter.CONTINUE
def close(self):
return Milter.CONTINUE
def check_spf(self):
receiver = self.receiver
for tf in self.conf.trusted_forwarder:
q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False)
res,code,txt = q.check()
if res == 'pass':
self.log("TRUSTED_FORWARDER:",tf)
break
else:
q = spf.query(self.connectip,self.canon_from,self.hello_name,
receiver=receiver,strict=False)
q.set_default_explanation(
'SPF fail: see http://openspf.org/why.html?sender=%s&ip=%s' % (q.s,q.i))
res,code,txt = q.check()
if res not in ('pass','temperror'):
if self.mailfrom != '<>':
# check hello name via spf unless spf pass
h = spf.query(self.connectip,'',self.hello_name,receiver=receiver)
hres,hcode,htxt = h.check()
if hres in ('deny','fail','neutral','softfail'):
self.log('REJECT: hello SPF: %s 550 %s' % (hres,htxt))
self.setreply('550','5.7.1',htxt,
"The hostname given in your MTA's HELO response is not listed",
"as a legitimate MTA in the SPF records for your domain. If you",
"get this bounce, the message was not in fact a forgery, and you",
"should IMMEDIATELY notify your email administrator of the problem."
)
return Milter.REJECT
else:
hres,hcode,htxt = res,code,txt
else: hres = None
p = SPFPolicy(q.s,self.conf.access_file)
if res == 'fail':
policy = p.getPolicy('spf-fail:')
if not policy or policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT
if res == 'softfail':
policy = p.getPolicy('spf-softfail:')
if policy and policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'5.7.1',txt)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return Milter.REJECT
elif res == 'permerror':
policy = p.getPolicy('spf-permerror:')
if not policy or policy == 'REJECT':
self.log('REJECT: SPF %s %i %s' % (res,code,txt))
# latest SPF draft recommends 5.5.2 instead of 5.7.1
self.setreply(str(code),'5.5.2',txt,
'There is a fatal syntax error in the SPF record for %s' % q.o,
'We cannot accept mail from %s until this is corrected.' % q.o
)
return Milter.REJECT
elif res == 'temperror':
policy = p.getPolicy('spf-temperror:')
if not policy or policy == 'REJECT':
self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt))
self.setreply(str(code),'4.3.0',txt)
return Milter.TEMPFAIL
elif res == 'neutral' or res == 'none':
policy = p.getPolicy('spf-neutral:')
if policy and policy == 'REJECT':
self.log('REJECT NEUTRAL:',q.s)
self.setreply('550','5.7.1',
"%s requires and SPF PASS to accept mail from %s. [http://openspf.org]"
% (receiver,q.s))
return Milter.REJECT
elif res == 'pass':
policy = p.getPolicy('spf-pass:')
if policy and policy == 'REJECT':
self.log('REJECT PASS:',q.s)
self.setreply('550','5.7.1',
"%s has been blacklisted by %s." % (q.s,receiver))
return Milter.REJECT
self.add_header('Received-SPF',q.get_header(res,receiver),0)
if hres and q.h != q.o:
self.add_header('X-Hello-SPF',hres,0)
return Milter.CONTINUE
if __name__ == "__main__":
Milter.factory = spfMilter
Milter.set_flags(Milter.CHGHDRS + Milter.ADDHDRS)
global config
config = read_config(['spfmilter.cfg','/etc/mail/spfmilter.cfg'])
miltername = config.miltername
socketname = config.socketname
print """To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=%s
X%s, S=local:%s
See the sendmail README for libmilter.
sample spfmilter startup""" % (miltername,miltername,socketname)
sys.stdout.flush()
Milter.runmilter("pyspffilter",socketname,240)
print "sample spfmilter shutdown"
Executable
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
#
# spfmilter This shell script takes care of starting and stopping spfmilter.
#
# chkconfig: 2345 80 30
# description: a process that checks SPF for messages sent through sendmail.
# processname: spfmilter
# config: /etc/mail/spfmilter.cfg
# pidfile: /var/run/milter/spfmilter.pid
python="python2.4"
pidof() {
set - ""
if set - `ps -e -o pid,cmd | grep "${python} spfmilter.py"` &&
[ "$2" != "grep" ]; then
echo $1
return 0
fi
return 1
}
# Source function library.
. /etc/rc.d/init.d/functions
[ -x /usr/lib/pymilter/start.sh ] || exit 0
RETVAL=0
prog="spfmilter"
start() {
# Start daemons.
echo -n "Starting $prog: "
if ! test -d /var/run/milter; then
mkdir -p /var/run/milter
chown mail:mail /var/run/milter
fi
daemon --check milter --user mail /usr/lib/pymilter/start.sh spfmilter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/spfmilter
return $RETVAL
}
stop() {
# Stop daemons.
echo -n "Shutting down $prog: "
killproc milter
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/spfmilter
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
RETVAL=$?
;;
condrestart)
if [ -f /var/lock/subsys/spfmilter ]; then
stop
start
RETVAL=$?
fi
;;
status)
status spfmilter
RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
exit $RETVAL
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/python2.3
# Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2004 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details.
# $Log$
# Revision 1.1.1.1 2005/05/31 18:07:19 customdesigned
# Release 0.6.9
#
# 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
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 /usr/lib/pymilter
fi
cd /var/log/milter
exec >>${appname}.log 2>&1
${python} ${appname}.py &
echo $! >/var/run/milter/${appname}.pid
+4 -1
View File
@@ -1,4 +1,7 @@
To: %(sender)s
From: postmaster@%(receiver)s
Subject: Critical mail server configuration error Subject: Critical mail server configuration error
Auto-Submitted: auto-generated (configuration error)
This is an automatically generated Delivery Status Notification. This is an automatically generated Delivery Status Notification.
@@ -23,7 +26,7 @@ SMTP (email) servers to prevent criminals from forging your
domain. The simplest step is usually to publish an SPF record domain. The simplest step is usually to publish an SPF record
with your Sender Policy. with your Sender Policy.
For more information, see: http://openspf.com For more information, see: http://openspf.org
I hate to annoy you with a DSN (Delivery Status I hate to annoy you with a DSN (Delivery Status
Notification) from a possibly forged email, but since you Notification) from a possibly forged email, but since you
+2
View File
@@ -2,6 +2,7 @@ import unittest
import testbms import testbms
import testmime import testmime
import testsample import testsample
import testutils
import os import os
def suite(): def suite():
@@ -9,6 +10,7 @@ def suite():
s.addTest(testbms.suite()) 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__':
+20 -1
View File
@@ -238,7 +238,7 @@ class BMSMilterTestCase(unittest.TestCase):
milter = TestMilter() milter = TestMilter()
milter.connect('testSmartAlias') milter.connect('testSmartAlias')
# test smart alias feature # test smart alias feature
key = ('foo@bar.com','baz@bat.com') key = ('foo@example.com','baz@bat.com')
bms.smart_alias[key] = ['ham@eggs.com'] bms.smart_alias[key] = ['ham@eggs.com']
rc = milter.feedMsg('test8',key[0],key[1]) rc = milter.feedMsg('test8',key[0],key[1])
self.assertEqual(rc,Milter.ACCEPT) self.assertEqual(rc,Milter.ACCEPT)
@@ -277,6 +277,25 @@ class BMSMilterTestCase(unittest.TestCase):
fp = milter._body fp = milter._body
open("test/test1.tstout","w").write(fp.getvalue()) open("test/test1.tstout","w").write(fp.getvalue())
def testFindsrs(self):
if not bms.srs:
import SRS
bms.srs = SRS.new(secret='test')
sender = bms.srs.forward('foo@bar.com','mail.example.com')
sndr = bms.findsrs(StringIO.StringIO(
"""Received: from [1.16.33.86] (helo=mail.example.com)
by bastion4.mail.zen.co.uk with smtp (Exim 4.50) id 1H3IBC-00013b-O9
for foo@bar.com; Sat, 06 Jan 2007 20:30:17 +0000
X-Mailer: "PyMilter-0.8.5"
<%s> foo
MIME-Version: 1.0
Content-Type: text/plain
To: foo@bar.com
From: postmaster@mail.example.com
""" % sender
))
self.assertEqual(sndr,'foo@bar.com')
# def testReject(self): # def testReject(self):
# "Test content based spam rejection." # "Test content based spam rejection."
# milter = TestMilter() # milter = TestMilter()
+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())