From 30f4c27c4588d8a8a78c5562b40c5bf62df4d009 Mon Sep 17 00:00:00 2001 From: Stuart Gathman Date: Sat, 13 Dec 2008 20:29:56 +0000 Subject: [PATCH] Split off milter applications. --- MANIFEST.in | 16 +- ban2zone.py | 14 - bms.py | 2029 -------------------------------------------- fail.txt | 35 - milter-template.py | 19 +- milter.cfg | 238 ------ milter.rc | 109 --- milter.rc7 | 85 -- miltermodule.c | 11 +- neutral.txt | 39 - permerror.txt | 35 - pymilter.spec | 370 +------- quarantine.txt | 29 - softfail.txt | 28 - spfmilter.cfg | 20 - spfmilter.py | 253 ------ spfmilter.rc | 85 -- strike3.txt | 69 -- temperror.txt | 33 - test.py | 2 - testbms.py | 323 ------- 21 files changed, 12 insertions(+), 3830 deletions(-) delete mode 100644 ban2zone.py delete mode 100644 bms.py delete mode 100644 fail.txt delete mode 100644 milter.cfg delete mode 100755 milter.rc delete mode 100755 milter.rc7 delete mode 100644 neutral.txt delete mode 100644 permerror.txt delete mode 100644 quarantine.txt delete mode 100644 softfail.txt delete mode 100644 spfmilter.cfg delete mode 100644 spfmilter.py delete mode 100755 spfmilter.rc delete mode 100644 strike3.txt delete mode 100644 temperror.txt delete mode 100644 testbms.py diff --git a/MANIFEST.in b/MANIFEST.in index 2b534c7..150f25a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,28 +9,14 @@ include MANIFEST.in include testsample.py include testmime.py include testutils.py -include testbms.py include rejects.py include report.py -include bms.py -include spf.py -include cid2spf.py -include spfquery.py -include ban2zone.py include test.py include sample.py include milter-template.py -include spfmilter.py -include spfmilter.rc -include spfmilter.cfg include test/* include doc/* include Milter/*.py include *.spec -include start.sh -include milter.rc -include milter.rc7 -include milter.cfg -include rhsbl.m4 -include *.txt include *.html +include start.sh diff --git a/ban2zone.py b/ban2zone.py deleted file mode 100644 index 492af13..0000000 --- a/ban2zone.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/python2.4 - -import socket -import sys - -banned_ips = set(socket.inet_aton(ip) - for fn in sys.argv[1:] - for ip in open(fn)) -banned_ips = list(banned_ips) -banned_ips.sort() -for ip in banned_ips: - a = socket.inet_ntoa(ip).split('.') - a.reverse() - print "%s\tIN A 127.0.0.2"%('.'.join(a)) diff --git a/bms.py b/bms.py deleted file mode 100644 index bab71d7..0000000 --- a/bms.py +++ /dev/null @@ -1,2029 +0,0 @@ -#!/usr/bin/env python -# A simple milter that has grown quite a bit. -# $Log$ -# Revision 1.136 2008/12/04 19:42:46 customdesigned -# SPF Pass policy -# -# Revision 1.135 2008/10/23 19:58:06 customdesigned -# Example config had different names than actual code :-) -# -# Revision 1.134 2008/10/11 15:45:46 customdesigned -# Don't greylist DSNs. -# -# Revision 1.133 2008/10/09 18:44:54 customdesigned -# Skip greylisting for good reputation. -# -# Revision 1.132 2008/10/09 00:55:13 customdesigned -# Don't reset greylist timer on early retries. -# -# Revision 1.131 2008/10/08 04:57:28 customdesigned -# Greylisting -# -# Revision 1.130 2008/10/02 03:19:00 customdesigned -# Delay strike3 REJECT and don't reject if whitelisted. -# Recognize vacation messages as autoreplies. -# -# Revision 1.129 2008/09/09 23:24:56 customdesigned -# Never ban a trusted relay. -# -# Revision 1.128 2008/09/09 23:08:16 customdesigned -# Wasn't reading banned_ips -# -# Revision 1.127 2008/08/25 18:32:22 customdesigned -# Handle missing gossip_node so self tests pass. -# -# Revision 1.126 2008/08/18 17:47:57 customdesigned -# Log rcpt for SRS rejections. -# -# Revision 1.125 2008/08/06 00:52:38 customdesigned -# CBV policy sends no DSN. DSN policy sends DSN. -# -# Revision 1.124 2008/08/05 18:04:06 customdesigned -# Send quarantine DSN to SPF PASS only. -# -# Revision 1.123 2008/07/29 21:59:29 customdesigned -# Parse ESMTP params -# -# Revision 1.122 2008/05/08 21:35:56 customdesigned -# Allow explicitly whitelisted email from banned_users. -# -# Revision 1.121 2008/04/10 14:59:35 customdesigned -# Configure gossip TTL. -# -# Revision 1.120 2008/04/02 18:59:14 customdesigned -# Release 0.8.10 -# -# Revision 1.119 2008/04/01 00:13:10 customdesigned -# Do not CBV whitelisted addresses. We already know they are good. -# -# Revision 1.118 2008/01/09 20:15:49 customdesigned -# Handle unquoted fullname when parsing email. -# -# Revision 1.117 2007/11/29 14:35:17 customdesigned -# Packaging tweaks. -# -# Revision 1.116 2007/11/01 20:09:14 customdesigned -# Support temperror policy in access. -# -# Revision 1.115 2007/10/10 18:23:54 customdesigned -# Send quarantine DSN to SPF pass (official or guessed) only. -# Reject blacklisted email too big for dspam. -# -# Revision 1.114 2007/10/10 18:07:50 customdesigned -# Check porn keywords in From header field. -# -# Revision 1.113 2007/09/25 16:37:26 customdesigned -# Tested on RH7 -# -# Revision 1.112 2007/09/13 14:51:03 customdesigned -# Report domain on reputation reject. -# -# Revision 1.111 2007/07/25 17:14:59 customdesigned -# Move milter apps to /usr/lib/pymilter -# -# Revision 1.110 2007/07/02 03:06:10 customdesigned -# Ban ips on bad mailfrom offenses as well as bad rcpts. -# -# Revision 1.109 2007/06/23 20:53:05 customdesigned -# Ban IPs based on too many invalid recipients in a connection. Requires -# configuring check_user. Tighten HELO best_guess policy. -# -# Revision 1.108 2007/04/19 16:02:43 customdesigned -# Do not process valid SRS recipients as delayed_failure. -# -# Revision 1.107 2007/04/15 01:01:13 customdesigned -# Ban ips with too many bad rcpts on a connection. -# -# Revision 1.105 2007/04/13 17:20:09 customdesigned -# Check access_file at startup. Compress rcpt to log. -# -# Revision 1.104 2007/04/05 17:59:07 customdesigned -# Stop querying gossip server twice. -# -# Revision 1.103 2007/04/02 18:37:25 customdesigned -# Don't disable gossip for temporary error. -# -# Revision 1.102 2007/03/30 18:13:41 customdesigned -# Report bestguess and helo-spf as key-value pairs in Received-SPF -# instead of in their own headers. -# -# Revision 1.101 2007/03/29 03:06:10 customdesigned -# Don't count DSN and unqualified MAIL FROM as internal_domain. -# -# Revision 1.100 2007/03/24 00:30:24 customdesigned -# Do not CBV for internal domains. -# -# Revision 1.99 2007/03/23 22:39:10 customdesigned -# Get SMTP-Auth policy from access_file. -# -# Revision 1.98 2007/03/21 04:02:13 customdesigned -# Properly log From: and Sender: -# -# Revision 1.97 2007/03/18 02:32:21 customdesigned -# Gossip configuration options: client or standalone with optional peers. -# -# Revision 1.96 2007/03/17 21:22:48 customdesigned -# New delayed DSN pattern. Retab (expandtab). -# -# Revision 1.95 2007/03/03 19:18:57 customdesigned -# Fix continuing findsrs when srs.reverse fails. -# -# Revision 1.94 2007/03/03 18:46:26 customdesigned -# Improve delayed failure detection. -# -# Revision 1.93 2007/02/07 23:21:26 customdesigned -# Use re for auto-reply recognition. -# -# Revision 1.92 2007/01/26 03:47:23 customdesigned -# Handle null in header value. -# -# Revision 1.91 2007/01/25 22:47:25 customdesigned -# Persist blacklisting from delayed DSNs. -# -# Revision 1.90 2007/01/23 19:46:20 customdesigned -# Add private relay. -# -# Revision 1.89 2007/01/22 02:46:01 customdesigned -# Convert tabs to spaces. -# -# Revision 1.88 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.87 2007/01/18 16:48:44 customdesigned -# Doc update. -# Parse From header for delayed failure detection. -# Don't check reputation of trusted host. -# Track IP reputation only when missing PTR. -# -# Revision 1.86 2007/01/16 05:17:29 customdesigned -# REJECT after data for blacklisted emails - so in case of mistakes, a -# legitimate sender will know what happened. -# -# Revision 1.85 2007/01/11 04:31:26 customdesigned -# Negative feedback for bad headers. Purge cache logs on startup. -# -# Revision 1.84 2007/01/10 04:44:25 customdesigned -# Documentation updates. -# -# Revision 1.83 2007/01/08 23:20:54 customdesigned -# Get user feedback. -# -# Revision 1.82 2007/01/06 04:21:30 customdesigned -# Add config file to spfmilter -# -# Revision 1.81 2007/01/05 23:33:55 customdesigned -# Make blacklist an AddrCache -# -# Revision 1.80 2007/01/05 23:12:12 customdesigned -# Move parse_addr, iniplist, ip4re to Milter.utils -# -# Revision 1.79 2007/01/05 21:25:40 customdesigned -# Move AddrCache to Milter package. -# -# Revision 1.78 2007/01/04 18:01:10 customdesigned -# Do plain CBV when template missing. -# -# Revision 1.77 2006/12/31 03:07:20 customdesigned -# Use HELO identity if good when MAILFROM is bad. -# -# Revision 1.76 2006/12/30 18:58:53 customdesigned -# Skip reputation/whitelist/blacklist when rejecting on SPF. Add X-Hello-SPF. -# -# Revision 1.75 2006/12/28 01:54:32 customdesigned -# Reject on bad_reputation or blacklist and nodspam. Match valid helo like -# PTR for guessed SPF pass. -# -# Revision 1.74 2006/12/19 00:59:30 customdesigned -# Add archive option to wiretap. -# -# Revision 1.73 2006/12/04 18:47:03 customdesigned -# Reject multiple recipients to DSN. -# Auto-disable gossip on DB error. -# -# Revision 1.72 2006/11/22 16:31:22 customdesigned -# SRS domains were missing srs_reject check when SES was active. -# -# Revision 1.71 2006/11/22 01:03:28 customdesigned -# Replace last use of deprecated rfc822 module. -# -# Revision 1.70 2006/11/21 18:45:49 customdesigned -# Update a use of deprecated rfc822. Recognize report-type=delivery-status -# -# See ChangeLog -# -# Author: Stuart D. Gathman -# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. -# This code is under the GNU General Public License. See COPYING for details. - -import sys -import os -import StringIO -import mime -import email.Errors -import Milter -import tempfile -import time -import socket -import re -import shutil -import gc -import anydbm -import Milter.dsn as dsn -from Milter.dynip import is_dynip as dynip -from Milter.utils import \ - iniplist,parse_addr,parse_header,ip4re,addr2bin,parseaddr -from Milter.config import MilterConfigParser -from Milter.greylist import Greylist - -from fnmatch import fnmatchcase -from email.Utils import getaddresses - -# Import gossip if available -try: - import gossip - import gossip.client - import gossip.server - gossip_node = None -except: gossip = None - -# Import pysrs if available -try: - import SRS - srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE) -except: SRS = None -try: - import SES -except: SES = None - -# Import spf if available -try: import spf -except: spf = None - -# Sometimes, MTAs reply to our DSN. We recognize this type of reply/DSN -# and check for the original recipient SRS encoded in Message-ID. -# If found, we blacklist that recipient. -_subjpats = ( - r'^failure notice', - r'^subjectbounce', - r'^returned mail', - r'^undeliver', - r'\bdelivery\b.*\bfail', - r'\bdelivery problem', - r'\bnot\s+be\s+delivered', - r'\buser unknown\b', - r'^failed', r'^mail failed', - r'^echec de distribution', - r'\berror\s+sending\b', - r'^fallo en la entrega', - r'\bfehlgeschlagen\b' -) -refaildsn = re.compile('|'.join(_subjpats),re.IGNORECASE) - -# We don't want to whitelist recipients of Autoreplys and other robots. -# There doesn't seem to be a foolproof way to recognize these, so -# we use this heuristic. The worst that can happen is someone won't get -# whitelisted when they should, or we'll whitelist some spammer for a while. -_autopats = ( - r'^read:', - r'\bautoreply:\b', - r'^return receipt', - r'^Your message\b.*\bawaits moderator approval' -) -reautoreply = re.compile('|'.join(_autopats),re.IGNORECASE) -import logging - -# Thanks to Chris Liechti for config parsing suggestions - -# Global configuration defaults suitable for test framework. -socketname = "/tmp/pythonsock" -reject_virus_from = () -wiretap_users = {} -discard_users = {} -wiretap_dest = None -mail_archive = None -_archive_lock = None -blind_wiretap = True -check_user = {} -block_forward = {} -hide_path = () -log_headers = False -block_chinese = False -case_sensitive_localpart = False -spam_words = () -porn_words = () -banned_exts = mime.extlist.split(',') -scan_zip = False -scan_html = True -scan_rfc822 = True -internal_connect = () -trusted_relay = () -private_relay = () -trusted_forwarder = () -internal_domains = () -banned_users = () -hello_blacklist = () -smart_alias = {} -dspam_dict = None -dspam_users = {} -dspam_train = {} -dspam_userdir = None -dspam_exempt = {} -dspam_whitelist = {} -whitelist_senders = {} -dspam_screener = () -dspam_internal = True # True if internal mail should be dspammed -dspam_reject = () -dspam_sizelimit = 180000 -srs = None -ses = None -srs_reject_spoofed = False -srs_domain = None -spf_reject_neutral = () -spf_accept_softfail = () -spf_accept_fail = () -spf_best_guess = False -spf_reject_noptr = False -supply_sender = False -access_file = None -timeout = 600 -banned_ips = set() -greylist = None - -logging.basicConfig( - stream=sys.stdout, - level=logging.INFO, - format='%(asctime)s %(message)s', - datefmt='%Y%b%d %H:%M:%S' -) -milter_log = logging.getLogger('milter') - -def read_config(list): - cp = MilterConfigParser({ - 'tempdir': "/var/log/milter/save", - 'datadir': "/var/log/milter", - 'socket': "/var/run/milter/pythonsock", - 'scan_html': 'no', - 'scan_rfc822': 'yes', - 'scan_zip': 'no', - 'block_chinese': 'no', - 'log_headers': 'no', - 'blind_wiretap': 'yes', - 'reject_spoofed': 'no', - 'reject_noptr': 'no', - 'supply_sender': 'no', - 'best_guess': 'no', - 'dspam_internal': 'yes', - 'case_sensitive_localpart': 'no' - }) - cp.read(list) - if cp.has_option('milter','datadir'): - print "chdir:",cp.get('milter','datadir') - os.chdir(cp.get('milter','datadir')) - - # milter section - tempfile.tempdir = cp.get('milter','tempdir') - global socketname, timeout, check_user, log_headers - global internal_connect, internal_domains, trusted_relay, hello_blacklist - global case_sensitive_localpart, private_relay - socketname = cp.get('milter','socket') - timeout = cp.getintdefault('milter','timeout',600) - check_user = cp.getaddrset('milter','check_user') - log_headers = cp.getboolean('milter','log_headers') - internal_connect = cp.getlist('milter','internal_connect') - internal_domains = cp.getlist('milter','internal_domains') - trusted_relay = cp.getlist('milter','trusted_relay') - private_relay = cp.getlist('milter','private_relay') - hello_blacklist = cp.getlist('milter','hello_blacklist') - case_sensitive_localpart = cp.getboolean('milter','case_sensitive_localpart') - - # defang section - global scan_rfc822, scan_html, block_chinese, scan_zip, block_forward - global banned_exts, porn_words, spam_words - if cp.has_section('defang'): - section = 'defang' - # for backward compatibility, - # banned extensions defaults to empty only when defang section exists - banned_exts = cp.getlist(section,'banned_exts') - else: # use milter section if no defang section for compatibility - section = 'milter' - scan_rfc822 = cp.getboolean(section,'scan_rfc822') - scan_zip = cp.getboolean(section,'scan_zip') - scan_html = cp.getboolean(section,'scan_html') - block_chinese = cp.getboolean(section,'block_chinese') - block_forward = cp.getaddrset(section,'block_forward') - porn_words = cp.getlist(section,'porn_words') - spam_words = cp.getlist(section,'spam_words') - - # scrub section - global hide_path, reject_virus_from - hide_path = cp.getlist('scrub','hide_path') - reject_virus_from = cp.getlist('scrub','reject_virus_from') - - # wiretap section - global blind_wiretap,wiretap_users,wiretap_dest,discard_users,mail_archive - blind_wiretap = cp.getboolean('wiretap','blind') - wiretap_users = cp.getaddrset('wiretap','users') - discard_users = cp.getaddrset('wiretap','discard') - wiretap_dest = cp.getdefault('wiretap','dest') - if wiretap_dest: wiretap_dest = '<%s>' % wiretap_dest - mail_archive = cp.getdefault('wiretap','archive') - - global smart_alias - for sa,v in [ - (k,cp.get('wiretap',k)) for k in cp.getlist('wiretap','smart_alias') - ] + (cp.has_section('smart_alias') and cp.items('smart_alias',True) or []): - print sa,v - sm = [q.strip() for q in v.split(',')] - if len(sm) < 2: - milter_log.warning('malformed smart alias: %s',sa) - continue - if len(sm) == 2: sm.append(sa) - if case_sensitive_localpart: - key = (sm[0],sm[1]) - else: - key = (sm[0].lower(),sm[1].lower()) - smart_alias[key] = sm[2:] - - # dspam section - global dspam_dict, dspam_users, dspam_userdir, dspam_exempt, dspam_internal - global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit - global whitelist_senders - whitelist_senders = cp.getaddrset('dspam','whitelist_senders') - dspam_dict = cp.getdefault('dspam','dspam_dict') - dspam_exempt = cp.getaddrset('dspam','dspam_exempt') - dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist') - dspam_users = cp.getaddrdict('dspam','dspam_users') - dspam_userdir = cp.getdefault('dspam','dspam_userdir') - dspam_screener = cp.getlist('dspam','dspam_screener') - dspam_train = set(cp.getlist('dspam','dspam_train')) - dspam_reject = cp.getlist('dspam','dspam_reject') - dspam_internal = cp.getboolean('dspam','dspam_internal') - if cp.has_option('dspam','dspam_sizelimit'): - dspam_sizelimit = cp.getint('dspam','dspam_sizelimit') - - # spf section - global spf_reject_neutral,spf_best_guess,SRS,spf_reject_noptr - global spf_accept_softfail,spf_accept_fail,supply_sender,access_file - global trusted_forwarder - if spf: - spf.DELEGATE = cp.getdefault('spf','delegate') - spf_reject_neutral = cp.getlist('spf','reject_neutral') - spf_accept_softfail = cp.getlist('spf','accept_softfail') - spf_accept_fail = cp.getlist('spf','accept_fail') - spf_best_guess = cp.getboolean('spf','best_guess') - spf_reject_noptr = cp.getboolean('spf','reject_noptr') - supply_sender = cp.getboolean('spf','supply_sender') - access_file = cp.getdefault('spf','access_file') - trusted_forwarder = cp.getlist('spf','trusted_forwarder') - srs_config = cp.getdefault('srs','config') - if srs_config: cp.read([srs_config]) - srs_secret = cp.getdefault('srs','secret') - if SRS and srs_secret: - global ses,srs,srs_reject_spoofed,srs_domain,banned_users - database = cp.getdefault('srs','database') - srs_reject_spoofed = cp.getboolean('srs','reject_spoofed') - maxage = cp.getintdefault('srs','maxage',8) - hashlength = cp.getintdefault('srs','hashlength',8) - separator = cp.getdefault('srs','separator','=') - if database: - import SRS.DB - srs = SRS.DB.DB(database=database,secret=srs_secret, - maxage=maxage,hashlength=hashlength,separator=separator) - else: - srs = SRS.Guarded.Guarded(secret=srs_secret, - maxage=maxage,hashlength=hashlength,separator=separator) - if SES: - ses = SES.new(secret=srs_secret,expiration=maxage) - srs_domain = set(cp.getlist('srs','ses')) - srs_domain.update(cp.getlist('srs','srs')) - else: - srs_domain = set(cp.getlist('srs','srs')) - srs_domain.update(cp.getlist('srs','sign')) - srs_domain.add(cp.getdefault('srs','fwdomain')) - banned_users = cp.getlist('srs','banned_users') - - if gossip: - global gossip_node, gossip_ttl - if cp.has_option('gossip','server'): - server = cp.get('gossip','server') - host,port = gossip.splitaddr(server) - gossip_node = gossip.client.Gossip(host,port) - else: - gossip_node = gossip.server.Gossip('gossip4.db',1000) - for p in cp.getlist('gossip','peers'): - host,port = gossip.splitaddr(p) - gossip_node.peers.append(gossip.server.Peer(host,port)) - gossip_ttl = cp.getintdefault('gossip','ttl',1) - - # greylist section - if cp.has_option('greylist','dbfile'): - global greylist - grey_db = cp.getdefault('greylist','dbfile') - grey_days = cp.getintdefault('greylist','retain',36) - grey_expire = cp.getintdefault('greylist','expire',6) - grey_time = cp.getintdefault('greylist','time',5) - greylist = Greylist(grey_db,grey_time,grey_expire,grey_days) - -def findsrs(fp): - lastln = None - for ln in fp: - if lastln: - if ln[0].isspace() and ln[0] != '\n': - lastln += ln - continue - try: - name,val = lastln.rstrip().split(None,1) - pos = val.find('= 0: - end = val.find('>',pos+4) - return srs.reverse(val[pos+1:end]) - except: pass - lnl = ln.lower() - if lnl.startswith('action:'): - if lnl.split()[-1] != 'failed': break - for k in ('message-id:','x-mailer:','sender:','references:'): - if lnl.startswith(k): - lastln = ln - break - -def param2dict(str): - pairs = [x.split('=',1) for x in str] - for e in pairs: - if len(e) < 2: e.append(None) - return dict([(k.upper(),v) for k,v in pairs]) - -class SPFPolicy(object): - "Get SPF policy by result from sendmail style access file." - def __init__(self,sender): - 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 close(self): - if self.acf: - self.acf.close() - - 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 - - def getFailPolicy(self): - policy = self.getPolicy('spf-fail:') - if not policy: - if self.domain in spf_accept_fail: - policy = 'CBV' - else: - policy = 'REJECT' - return policy - - def getNonePolicy(self): - policy = self.getPolicy('spf-none:') - if not policy: - if spf_reject_noptr: - policy = 'REJECT' - else: - policy = 'CBV' - return policy - - def getSoftfailPolicy(self): - policy = self.getPolicy('spf-softfail:') - if not policy: - if self.domain in spf_accept_softfail: - policy = 'OK' - elif self.domain in spf_reject_neutral: - policy = 'REJECT' - else: - policy = 'CBV' - return policy - - def getNeutralPolicy(self): - policy = self.getPolicy('spf-neutral:') - if not policy: - if self.domain in spf_reject_neutral: - policy = 'REJECT' - policy = 'OK' - return policy - - def getPermErrorPolicy(self): - policy = self.getPolicy('spf-permerror:') - if not policy: - policy = 'REJECT' - return policy - - def getTempErrorPolicy(self): - policy = self.getPolicy('spf-temperror:') - if not policy: - policy = 'REJECT' - return policy - - def getPassPolicy(self): - policy = self.getPolicy('spf-pass:') - if not policy: - policy = 'OK' - return policy - -from Milter.cache import AddrCache - -cbv_cache = AddrCache(renew=7) -auto_whitelist = AddrCache(renew=60) -blacklist = AddrCache(renew=30) - -class bmsMilter(Milter.Milter): - """Milter to replace attachments poisonous to Windows with a WARNING message, - check SPF, and other anti-forgery features, and implement wiretapping - and smart alias redirection.""" - - def log(self,*msg): - milter_log.info('[%d] %s',self.id,' '.join([str(m) for m in msg])) - - def __init__(self): - self.tempname = None - self.mailfrom = None # sender in SMTP form - self.canon_from = None # sender in end user form - self.fp = None - self.bodysize = 0 - self.id = Milter.uniqueID() - - # delrcpt can only be called from eom(). This accumulates recipient - # changes which can then be applied by alter_recipients() - def del_recipient(self,rcpt): - rcpt = rcpt.lower() - if not rcpt in self.discard_list: - self.discard_list.append(rcpt) - - # addrcpt can only be called from eom(). This accumulates recipient - # changes which can then be applied by alter_recipients() - def add_recipient(self,rcpt): - rcpt = rcpt.lower() - if not rcpt in self.redirect_list: - self.redirect_list.append(rcpt) - - # addheader can only be called from eom(). This accumulates added headers - # which can then be applied by alter_headers() - def add_header(self,name,val,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 - # 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,internal_connect): - self.internal_connection = True - if iniplist(ipaddr,trusted_relay): - self.trusted_relay = True - else: ipaddr = '' - self.connectip = ipaddr - self.missing_ptr = dynip(hostname,self.connectip) - if self.internal_connection: - connecttype = 'INTERNAL' - else: - connecttype = 'EXTERNAL' - if self.trusted_relay: - connecttype += ' TRUSTED' - if self.missing_ptr: - connecttype += ' DYN' - self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype)) - if addr2bin(ipaddr) in banned_ips: - self.log("REJECT: BANNED IP") - self.setreply('550','5.7.1', 'Banned for dictionary attacks') - return Milter.REJECT - self.hello_name = None - self.connecthost = hostname - if hostname == 'localhost' and not ipaddr.startswith('127.') \ - or hostname == '.': - self.log("REJECT: PTR is",hostname) - self.setreply('550','5.7.1', '"%s" is not a reasonable PTR name'%hostname) - return Milter.REJECT - self.offenses = 0 - return Milter.CONTINUE - - def hello(self,hostname): - self.hello_name = hostname - self.log("hello from %s" % hostname) - if ip4re.match(hostname): - self.log("REJECT: numeric hello name:",hostname) - self.setreply('550','5.7.1','hello name cannot be numeric ip') - return Milter.REJECT - if not self.internal_connection and hostname in hello_blacklist: - self.log("REJECT: spam from self:",hostname) - self.setreply('550','5.7.1', - 'Your mail server lies. Its name is *not* %s.' % hostname) - return Milter.REJECT - if hostname == 'GC': - n = gc.collect() - self.log("gc:",n,' unreachable objects') - self.log("auto-whitelist:",len(auto_whitelist),' entries') - self.log("cbv_cache:",len(cbv_cache),' entries') - self.setreply('550','5.7.1','%d unreachable objects'%n) - return Milter.REJECT - # HELO not allowed after MAIL FROM - if self.mailfrom: self.offense(inc=2) - return Milter.CONTINUE - - def smart_alias(self,to): - if smart_alias: - if case_sensitive_localpart: - t = parse_addr(to) - else: - t = parse_addr(to.lower()) - if len(t) == 2: - ct = '@'.join(t) - else: - ct = t[0] - if case_sensitive_localpart: - cf = self.canon_from - else: - cf = self.canon_from.lower() - cf0 = cf.split('@',1) - if len(cf0) == 2: - cf0 = '@' + cf0[1] - else: - cf0 = cf - for key in ((cf,ct),(cf0,ct)): - if smart_alias.has_key(key): - self.del_recipient(to) - for t in smart_alias[key]: - self.add_recipient('<%s>'%t) - - def offense(self,inc=1): - self.offenses += inc - if self.offenses > 3 and not self.trusted_relay: - try: - ip = addr2bin(self.connectip) - if ip not in banned_ips: - banned_ips.add(ip) - print >>open('banned_ips','a'),self.connectip - self.log("BANNED IP:",self.connectip) - except: pass - return Milter.REJECT - - # 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) - #param = param2dict(str) - #self.envid = param.get('ENVID',None) - #self.mail_param = param - self.fp = StringIO.StringIO() - self.tempname = None - self.mailfrom = f - self.forward = True - self.bodysize = 0 - self.hidepath = False - self.discard = False - self.dspam = True - self.whitelist = False - self.blacklist = False - self.greylist = False - self.reject_spam = True - self.data_allowed = True - self.delayed_failure = None - self.trust_received = self.trusted_relay - self.trust_spf = self.trusted_relay - self.redirect_list = [] - self.discard_list = [] - self.new_headers = [] - self.recipients = [] - self.cbv_needed = None - self.whitelist_sender = False - self.postmaster_reply = False - t = parse_addr(f) - if len(t) == 2: t[1] = t[1].lower() - self.canon_from = '@'.join(t) - # Some braindead MTAs can't be relied upon to properly flag DSNs. - # This heuristic tries to recognize such. - self.is_bounce = (f == '<>' or t[0].lower() in banned_users - #and t[1] == self.hello_name - ) - - # Check SMTP AUTH, also available: - # auth_authen authenticated user - # auth_author (ESMTP AUTH= param) - # auth_ssf (connection security, 0 = unencrypted) - # auth_type (authentication method, CRAM-MD5, DIGEST-MD5, PLAIN, etc) - # cipher_bits SSL encryption strength - # cert_subject SSL cert subject - # verify SSL cert verified - - self.user = self.getsymval('{auth_authen}') - if self.user: - # Very simple SMTP AUTH policy by default: - # any successful authentication is considered INTERNAL - # Detailed authorization policy is configured in the access file below. - self.internal_connection = True - self.log( - "SMTP AUTH:",self.user, self.getsymval('{auth_type}'), - "sslbits =",self.getsymval('{cipher_bits}'), - "ssf =",self.getsymval('{auth_ssf}'), "INTERNAL" - ) - if self.getsymval('{verify}'): - self.log("SSL AUTH:", - self.getsymval('{cert_subject}'), - "verify =",self.getsymval('{verify}') - ) - - self.fp.write('From %s %s\n' % (self.canon_from,time.ctime())) - self.internal_domain = False - if len(t) == 2: - user,domain = t - for pat in internal_domains: - if fnmatchcase(domain,pat): - self.internal_domain = True - break - if self.internal_connection: - if self.user: - p = SPFPolicy('%s@%s'%(self.user,domain)) - policy = p.getPolicy('smtp-auth:') - p.close() - else: - policy = None - if policy: - if policy != 'OK': - self.log("REJECT: unauthorized user",self.user, - "at",self.connectip,"sending MAIL FROM",self.canon_from) - self.setreply('550','5.7.1', - 'SMTP user %s is not authorized to use MAIL FROM %s.' % - (self.user,self.canon_from) - ) - return Milter.REJECT - elif internal_domains and not self.internal_domain: - self.log("REJECT: zombie PC at ",self.connectip, - " sending MAIL FROM ",self.canon_from) - self.setreply('550','5.7.1', - 'Your PC is using an unauthorized MAIL FROM.', - 'It is either badly misconfigured or controlled by organized crime.' - ) - return Milter.REJECT - wl_users = whitelist_senders.get(domain,()) - if user in wl_users or '' in wl_users: - self.whitelist_sender = True - - self.rejectvirus = domain in reject_virus_from - if user in wiretap_users.get(domain,()): - self.add_recipient(wiretap_dest) - self.smart_alias(wiretap_dest) - if user in discard_users.get(domain,()): - self.discard = True - exempt_users = dspam_whitelist.get(domain,()) - if user in exempt_users or '' in exempt_users: - self.dspam = False - else: - self.rejectvirus = False - domain = None - 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.umis = None - self.spf = None - self.policy = None - if not (self.internal_connection or self.trusted_relay) \ - and self.connectip and spf: - rc = self.check_spf() - if rc != Milter.CONTINUE: - if rc != Milter.TEMPFAIL: self.offense() - return rc - self.greylist = True - else: - rc = Milter.CONTINUE - # FIXME: parse Received-SPF from trusted_relay for SPF result - res = self.spf and self.spf_guess - hres = self.spf and self.spf_helo - # Check whitelist and blacklist - if auto_whitelist.has_key(self.canon_from): - self.greylist = False - if res == 'pass' or self.trusted_relay: - self.whitelist = True - self.log("WHITELIST",self.canon_from) - else: - self.dspam = False - self.log("PROBATION",self.canon_from) - if res not in ('permerror','softfail'): - self.cbv_needed = None - elif cbv_cache.has_key(self.canon_from) and cbv_cache[self.canon_from] \ - or domain in blacklist: - # FIXME: don't use cbv_cache for blacklist if policy is 'OK' - if not self.internal_connection: - self.offense(inc=2) - if not dspam_userdir: - if domain in blacklist: - self.log('REJECT: BLACKLIST',self.canon_from) - self.setreply('550','5.7.1', 'Sender email local blacklist') - else: - res = cbv_cache[self.canon_from] - desc = "CBV: %d %s" % res[:2] - self.log('REJECT:',desc) - self.setreply('550','5.7.1',*desc.splitlines()) - return Milter.REJECT - self.greylist = False # don't delay - use spam for training - self.blacklist = True - self.log("BLACKLIST",self.canon_from) - else: - # REJECT delayed until after checking whitelist - if self.policy == 'REJECT': - self.log('REJECT: no PTR, HELO or SPF') - self.setreply('550','5.7.1', - "You must have a valid HELO or publish SPF: http://www.openspf.org ", - "Contact your mail administrator IMMEDIATELY! Your mail server is ", - "severely misconfigured. It has no PTR record (dynamic PTR records ", - "that contain your IP don't count), an invalid or dynamic HELO, ", - "and no SPF record." - ) - return self.offense() # ban ip if too many bad MFROMs - global gossip - if gossip and domain and rc == Milter.CONTINUE \ - and not (self.internal_connection or self.trusted_relay) \ - and gossip_node: - if self.spf and self.spf.result == 'pass': - qual = 'SPF' - elif res == 'pass': - qual = 'GUESS' - elif hres == 'pass': - qual = 'HELO' - domain = self.spf.h - else: - # No good identity: blame purported domain. Qualify by SPF - # result so NEUTRAL will get separate reputation from SOFTFAIL. - qual = res - try: - umis = gossip.umis(domain+qual,self.id+time.time()) - res = gossip_node.query(umis,domain,qual,1) - if res: - res,hdr,val = res - self.add_header(hdr,val) - a = val.split(',') - self.reputation = int(a[-2]) - self.confidence = int(a[-1]) - self.umis = umis - self.from_domain = domain - # We would like to reject on bad reputation here, but we - # need to give special consideration to postmaster. So - # we have to wait until envrcpt(). Perhaps an especially - # bad reputation could be rejected here. - if self.reputation < -70 and self.confidence > 5: - self.log('REJECT: REPUTATION') - self.setreply('550','5.7.1', - 'Your domain has been sending nothing but spam') - return Milter.REJECT - if self.reputation > 40 and self.confidence > 1: - self.greylist = False - except: - gossip = None - raise - return rc - - def check_spf(self): - receiver = self.receiver - for tf in trusted_forwarder: - q = spf.query(self.connectip,'',tf,receiver=receiver,strict=False) - res,code,txt = q.check() - if res == 'none': - res,code,txt = q.best_guess('v=spf1 a mx') - 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() - q.result = res - if res in ('unknown','permerror') and q.perm_error and q.perm_error.ext: - self.cbv_needed = (q,'permerror') # report SPF syntax error to sender - res,code,txt = q.perm_error.ext # extended (lax processing) result - txt = 'EXT: ' + txt - p = SPFPolicy(q.s) - # FIXME: try:finally to close policy db, or reuse with lock - if res in ('error','temperror'): - if self.need_cbv(p.getTempErrorPolicy(),q,'temperror'): - self.log('TEMPFAIL: SPF %s %i %s' % (res,code,txt)) - self.setreply(str(code),'4.3.0',txt, - 'We cannot accept your email until the DNS server for %s' % q.o, - 'is operational for TXT record queries.' - ) - return Milter.TEMPFAIL - res,code,txt = 'none',250,'EXT: ignoring DNS error' - hres = None - if res != 'pass': - 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() - # FIXME: in a few cases, rejecting on HELO neutral causes problems - # for senders forced to use their braindead ISPs email service. - 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 - if hres == 'none' and spf_best_guess \ - and not dynip(self.hello_name,self.connectip): - # HELO must match more exactly. Don't match PTR or zombies - # will be able to get a best_guess pass on their ISPs domain. - hres,hcode,htxt = h.best_guess('v=spf1 a mx') - else: - hres,hcode,htxt = res,code,txt - ores = res - if self.internal_domain and res == 'none': - # we don't accept our own domains externally without an SPF record - self.log('REJECT: spam from self',q.o) - self.setreply('550','5.7.1',"I hate talking to myself!") - return Milter.REJECT - if spf_best_guess and res == 'none': - #self.log('SPF: no record published, guessing') - q.set_default_explanation( - 'SPF guess: see http://openspf.org/why.html') - # best_guess should not result in fail - if self.missing_ptr: - # ignore dynamic PTR for best guess - res,code,txt = q.best_guess('v=spf1 a/24 mx/24') - else: - res,code,txt = q.best_guess() - if res != 'pass' and hres == 'pass' and spf.domainmatch([q.h],q.o): - res = 'pass' # get a guessed pass for valid matching HELO - if self.missing_ptr and ores == 'none' and res != 'pass' \ - and hres != 'pass': - # this bad boy has no credentials whatsoever - res = 'none' - policy = p.getNonePolicy() - if policy in ('CBV','DSN'): - self.offenses = 3 # ban ip if any bad recipient - self.need_cbv(policy,q,'strike3') - # REJECT delayed until after checking whitelist - if res in ('deny', 'fail'): - if self.need_cbv(p.getFailPolicy(),q,'fail'): - 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 . - return Milter.REJECT - elif res == 'softfail': - if self.need_cbv(p.getSoftfailPolicy(),q,'softfail'): - self.log('REJECT: SPF %s %i %s' % (res,code,txt)) - self.setreply('550','5.7.1', - 'SPF softfail: If you get this Delivery Status Notice, your email', - 'was probably legitimate. Your administrator has published SPF', - 'records in a testing mode. The SPF record reported your email as', - 'a forgery, which is a mistake if you are reading this. Please', - 'notify your administrator of the problem immediately.' - ) - return Milter.REJECT - elif res == 'neutral': - if self.need_cbv(p.getNeutralPolicy(),q,'neutral'): - self.log('REJECT: SPF neutral for',q.s) - self.setreply('550','5.7.1', - 'mail from %s must pass SPF: http://openspf.org/why.html' % q.o, - 'The %s domain is one that spammers love to forge. Due to' % q.o, - 'the volume of forged mail, we can only accept mail that', - 'the SPF record for %s explicitly designates as legitimate.' % q.o, - 'Sending your email through the recommended outgoing SMTP', - 'servers for %s should accomplish this.' % q.o - ) - return Milter.REJECT - elif res == 'pass': - if self.need_cbv(p.getPassPolicy(),q,'pass'): - self.log('REJECT: SPF pass for',q.s) - self.setreply('550','5.7.1', - "We don't accept mail from %s" %q.o, - "Your email from %s comes from an authorized server, however"%q.o, - "we still don't want it - we just don't like %s."%q.o - ) - return Milter.REJECT - elif res in ('unknown','permerror'): - if self.need_cbv(p.getPermErrorPolicy(),q,'permerror'): - 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 - kv = {} - if hres and q.h != q.o: - kv['helo_spf'] = hres - if res != q.result: - kv['bestguess'] = res - self.add_header('Received-SPF',q.get_header(q.result,receiver,**kv),0) - self.spf_guess = res - self.spf_helo = hres - self.spf = q - return Milter.CONTINUE - - # hide_path causes a copy of the message to be saved - until we - # track header mods separately from body mods - so use only - # in emergencies. - def envrcpt(self,to,*str): - try: - param = param2dict(str) - self.notify = param.get('NOTIFY','FAILURE,DELAY').upper().split(',') - if 'NEVER' in self.notify: self.notify = () - #self.rcpt_param = param - except: - self.log("REJECT: invalid PARAM:",to,str) - self.setreply('550','5.7.1','Invalid RCPT PARAM') - return Milter.REJECT - # mail to MAILER-DAEMON is generally spam that bounced - if to.startswith(' 1: - newaddr = newaddr[0] - self.log("ses rcpt:",newaddr) - else: - newaddr = srs.reverse(oldaddr) - # Currently, a sendmail map reverses SRS. We just log it here. - self.log("srs rcpt:",newaddr) - self.dspam = False # verified as reply to mail we sent - self.blacklist = False - self.greylist = False - self.delayed_failure = False - except: - if not (self.internal_connection or self.trusted_relay): - if srsre.match(oldaddr): - self.log("REJECT: srs spoofed:",oldaddr) - self.setreply('550','5.7.1','Invalid SRS signature') - return Milter.REJECT - if oldaddr.startswith('SES='): - self.log("REJECT: ses spoofed:",oldaddr) - self.setreply('550','5.7.1','Invalid SES signature') - return Milter.REJECT - # reject for certain recipients are delayed until after DATA - if auto_whitelist.has_precise_key(self.canon_from): - self.log("WHITELIST: DSN from",self.canon_from) - else: - if srs_reject_spoofed \ - and user.lower() not in ('postmaster','abuse'): - return self.forged_bounce(to) - self.data_allowed = not srs_reject_spoofed - - if not self.internal_connection and domain in private_relay: - self.log('REJECT: RELAY:',to) - self.setreply('550','5.7.1','Unauthorized relay for %s' % domain) - return Milter.REJECT - - # non DSN mail to SRS address will bounce due to invalid local part - canon_to = '@'.join(t) - if canon_to == 'postmaster@' + self.receiver: - self.postmaster_reply = True - - self.recipients.append(canon_to) - # FIXME: use newaddr to check rcpt - users = check_user.get(domain) - if self.discard: - self.del_recipient(to) - # don't check userlist if signed MFROM for now - userl = user.lower() - if users and not newaddr and not userl in users: - self.log('REJECT: RCPT TO:',to,str) - if gossip and self.umis: - gossip_node.feedback(self.umis,1) - self.umis = None - return self.offense() - # FIXME: should dspam_exempt be case insensitive? - if user in block_forward.get(domain,()): - self.forward = False - exempt_users = dspam_exempt.get(domain,()) - if user in exempt_users or '' in exempt_users: - if self.blacklist: - self.log('REJECT: BLACKLISTED, rcpt to',to,str) - self.setreply('550','5.7.1','Sending domain has been blacklisted') - return Milter.REJECT - self.dspam = False - if userl != 'postmaster' and self.umis \ - and self.reputation < -50 and self.confidence > 3: - domain = self.from_domain - self.log('REJECT: REPUTATION, rcpt to',to,str) - self.setreply('550','5.7.1','%s has been sending mostly spam'%domain) - return Milter.REJECT - - if domain in hide_path: - self.hidepath = True - if not domain in dspam_reject: - self.reject_spam = False - - except: - self.log("rcpt to",to,str) - raise - if self.greylist and greylist and self.canon_from: - # no policy for trusted or internal - rc = greylist.check(self.connectip,self.canon_from,canon_to) - if rc == 0: - self.log("GREYLIST:",self.connectip,self.canon_from,canon_to) - self.setreply('451','4.7.1', - 'Greylisted: http://projects.puremagic.com/greylisting/', - 'Please retry in %.1f minutes'%(greylist.greylist_time/60.0)) - return Milter.TEMPFAIL - self.log("GREYLISTED: %d"%rc) - - self.log("rcpt to",to,str) - self.smart_alias(to) - # get recipient after virtusertable aliasing - #rcpt = self.getsymval("{rcpt_addr}") - #self.log("rcpt-addr",rcpt); - return Milter.CONTINUE - - # Heuristic checks for spam headers - def check_header(self,name,val): - lname = name.lower() - # val is decoded header value - if lname == 'subject': - - # check for common spam keywords - for wrd in spam_words: - if val.find(wrd) >= 0: - self.log('REJECT: %s: %s' % (name,val)) - self.setreply('550','5.7.1','That subject is not allowed') - return Milter.REJECT - - # even if we wanted the Taiwanese spam, we can't read Chinese - if block_chinese: - if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'): - self.log('REJECT: %s: %s' % (name,val)) - self.setreply('550','5.7.1',"We don't understand chinese") - return Milter.REJECT - - # check for spam that claims to be legal - lval = val.lower().strip() - for adv in ("adv:","adv.","adv ","[adv]","(adv)","advt:","advert:"): - if lval.startswith(adv): - self.log('REJECT: %s: %s' % (name,val)) - self.setreply('550','5.7.1','Advertising not accepted here') - return Milter.REJECT - for adv in ("adv","(adv)","[adv]"): - if lval.endswith(adv): - self.log('REJECT: %s: %s' % (name,val)) - self.setreply('550','5.7.1','Advertising not accepted here') - return Milter.REJECT - - # check for porn keywords - for w in porn_words: - if lval.find(w) >= 0: - self.log('REJECT: %s: %s' % (name,val)) - self.setreply('550','5.7.1','That subject is not allowed') - return Milter.REJECT - - # check for annoying forwarders - if not self.forward: - if lval.startswith("fwd:") or lval.startswith("[fw"): - self.log('REJECT: %s: %s' % (name,val)) - self.setreply('550','5.7.1','I find unedited forwards annoying') - return Milter.REJECT - - # check for delayed bounce of CBV - if self.postmaster_reply and srs: - if refaildsn.search(lval): - self.delayed_failure = val.strip() - # if confirmed by finding our signed Message-ID, - # original sender (encoded in Message-ID) is blacklisted - - elif lname == 'from': - fname,email = parseaddr(val) - # check for porn keywords - lval = fname.lower().strip() - for w in porn_words: - if lval.find(w) >= 0: - self.log('REJECT: %s: %s' % (name,val)) - self.setreply('550','5.7.1','Watch your language') - return Milter.REJECT - if email.lower().startswith('postmaster@'): - # Yes, if From header comes last, this might not help much. - # But this is a heuristic - if MTAs would send proper DSNs in - # the first place, none of this would be needed. - self.is_bounce = True - - # check for invalid message id - elif lname == 'message-id' and len(val) < 4: - self.log('REJECT: %s: %s' % (name,val)) - return Milter.REJECT - - # check for common bulk mailers - elif lname == 'x-mailer': - mailer = val.lower() - if mailer in ('direct email','calypso','mail bomber') \ - or mailer.find('optin') >= 0: - self.log('REJECT: %s: %s' % (name,val)) - return Milter.REJECT - return Milter.CONTINUE - - def forged_bounce(self,rcpt='-'): - if self.mailfrom != '<>': - self.log("REJECT: bogus DSN",rcpt) - self.setreply('550','5.7.1', - "I do not accept normal mail from %s." % self.mailfrom.split('@')[0], - "All such mail has turned out to be Delivery Status Notifications", - "which failed to be marked as such. Please send a real DSN if", - "you need to. Use another MAIL FROM if you need to send me mail." - ) - else: - self.log('REJECT: bounce with no SRS encoding',rcpt) - self.setreply('550','5.7.1', - "I did not send you that message. Please consider implementing SPF", - "(http://openspf.org) to avoid bouncing mail to spoofed senders.", - "Thank you." - ) - return Milter.REJECT - - def header(self,name,hval): - if not self.data_allowed: - return self.forged_bounce() - - lname = name.lower() - # decode near ascii text to unobfuscate - val = parse_header(hval) - if not self.internal_connection and not (self.blacklist or self.whitelist): - rc = self.check_header(name,val) - if rc != Milter.CONTINUE: - if gossip and self.umis: - gossip_node.feedback(self.umis,1) - return rc - elif self.whitelist_sender: - # check for AutoReplys - if (lname == 'subject' and reautoreply.match(val)) \ - or (lname == 'user-agent' and val.lower().startswith('vacation')): - self.whitelist_sender = False - self.log('AUTOREPLY: not whitelisted') - - # log selected headers - if log_headers or lname in ('subject','x-mailer'): - self.log('%s: %s' % (name,val)) - elif self.trust_received and lname == 'received': - self.trust_received = False - self.log('%s: %s' % (name,val.splitlines()[0])) - elif self.trust_spf and lname == 'received-spf': - self.trust_spf = False - self.log('%s: %s' % (name,val.splitlines()[0])) - if self.fp: - try: - val = val.encode('us-ascii') - except: - val = hval - self.fp.write("%s: %s\n" % (name,val)) # add header to buffer - return Milter.CONTINUE - - def eoh(self): - if not self.fp: return Milter.TEMPFAIL # not seen by envfrom - if not self.data_allowed: - return self.forged_bounce() - for name,val,idx in self.new_headers: - self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer - self.fp.write("\n") # terminate headers - if not self.internal_connection: - msg = None # parse headers only if needed - if not self.delayed_failure: - self.fp.seek(0) - msg = email.message_from_file(self.fp) - if msg.get_param('report-type','').lower() == 'delivery-status': - self.is_bounce = True - self.delayed_failure = msg.get('subject','DSN') - # log when neither sender nor from domains matches mail from domain - if supply_sender and self.mailfrom != '<>': - if not msg: - self.fp.seek(0) - msg = email.message_from_file(self.fp) - mf_domain = self.canon_from.split('@')[-1] - for rn,hf in getaddresses(msg.get_all('from',[]) - + msg.get_all('sender',[])): - t = parse_addr(hf) - if len(t) == 2: - hd = t[1].lower() - if hd == mf_domain or mf_domain.endswith('.'+hd): break - else: - for f in msg.get_all('from',[]): - self.log('From:',f) - sender = msg.get_all('sender') - if sender: - for f in sender: - self.log('Sender:',f) - else: - self.log("NOTE: Supplying MFROM as Sender"); - self.add_header('Sender',self.mailfrom) - del msg - # copy headers to a temp file for scanning the body - self.fp.seek(0) - headers = self.fp.getvalue() - self.fp.close() - fd,fname = tempfile.mkstemp(".defang") - self.tempname = fname - self.fp = os.fdopen(fd,"w+b") - self.fp.write(headers) # IOError (e.g. disk full) causes TEMPFAIL - # check if headers are really spammy - if dspam_dict and not self.internal_connection: - ds = dspam.dspam(dspam_dict,dspam.DSM_PROCESS, - dspam.DSF_CHAINED|dspam.DSF_CLASSIFY) - try: - ds.process(headers) - if ds.probability > 0.93 and self.dspam and not self.whitelist: - self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability) - self.setreply('550','5.7.1','Your Message looks spammy') - return Milter.REJECT - self.add_header('X-DSpam-HeaderScore','%f'%ds.probability) - finally: - ds.destroy() - return Milter.CONTINUE - - def body(self,chunk): # copy body to temp file - if self.fp: - self.fp.write(chunk) # IOError causes TEMPFAIL in milter - self.bodysize += len(chunk) - return Milter.CONTINUE - - def _headerChange(self,msg,name,value): - if value: # add header - self.addheader(name,value) - else: # delete all headers with name - h = msg.getheaders(name) - if h: - for i in range(len(h),0,-1): - self.chgheader(name,i-1,'') - - def _chk_ext(self,name): - "Check a name for dangerous Winblows extensions." - if not name: return name - lname = name.lower() - for ext in self.bad_extensions: - if lname.endswith(ext): return name - return None - - - def _chk_attach(self,msg): - "Filter attachments by content." - # check for bad extensions - mime.check_name(msg,self.tempname,ckname=self._chk_ext,scan_zip=scan_zip) - # remove scripts from HTML - if scan_html: - mime.check_html(msg,self.tempname) - # don't let a tricky virus slip one past us - if scan_rfc822: - msg = msg.get_submsg() - if isinstance(msg,email.Message.Message): - return mime.check_attachments(msg,self._chk_attach) - return Milter.CONTINUE - - def alter_recipients(self,discard_list,redirect_list): - for rcpt in discard_list: - if rcpt in redirect_list: continue - self.log("DISCARD RCPT: %s" % rcpt) # log discarded rcpt - self.delrcpt(rcpt) - for rcpt in redirect_list: - if rcpt in discard_list: continue - self.log("APPEND RCPT: %s" % rcpt) # log appended rcpt - self.addrcpt(rcpt) - if not blind_wiretap: - self.addheader('Cc',rcpt) - - # - def gossip_header(self): - "Set UMIS from GOSSiP header." - msg = email.message_from_file(self.fp) - gh = msg.get('x-gossip') - if gh: - self.log('X-GOSSiP:',gh) - self.umis,_ = gh.split(',',1) - - # check spaminess for recipients in dictionary groups - # if there are multiple users getting dspammed, then - # a signature tag for each is added to the message. - - # FIXME: quarantine messages rejected via fixed patterns above - # this will give a fast start to stats - - def check_spam(self): - "return True/False if self.fp, else return Milter.REJECT/TEMPFAIL/etc" - self.screened = False - if not dspam_userdir: return False - ds = Dspam.DSpamDirectory(dspam_userdir) - ds.log = self.log - ds.headerchange = self._headerChange - modified = False - for rcpt in self.recipients: - if dspam_users.has_key(rcpt.lower()): - user = dspam_users.get(rcpt.lower()) - if user: - try: - self.fp.seek(0) - txt = self.fp.read() - if user == 'spam' and self.internal_connection: - sender = dspam_users.get(self.canon_from) - if sender: - self.log("SPAM: %s" % sender) # log user for SPAM - ds.add_spam(sender,txt) - txt = None - self.fp.seek(0) - self.gossip_header() - self.fp = None - return Milter.DISCARD - elif user == 'falsepositive' and self.internal_connection: - sender = dspam_users.get(self.canon_from) - if sender: - self.log("FP: %s" % sender) # log user for FP - txt = ds.false_positive(sender,txt) - self.fp = StringIO.StringIO(txt) - self.gossip_header() - self.delrcpt('<%s>' % rcpt) - self.recipients = None - self.rejectvirus = False - return True - elif not self.internal_connection or dspam_internal: - if len(txt) > dspam_sizelimit: - self.log("Large message:",len(txt)) - if self.blacklist: - self.log('REJECT: BLACKLISTED') - self.setreply('550','5.7.1', - '%s has been blacklisted.'%self.canon_from) - self.fp = None - return Milter.REJECT - return False - if user == 'honeypot' and Dspam.VERSION >= '1.1.9': - keep = False # keep honeypot mail - self.fp = None - if len(self.recipients) > 1: - self.log("HONEYPOT:",rcpt,'SCREENED') - if self.whitelist: - # don't train when recipients includes honeypot - return False - if self.spf and self.mailfrom != '<>': - # check that sender accepts quarantine DSN - if self.spf.result == 'pass': - msg = mime.message_from_file(StringIO.StringIO(txt)) - rc = self.send_dsn(self.spf,msg,'quarantine') - del msg - else: - rc = self.send_dsn(self.spf) - if rc != Milter.CONTINUE: - return rc - ds.check_spam(user,txt,self.recipients,quarantine=True, - force_result=dspam.DSR_ISSPAM) - else: - ds.check_spam(user,txt,self.recipients,quarantine=keep, - force_result=dspam.DSR_ISSPAM) - self.log("HONEYPOT:",rcpt) - return Milter.DISCARD - if self.whitelist: - # Sender whitelisted: tag, but force as ham. - # User can change if actually spam. - txt = ds.check_spam(user,txt,self.recipients, - force_result=dspam.DSR_ISINNOCENT) - elif self.blacklist: - txt = ds.check_spam(user,txt,self.recipients, - force_result=dspam.DSR_ISSPAM) - elif user in dspam_train: - txt = ds.check_spam(user,txt,self.recipients) - else: - txt = ds.check_spam(user,txt,self.recipients,classify=True) - if txt: - self.add_header("X-DSpam-Score",'%f' % ds.probability) - return False - if not txt: - # DISCARD if quarrantined for any recipient. It - # will be resent to all recipients if they submit - # as a false positive. - self.log("DSPAM:",user,rcpt) - self.fp = None - return Milter.DISCARD - self.fp = StringIO.StringIO(txt) - modified = True - except Exception,x: - self.log("check_spam:",x) - milter_log.error("check_spam: %s",x,exc_info=True) - # screen if no recipients are dspam_users - if not modified and dspam_screener and not self.internal_connection \ - and self.dspam: - self.fp.seek(0) - txt = self.fp.read() - if len(txt) > dspam_sizelimit: - self.log("Large message:",len(txt)) - return False - screener = dspam_screener[self.id % len(dspam_screener)] - if not ds.check_spam(screener,txt,self.recipients, - classify=True,quarantine=False): - if self.whitelist: - # messages is whitelisted but looked like spam, Train on Error - self.log("TRAIN:",screener,'X-Dspam-Score: %f' % ds.probability) - # user can't correct anyway if really spam, so discard tag - ds.check_spam(screener,txt,self.recipients, - force_result=dspam.DSR_ISINNOCENT) - return False - if self.reject_spam and self.spf.result != 'pass': - self.log("DSPAM:",screener, - 'REJECT: X-DSpam-Score: %f' % ds.probability) - self.setreply('550','5.7.1','Your Message looks spammy') - self.fp = None - return Milter.REJECT - self.log("DSPAM:",screener,"SCREENED") - if self.spf and self.mailfrom != '<>': - # check that sender accepts quarantine DSN - self.fp.seek(0) - if self.spf.result == 'pass' or self.cbv_needed: - msg = mime.message_from_file(self.fp) - if self.spf.result == 'pass': - rc = self.send_dsn(self.spf,msg,'quarantine') - else: - rc = self.do_needed_cbv(msg) - del msg - else: - rc = self.send_dsn(self.spf) - if rc != Milter.CONTINUE: - self.fp = None - return rc - if not ds.check_spam(screener,txt,self.recipients,classify=True): - self.fp = None - return Milter.DISCARD - # Message no longer looks spammy, deliver normally. We lied in the DSN. - elif self.blacklist: - # message is blacklisted but looked like ham, Train on Error - self.log("TRAINSPAM:",screener,'X-Dspam-Score: %f' % ds.probability) - ds.check_spam(screener,txt,self.recipients,quarantine=False, - force_result=dspam.DSR_ISSPAM) - self.fp = None - self.setreply('550','5.7.1', 'Sender email local blacklist') - return Milter.REJECT - elif self.whitelist and ds.totals[1] < 1000: - self.log("TRAIN:",screener,'X-Dspam-Score: %f' % ds.probability) - # user can't correct anyway if really spam, so discard tag - ds.check_spam(screener,txt,self.recipients, - force_result=dspam.DSR_ISINNOCENT) - return False - # log spam score for screened messages - self.add_header("X-DSpam-Score",'%f' % ds.probability) - self.screened = True - return modified - - # train late in eom(), after failed CBV - # FIXME: need to undo if registered as ham with a dspam_user - def train_spam(self): - "Train screener with current message as spam" - if not dspam_userdir: return - if not dspam_screener: return - ds = Dspam.DSpamDirectory(dspam_userdir) - ds.log = self.log - self.fp.seek(0) - txt = self.fp.read() - if len(txt) > dspam_sizelimit: - self.log("Large message:",len(txt)) - return - screener = dspam_screener[self.id % len(dspam_screener)] - # since message will be rejected, we do not quarantine - ds.check_spam(screener,txt,self.recipients,force_result=dspam.DSR_ISSPAM, - quarantine=False) - self.log("TRAINSPAM:",screener,'X-Dspam-Score: %f' % ds.probability) - - def do_needed_cbv(self,msg): - q,template_name = self.cbv_needed - rc = self.send_dsn(q,msg,template_name) - self.cbv_needed = None - return rc - - def need_cbv(self,policy,q,tname): - self.policy = policy - if policy == 'CBV': - if self.mailfrom != '<>' and not self.cbv_needed: - self.cbv_needed = (q,None) - elif policy == 'DSN': - if self.mailfrom != '<>' and not self.cbv_needed: - self.cbv_needed = (q,tname) - elif policy != 'OK': - return True - return False - - def eom(self): - if not self.fp: - return Milter.ACCEPT # no message collected - so no eom processing - - if self.is_bounce and len(self.recipients) > 1: - self.log("REJECT: DSN to multiple recipients") - self.setreply('550','5.7.1', 'DSN to multiple recipients') - return Milter.REJECT - - try: - # check for delayed bounce - if self.delayed_failure: - self.fp.seek(0) - sender = findsrs(self.fp) - if sender: - cbv_cache[sender] = 550,self.delayed_failure - # make blacklisting persistent, since delayed DSNs are expensive - blacklist[sender] = None - try: - # save message for debugging - fname = tempfile.mktemp(".dsn") - os.rename(self.tempname,fname) - except: - fname = self.tempname - self.tempname = None - self.log('BLACKLIST:',sender,fname) - return Milter.DISCARD - - - # analyze external mail for spam - spam_checked = self.check_spam() # tag or quarantine for spam - if not self.fp: - if gossip and self.umis: - gossip_node.feedback(self.umis,1) - return spam_checked - - # analyze all mail for dangerous attachments and scripts - self.fp.seek(0) - msg = mime.message_from_file(self.fp) - # pass header changes in top level message to sendmail - msg.headerchange = self._headerChange - - # filter leaf attachments through _chk_attach - assert not msg.ismodified() - self.bad_extensions = ['.' + x for x in banned_exts] - rc = mime.check_attachments(msg,self._chk_attach) - except: # milter crashed trying to analyze mail - exc_type,exc_value = sys.exc_info()[0:2] - if dspam_userdir and exc_type == dspam.error: - if not exc_value.strerror: - exc_value.strerror = exc_value.args[0] - if exc_value.strerror == 'Lock failed': - milter_log.warn("LOCK: BUSY") # log filename - self.setreply('450','4.2.0', - 'Too busy discarding spam. Please try again later.') - return Milter.TEMPFAIL - fname = tempfile.mktemp(".fail") # save message that caused crash - os.rename(self.tempname,fname) - self.tempname = None - if exc_type == email.Errors.BoundaryError: - milter_log.warn("MALFORMED: %s",fname) # log filename - if self.internal_connection: - # accept anyway for now - return Milter.ACCEPT - self.setreply('554','5.7.7', - 'Boundary error in your message, are you a spammer?') - return Milter.REJECT - if exc_type == email.Errors.HeaderParseError: - milter_log.warn("MALFORMED: %s",fname) # log filename - self.setreply('554','5.7.7', - 'Header parse error in your message, are you a spammer?') - return Milter.REJECT - milter_log.error("FAIL: %s",fname) # log filename - # let default exception handler print traceback and return 451 code - raise - if rc == Milter.REJECT: return rc; - if rc == Milter.DISCARD: return rc; - - if rc == Milter.CONTINUE: rc = Milter.ACCEPT # for testbms.py compat - - defanged = msg.ismodified() - - if self.hidepath: del msg['Received'] - - if self.recipients == None: - # false positive being recirculated - self.recipients = msg.get_all('x-dspam-recipients',[]) - if self.recipients: - for rcptlist in self.recipients: - for rcpt in rcptlist.split(','): - self.addrcpt('<%s>' % rcpt.strip()) - del msg['x-dspam-recipients'] - else: - self.addrcpt(self.mailfrom) - else: - self.alter_recipients(self.discard_list,self.redirect_list) - # auto whitelist original recipients - if not defanged and self.whitelist_sender: - for canon_to in self.recipients: - user,domain = canon_to.split('@') - if internal_domains: - for pat in internal_domains: - if fnmatchcase(domain,pat): break - else: - auto_whitelist[canon_to] = None - self.log('Auto-Whitelist:',canon_to) - else: - auto_whitelist[canon_to] = None - self.log('Auto-Whitelist:',canon_to) - - for name,val,idx in self.new_headers: - try: - try: - self.addheader(name,val,idx) - except TypeError: - val = val.replace('\x00',r'\x00') - self.addheader(name,val,idx) - except Milter.error: - self.addheader(name,val) # older sendmail can't insheader - - # Do not send CBV to internal domains (since we'll just get - # the "Fraudulent MX" error). Whitelisted senders clearly do not - # need CBV. However, whitelisted domains might (to discover - # bogus localparts). Need a way to tell the difference. - if self.cbv_needed and not self.internal_domain: - rc = self.do_needed_cbv(msg) - if rc == Milter.REJECT: - # Do not feedback here, because feedback should only occur - # for messages that have gone to DATA. Reputation lets us - # reject before DATA for persistent spam domains, saving - # cycles and bandwidth. - - # Do feedback here, because CBV costs quite a bit more than - # simply rejecting before DATA. Bad reputation will acrue to - # the IP or HELO, since we won't get here for validated MAILFROM. - # See Proverbs 26:4,5 - if gossip and self.umis: - gossip_node.feedback(self.umis,1) - self.train_spam() - return Milter.REJECT - if rc != Milter.CONTINUE: - return rc - - if mail_archive: - global _archive_lock - if not _archive_lock: - import thread - _archive_lock = thread.allocate_lock() - _archive_lock.acquire() - try: - fin = open(self.tempname,'r') - fout = open(mail_archive,'a') - shutil.copyfileobj(fin,fout,8192) - finally: - _archive_lock.release() - fin.close() - fout.close() - - if not defanged and not spam_checked: - if gossip and self.umis and self.screened: - gossip_node.feedback(self.umis,0) - os.remove(self.tempname) - self.tempname = None # prevent re-removal - self.log("eom") - return rc # no modified attachments - - # Body modified, copy modified message to a temp file - if defanged: - if self.rejectvirus and not self.hidepath: - self.log("REJECT virus from",self.mailfrom) - self.setreply('550','5.7.1','Attachment type not allowed.', - 'You attempted to send an attachment with a banned extension.') - self.tempname = None - return Milter.REJECT - self.log("Temp file:",self.tempname) - self.tempname = None # prevent removal of original message copy - out = tempfile.TemporaryFile() - try: - msg.dump(out) - out.seek(0) - # Since we wrote headers with '\n' (no CR), - # the following header/body split should always work. - msg = out.read().split('\n\n',1)[-1] - self.replacebody(msg) # feed modified message to sendmail - if spam_checked: - if gossip and self.umis: - gossip_node.feedback(self.umis,0) - self.log("dspam") - return rc - finally: - out.close() - return Milter.TEMPFAIL - - def send_dsn(self,q,msg=None,template_name=None): - sender = q.s - cached = cbv_cache.has_key(sender) - if cached: - self.log('CBV:',sender,'(cached)') - res = cbv_cache[sender] - else: - m = None - if template_name: - fname = template_name+'.txt' - try: - template = file(template_name+'.txt').read() - m = dsn.create_msg(q,self.recipients,msg,template) - self.log('CBV:',sender,'Using:',fname) - except IOError: pass - if not m: - self.log('CBV:',sender,'PLAIN (%s)'%q.result) - else: - if srs: - # Add SRS coded sender to various headers. When (incorrectly) - # replying to our DSN, any of these which are preserved - # allow us to track the source. - msgid = srs.forward(sender,self.receiver) - m.add_header('Message-Id','<%s>'%msgid) - if 'x-mailer' in m: - m.replace_header('x-mailer','"%s" <%s>' % (m['x-mailer'],msgid)) - else: - m.add_header('X-Mailer','"Python Milter" <%s>'%msgid) - m.add_header('Sender','"Python Milter" <%s>'%msgid) - m = m.as_string() - print >>open(template_name+'.last_dsn','w'),m - # if missing template, do plain CBV - res = dsn.send_dsn(sender,self.receiver,m,timeout=timeout) - if res: - desc = "CBV: %d %s" % res[:2] - if 400 <= res[0] < 500: - self.log('TEMPFAIL:',desc) - self.setreply('450','4.2.0',*desc.splitlines()) - return Milter.TEMPFAIL - cbv_cache[sender] = res - self.log('REJECT:',desc) - try: - self.setreply('550','5.7.1',*desc.splitlines()) - except TypeError: - self.setreply('550','5.7.1',"Callback failure") - return Milter.REJECT - cbv_cache[sender] = res - return Milter.CONTINUE - - def close(self): - if self.tempname: - os.remove(self.tempname) # remove in case session aborted - if self.fp: - self.fp.close() - - return Milter.CONTINUE - - def abort(self): - self.log("abort after %d body chars" % self.bodysize) - return Milter.CONTINUE - -def main(): - if access_file: - try: - acf = anydbm.open(access_file,'r') - acf.close() - except: - milter_log.error('Unable to read: %s',access_file) - return - try: - from glob import glob - global banned_ips - banned_ips = set(addr2bin(ip) - for fn in glob('banned_ips*') - for ip in open(fn)) - print len(banned_ips),'banned ips' - except: - milter_log.exception('Error reading banned_ips') - Milter.factory = bmsMilter - flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS - if wiretap_dest or smart_alias or dspam_userdir: - flags = flags + Milter.ADDRCPT - if srs or len(discard_users) > 0 or smart_alias or dspam_userdir: - flags = flags + Milter.DELRCPT - Milter.set_flags(flags) - socket.setdefaulttimeout(60) - milter_log.info("bms milter startup") - Milter.runmilter("pythonfilter",socketname,timeout) - milter_log.info("bms milter shutdown") - -if __name__ == "__main__": - read_config(["/etc/mail/pymilter.cfg","milter.cfg"]) - - cbv_cache.load('send_dsn.log',age=30) - auto_whitelist.load('auto_whitelist.log',age=120) - blacklist.load('blacklist.log',age=60) - - if dspam_dict: - import dspam # low level spam check - if dspam_userdir: - import dspam - import Dspam # high level spam check - try: - dspam_version = Dspam.VERSION - except: - dspam_version = '1.1.4' - assert dspam_version >= '1.1.5' - main() diff --git a/fail.txt b/fail.txt deleted file mode 100644 index c608982..0000000 --- a/fail.txt +++ /dev/null @@ -1,35 +0,0 @@ -To: %(sender)s -From: postmaster@%(receiver)s -Subject: SPF fail (EMAIL FORGERY) -Auto-Submitted: auto-generated (configuration error) - -This is an automatically generated Delivery Status Notification. - -*** WARNING! YOU ARE SENDING FROM AN UNAUTHORIZED LOCATION *** - -The email administrator for '%(sender_domain)' (YOUR administrator) -has FORBIDDEN you to send email from this location. IMMEDIATELY contact your -email administrator and follow his instructions to properly send mail. - -THIS IS A WARNING MESSAGE ONLY. - -YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. - -Delivery to the following recipients has been delayed. - - %(rcpt)s - -Subject: %(subject)s -Received-SPF: %(spf_result)s - -Your sender policy indicated that the above email was forged. -Because we believe your policy is in error, we have accepted the -email anyway. Please ask your email administrator to review -your SPF policy. You may also have neglected to follow your -postmaster's instructions for configuring outgoing email. - -If you need further assistance, please do not hesitate to contact me. - -Kind regards, -Stuart D Gathman -postmaster@%(receiver)s diff --git a/milter-template.py b/milter-template.py index 510bf02..0835104 100644 --- a/milter-template.py +++ b/milter-template.py @@ -12,25 +12,8 @@ import StringIO import time import email from socket import AF_INET, AF_INET6 +from Milter import parse_addr -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): diff --git a/milter.cfg b/milter.cfg deleted file mode 100644 index 5151464..0000000 --- a/milter.cfg +++ /dev/null @@ -1,238 +0,0 @@ -[milter] -# the directory with log and data files -datadir = /var/log/milter -# the socket used to communicate with sendmail. Must match sendmail.cf -socket=/var/run/milter/pythonsock -# where to save original copies of defanged and failed messages -tempdir = /var/log/milter/save -# how long to wait for a response from sendmail before giving up -;timeout=600 -log_headers = 0 -# Connection ips and hostnames are matched against this glob style list -# to recognize internal senders. You probably need to change this. -# The default is a good guess to try and prevent newbie frustration. -internal_connect = 192.168.0.0/16,127.* - -# mail that is not an internal_connect and claims to be from an -# internal domain is rejected. Furthermore, internal mail that -# does not claim to be from an internal domain is rejected. -# You should enable SPF instead if you can. SPF is much more comprehensive and -# flexible. However, SPF is not currently checked for outgoing -# (internal_connect) mail because it doesn't yet handle authorizing -# internal IPs locally. -;internal_domains = mycorp.com,localhost.localdomain - -# connections from a trusted relay can trust the first Received header -# SPF checks are bypassed for internal connections and trusted relays. -;trusted_relay = 1.2.3.4, 66.12.34.56 - -# Relaying to these domains is allowed from internal connections only. -# You might want to restrict aol.com, for instance, so that stupid -# users don't forward their spam to aol for filtering and get your MTA -# blacklisted by aol. -;private_relay = aol.com, yahoo.com - -# Reject external senders with hello names no legit external sender would use. -# SPF will do this also, but listing your own domain and mailserver here -# will save some DNS lookups when rejecting certain viruses. -;hello_blacklist = mycorp.com, 66.12.34.56 - -# Reject mail for domains mentioned unless user is mentioned here also -;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com - -# Treat localparts in milter.cfg as case-insensitive -case_sensitive_localpart = true - -# features intended to filter or block incoming mail -[defang] - -# do virus scanning on attached messages also -scan_rfc822 = 0 -# do virus scanning on attached zipfiles also -scan_zip = 0 -# Comment out scripts in HTML attachments. Can be CPU intensive. -scan_html = 0 -# reject messages with asian fonts because we can't read them -block_chinese = 0 -# list users who hate forwarded mail -;block_forward = egghead@mycorp.com, busybee@mycorp.com -# reject mail with these case insensitive strings in the subject -porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck, - vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck, xanaax, - p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax, diazepam, - v1@gra, xan@x, cialis, ci@lis, frëe, xănax, valíum, vălium, via-gra, - x@n3x, vicod3n, penís, c0d1n, phentermine, en1arge, dip1oma, v1codin, - valium, rolex, sexual, fuck, adv1t, vgaira, medz, acai berry -# reject mail with these case sensitive strings in the subject -spam_words = $$$, !!!, XXX, FREE, HGH -# attachments with these extensions will be replaced with a warning -# message. A copy of the original will be saved. -banned_exts = ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta, - inf,ins,isp,js,jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct, - shs,url,vb,vbe,vbs,wsc,wsf,wsh - -# See http://bmsi.com/python/pysrs.html for details -[srs] -config=/etc/mail/pysrs.cfg -# SRS options can be set here also, but must match the sendmail plugin -;secret="shhhh!" -;maxage=21 -;hashlength=4 -;database=/var/log/milter/srsdata -;fwdomain = mydomain.com -# turn this on after a grace period to reject spoofed DSNs -reject_spoofed = 0 -# Many braindead MTAs send DSNs with a non-DSN MFROM (e.g. to report that -# some virus claiming to be sent by you). This heuristic -# refuses mail from user names commonly abused in that way. -;banned_users = postmaster, mailer-daemon, clamav - -# See http://www.openspf.com for more info on SPF. -[spf] -# namespace where SPF records can be supplied for domains without one -# records are searched for under _spf.domain.com -;delegate = domain.com -# domains where a neutral SPF result should cause mail to be rejected -;reject_neutral = aol.com -# use a default (v=spf1 a/24 mx/24 ptr) when no SPF records are published -;best_guess = 0 -# Reject senders that have neither PTR nor valid HELO nor SPF records, or send -# DSN otherwise -;reject_noptr = 0 -# always accept softfail from these domains, or send DSN otherwise -;accept_softfail = bounces.amazon.com -# Treat fail from these domains like softfail: because their SPF record -# or an important sender is screwed up. Must have valid HELO, however. -;accept_fail = custhelp.com -# Use sendmail access map or similar format for detailed spf policy. -# SPF entries in the access map will override any defaults set above. -;access_file = /etc/mail/access.db -# Add MAIL FROM as Sender when Sender is missing and From domain -# doesn't match MAIL FROM. Outlook and other email clients will then display -# something like: "Sent by sender@domain.com on behalf of from@example.com" -;supply_sender = 0 -# Connections that get an SPF pass for a pretend MAIL FROM of -# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM. -# This is for non-SRS forwarders. It is a simple implementation that -# is inefficient for more than a few entries. -;trusted_forwarder = careerbuilder.com - -# features intended to clean up outgoing mail -[scrub] -# domains that block visible private nodes -;hide_path = jcpenney.com -# reject, don't just replace with warning, viruses from these domains -;reject_virus_from = mycorp.com - -# features intended for spying on users and coworkers -[wiretap] -blind = 1 -# -# wiretap lets you surreptitiously monitor a users outgoing email -# (sendmail aliases let you monitor incoming mail) -# -;users = disloyal@bigcorp.com, bigmouth@bigcorp.com -# multiple destinations can use smart_alias -;dest = spy@bigcorp.com -# discard outgoing mail without alerting sender -# can be used in conjunction with wiretap to censor outgoing mail -;discard_users = canned@bigcorp.com -# archive copies all delivered mail to a file -;mail_archive = /var/log/mail_archive - -# -# smart aliases trigger on both sender and recipient -# alias = sender, recipient[, destination] -# -[smart_alias] -# multiple wiretap monitors. Smart aliases are applied after wiretap. -;spy1 = disloyal@bigcorp.com,spy@bigcorp.com -;spy2 = bigmouth@bigcorp.com,spy@bigcorp.com -# mail from client@clientcorp.com to sue@bigcorp.com is redirected to -# local alias copycust -;copycust = client@clientcorp.com,sue@bigcorp.com -# mail from cust@othercorp.com to walter@bigcorp.com is redirected to -# boss@bigcorp.com -;walter = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com -# additional copies can be added -;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com, -; walter@bigcorp.com -;bulk = soruce@telex.com,bob@jsconnor.com -;bulk1 = soruce@telex.com,larry@jsconnor.com,bulk - -# See http://bmsi.com/python/dspam.html -[dspam] -# Select a well moderated dspam dictionary to reject spammy headers. -# To filter on the entire message, use the full setup below. -# only EXTERNAL messages are dspam filtered -;dspam_dict=/var/lib/dspam/moderator.dict - -# Recipients of mail sent from these senders are added to the auto_whitelist. -# Auto_whitelisted senders with an SPF PASS are never rejected by dspam, and -# messages from auto_whitelisted senders will be used to train screener -# dictionaries as innocent mail. -;whitelist_senders = @mycorp.com - -# Opt-out recipients entirely from dspam screening and header triage -;dspam_exempt=getitall@mycorp.com -# Do not scan mail (ostensibly) from these senders -;dspam_whitelist=getitall@sender.com -# Reject spam to these domains instead of quarantining it. -;dspam_reject=othercorp.com -# Scan internal mail - often a good source of stats on legit mail. -;dspam_internal=1 - -# directory for dspam user quarantine, signature db, and dictionaries -# defining this activates the dspam application -# dspam and dspam-python must be installed -;dspam_userdir=/var/lib/dspam -# do not dspam messages larger than this -;dspam_sizelimit=180000 - -# Map email addresses and aliases to dspam users -;dspam_users=david,goliath,spam,falsepositive -# List dspam users which train on all delivered messages, as opposed to -# "train on error" which trains only when a spam or falsepositive is reported. -# Training mode will build the dictionary faster, but requires close attention -# so as not to miss any spam or false positives. -;dspam_train=goliath -;david=david@foocorp.com,david.yelnetz@foocorp.com,david@bar.foocorp.com -;goliath=giant@foocorp.com,goliath.philistine@foocorp.com -# address to forward spam to. milter will process these and not deliver -;spam=spam@foocorp.com -# address to forward false positives to. milter will process and not deliver -;falsepositive=ham@foocorp.com -# account which receives only spam: all received messages are marked as spam. -;honeypot=spam-me@example.com -# the dspam_screener is a list of dspam users who screen mail for all -# recipients who are not dspam_users. Spam goes to the screeners quarantine, -# and the original recipients are saved so that false positives can be properly -# delivered. -;dspam_screener=david,goliath -# The dspam CGI can also be used: logins must match dspam users - -# Optional pygossip interface -# -# GOSSiP tracks reputation of domain:qualifier pairs. For instance, -# the reputation of example.com:SPF is tracked separately from -# example.com:neutral. Currently qualifiers are -# SPF,neutral,softfail,fail,permerror,GUESS,HELO -[gossip] -# Use a dedicated GOSSiP server. If not specified, a local database -# will be used. -;server=host:11900 -# To include peers of a peer in reputation, set ttl=2 -;ttl=1 -# If a local database is used, also consult these GOSSiP servers about -# domains. Peer reputation is also tracked as to how often they -# agree with us, and weighted accordingly. -;peers=host1:port,host2 - -[greylist] -dbfile=greylist.db -# mins (Google retries in 5 mins) -time=5 -# hours (some legit sites don't retry for 6 hours) -expire=6 -# days (keep "first monday" type mailings on file) -retain=36 diff --git a/milter.rc b/milter.rc deleted file mode 100755 index 17288f3..0000000 --- a/milter.rc +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -# -# milter This shell script takes care of starting and stopping milter. -# -# chkconfig: 2345 80 30 -# description: Milter is a process that filters messages sent through sendmail. -# processname: milter -# config: /etc/mail/pymilter.cfg -# pidfile: /var/run/milter/milter.pid - -python="python2.4" - -pidof() { - set - "" - if set - `ps -e -o pid,cmd | grep "${python} bms.py"` && - [ "$2" != "grep" ]; then - echo $1 - return 0 - fi - return 1 -} - -# Source function library. -. /etc/rc.d/init.d/functions - -[ -x /usr/lib/pymilter/start.sh ] || exit 0 - -RETVAL=0 -prog="milter" - -start() { - # Start daemons. - - echo -n "Starting $prog: " - if ! test -d /var/run/milter; then - mkdir -p /var/run/milter - chown mail:mail /var/run/milter - fi - daemon --check milter --user mail /usr/lib/pymilter/start.sh milter bms - RETVAL=$? - echo - [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter - return $RETVAL -} - -stop() { - # Stop daemons. - echo -n "Shutting down $prog: " - # Find pid. - pid= - base="milter" - if [ -f /var/run/milter/milter.pid ]; then - local line p - read line < /var/run/milter/milter.pid - for p in $line ; do - [ -z "${p//[0-9]/}" -a -d "/proc/$p" ] && pid="$pid $p" - done - fi - if test -n "$pid"; then - checkpid $pid && kill "$pid" - for i in 1 2 3 4 5 6 7 8 9 0; do - checkpid $pid && sleep 2 || break - done - if checkpid $pid; then - failure $"$base shutdown" - RETVAL=1 - else - success $"$base shutdown" - RETVAL=0 - fi - else - killproc -d 9 milter - RETVAL=$? - fi - echo - [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter - return $RETVAL -} - -# See how we were called. -case "$1" in - start) - start - ;; - stop) - stop - ;; - restart|reload) - stop - start - RETVAL=$? - ;; - condrestart) - if [ -f /var/lock/subsys/milter ]; then - stop - start - RETVAL=$? - fi - ;; - status) - status milter - RETVAL=$? - ;; - *) - echo "Usage: $0 {start|stop|restart|condrestart|status}" - exit 1 -esac - -exit $RETVAL diff --git a/milter.rc7 b/milter.rc7 deleted file mode 100755 index 8fdebe7..0000000 --- a/milter.rc7 +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash -# -# milter This shell script takes care of starting and stopping milter. -# -# chkconfig: 2345 80 30 -# description: Milter is a process that filters messages sent through sendmail. -# processname: milter -# config: /etc/mail/pymilter.cfg -# pidfile: /var/run/milter/milter.pid - -python="python2.4" - -pidof() { - set - "" - if set - `ps -e -o pid,wchan,cmd | grep "rt_sig ${python} bms.py"` && - [ "$3" != "grep" ]; then - echo $1 - return 0 - fi - return 1 -} - -# Source function library. -. /etc/rc.d/init.d/functions - -[ -x /usr/lib/pymilter/start.sh ] || exit 0 - -RETVAL=0 -prog="milter" - -start() { - # Start daemons. - - echo -n "Starting $prog: " - if ! test -d /var/run/milter; then - mkdir -p /var/run/milter - chown mail:mail /var/run/milter - fi - daemon --check milter --user mail /usr/lib/pymilter/start.sh milter bms - RETVAL=$? - echo - [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter - return $RETVAL -} - -stop() { - # Stop daemons. - echo -n "Shutting down $prog: " - killproc milter - RETVAL=$? - echo - [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter - return $RETVAL -} - -# See how we were called. -case "$1" in - start) - start - ;; - stop) - stop - ;; - restart|reload) - stop - start - RETVAL=$? - ;; - condrestart) - if [ -f /var/lock/subsys/milter ]; then - stop - start - RETVAL=$? - fi - ;; - status) - status milter - RETVAL=$? - ;; - *) - echo "Usage: $0 {start|stop|restart|condrestart|status}" - exit 1 -esac - -exit $RETVAL diff --git a/miltermodule.c b/miltermodule.c index da52766..cd5b397 100644 --- a/miltermodule.c +++ b/miltermodule.c @@ -35,6 +35,9 @@ $ python setup.py help libraries=["milter","smutil","resolv"] * $Log$ + * Revision 1.14 2008/12/04 19:43:00 customdesigned + * Doc updates. + * * Revision 1.13 2008/11/23 03:06:47 customdesigned * Milter support for chgfrom. * @@ -188,10 +191,10 @@ $ python setup.py help #endif #define _FFR_MULTILINE (MAX_ML_REPLY > 1) -#include -#include -#include -#include +//#include // shouldn't be needed - use Python API +#include // Python C API +#include // libmilter API +#include // socket API /* See if we have IPv4 and/or IPv6 support in this OS and in diff --git a/neutral.txt b/neutral.txt deleted file mode 100644 index dbc40cc..0000000 --- a/neutral.txt +++ /dev/null @@ -1,39 +0,0 @@ -To: %(sender)s -From: postmaster@%(receiver)s -Subject: SPF %(result)s (POSSIBLE FORGERY) -Auto-Submitted: auto-generated (sender verification) - -This is an automatically generated Delivery Status Notification. - -THIS IS A WARNING MESSAGE ONLY. - -YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. - -Delivery to the following recipients has been delayed. - - %(rcpt)s - -Subject: %(subject)s -Received-SPF: %(spf_result)s - -Your sender policy (or lack thereof) indicated that the above email was not -sent via an authorized SMTP server, but may still be legitimate. Since there -is no positive confirmation that the message is really from you, we have -to give it extra scrutiny - including verifying that the sender really -exists by sending you this DSN. We will remember this sender and not -bother you again for a while. You can avoid this message entirely for -legitimate mail by using an authorized SMTP server. Contact your mail -administrator and ask how to configure your email client to use an -authorized server. - -If you never sent the above message, then your domain has been forged. -Your mail admin needs to publish a strict SPF record so that I can reject -those forgeries instead of bugging you about them. - -See http://openspf.org for details. - -If you need further assistance, please do not hesitate to contact me. - -Kind regards, - -postmaster@%(receiver)s diff --git a/permerror.txt b/permerror.txt deleted file mode 100644 index 718ce83..0000000 --- a/permerror.txt +++ /dev/null @@ -1,35 +0,0 @@ -To: %(sender)s -From: postmaster@%(receiver)s -Subject: Critical SPF configuration error -Auto-Submitted: auto-generated (configuration error) - -This is an automatically generated Delivery Status Notification. - -THIS IS A WARNING MESSAGE ONLY. - -YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. - -Delivery to the following recipients has been delayed. - - %(rcpt)s - -Subject: %(subject)s -Received-SPF: %(spf_result)s - -Your spf record has a permanent error. The error was: - - %(perm_error)s - -We will reinterpret your record using "lax" processing heuristics -which may result in your mail being accepted anyway. But you or your -mail administrator need to fix your SPF record as soon as possible. - -We are sending you this message to alert you to the fact that -you have problems with your email configuration. - -If you need further assistance, please do not hesitate to -contact me again. - -Kind regards, - -postmaster@%(receiver)s diff --git a/pymilter.spec b/pymilter.spec index 3fcd891..e403712 100644 --- a/pymilter.spec +++ b/pymilter.spec @@ -1,198 +1,10 @@ -# This spec file contains 2 noarch packages in addition to the pymilter -# module. To compile all three on 32-bit Intel, use: -# rpmbuild -ba --target=i386,noarch pymilter.spec - %define __python python2.4 %define version 0.8.12 %define release 1%{?dist}.py24 -# what version of RH are we building for? +%define libdir %{_libdir}/pymilter +%define name pymilter %define redhat7 0 -# Options for Redhat version 6.x: -# rpm -ba|--rebuild --define "rh7 1" -%{?rh7:%define redhat7 1} - -# some systems dont have initrddir defined -%{?_initrddir:%define _initrddir /etc/rc.d/init.d} - -%if %{redhat7} -# Redhat 7.x and earlier (multiple ps lines per thread) -%define sysvinit milter.rc7 -%else -%define sysvinit milter.rc -%endif -# RH9, other systems (single ps line per process) -%ifos aix4.1 -%define libdir /var/log/milter -%else -%define libdir %{_libdir}/pymilter -%endif - -%ifarch noarch -Name: milter -Group: Applications/System -Summary: BMS spam and reputation milter -Version: %{version} -Release: %{release} -Source: pymilter-%{version}.tar.gz -#Patch: %{name}-%{version}.patch -License: GPLv2+ -Group: Development/Libraries -BuildRoot: %{_tmppath}/%{name}-buildroot -Vendor: Stuart D. Gathman -Url: http://www.bmsi.com/python/milter.html -Requires: %{__python} >= 2.4, pyspf >= 2.0.4, pymilter -%ifos Linux -Requires: chkconfig -%endif - -%description -n milter -A complex but effective spam filtering, SPF checking, greylisting, -and reputation tracking mail application. It uses pydspam if installed for -bayesian filtering. - -%package spf -Group: Applications/System -Summary: BMS spam and reputation milter -Requires: pyspf >= 2.0.4, pymilter -Obsoletes: pymilter-spf < 0.8.10 - -%description spf -A simple mail filter to add Received-SPF headers and reject forged mail. -Rejection policy is configured via sendmail access file and can be -tailored by domain. - -%prep -%setup -q -n pymilter-%{version} -#patch -p0 -b .bms - -%install -rm -rf $RPM_BUILD_ROOT -mkdir -p $RPM_BUILD_ROOT/var/log/milter -mkdir -p $RPM_BUILD_ROOT/etc/mail -mkdir $RPM_BUILD_ROOT/var/log/milter/save -mkdir -p $RPM_BUILD_ROOT%{libdir} -cp *.txt $RPM_BUILD_ROOT/var/log/milter -cp -p bms.py spfmilter.py ban2zone.py $RPM_BUILD_ROOT%{libdir} -cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg -cp spfmilter.cfg $RPM_BUILD_ROOT/etc/mail - -# logfile rotation -mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d -cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF' -/var/log/milter/milter.log { - copytruncate - compress -} -/var/log/milter/banned_ips { - rotate 7 - daily - copytruncate -} -EOF - -# purge saved defanged message copies -mkdir -p $RPM_BUILD_ROOT/etc/cron.daily -%ifos aix4.1 -R= -%else -R='-r' -%endif -cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF' -#!/bin/sh - -find /var/log/milter/save -mtime +7 | xargs $R rm -# work around memory leak -/etc/init.d/milter condrestart -EOF -chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter - -%ifnos aix4.1 -mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d -cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter -cp spfmilter.rc $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter -ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF' -/^python=/ -c -python="%{__python}" -. -w -q -EOF -ed $RPM_BUILD_ROOT/etc/rc.d/init.d/spfmilter <<'EOF' -/^python=/ -c -python="%{__python}" -. -w -q -EOF -%endif # aix4.1 - -mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack -cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack - -%ifos aix4.1 -%post -mkssys -s milter -p %{libdir}/start.sh -u 25 -S -n 15 -f 9 -G mail || : - -%preun -if [ $1 = 0 ]; then - rmssys -s milter || : -fi -%else # not aix4.1 -%post -n milter -#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf" -/sbin/chkconfig --add milter - -%preun -n milter -if [ $1 = 0 ]; then - /sbin/chkconfig --del milter -fi -%post spf -#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf" -/sbin/chkconfig --add spfmilter - -%preun spf -if [ $1 = 0 ]; then - /sbin/chkconfig --del spfmilter -fi -%endif # aix4.1 - -%files -%defattr(-,root,root) -/etc/logrotate.d/milter -/etc/cron.daily/milter -%ifos aix4.1 -%defattr(-,smmsp,mail) -%else -/etc/rc.d/init.d/milter -%defattr(-,mail,mail) -%endif -%dir /var/log/milter -%dir /var/log/milter/save -%{libdir}/bms.py -%{libdir}/ban2zone.py -%config(noreplace) /var/log/milter/strike3.txt -%config(noreplace) /var/log/milter/softfail.txt -%config(noreplace) /var/log/milter/fail.txt -%config(noreplace) /var/log/milter/neutral.txt -%config(noreplace) /var/log/milter/quarantine.txt -%config(noreplace) /var/log/milter/permerror.txt -%config(noreplace) /var/log/milter/temperror.txt -%config(noreplace) /etc/mail/pymilter.cfg -/usr/share/sendmail-cf/hack/rhsbl.m4 - -%files spf -%defattr(-,root,root) -%dir /var/log/milter -%{libdir}/spfmilter.py* -%config(noreplace) /etc/mail/spfmilter.cfg -/etc/rc.d/init.d/spfmilter - -%else # not noarch - -%define name pymilter Summary: Python interface to sendmail milter API Name: %{name} Version: %{version} @@ -260,37 +72,19 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh %config %{libdir}/start.sh %dir %attr(0755,mail,mail) /var/run/milter -%endif # noarch - %clean rm -rf $RPM_BUILD_ROOT %changelog * Mon Nov 24 2008 Stuart Gathman 0.8.12-1 +- Split pymilter into its own CVS module - Support chgfrom and addrcpt_par -- 2 demerits for HELO after MAIL FROM - Support NS records in Milter.dns -- Make initscript use pid file. -- Fix greylist config -- SPF Pass policy -* Sat Oct 11 2008 Stuart Gathman 0.8.11-1 -- Support greylisting -- Recognize vacation messages as autoreplies. -- Never ban a trusted relay. -- Missing global reading banned_ips -- ban2zone.py * Mon Aug 25 2008 Stuart Gathman 0.8.10-2 - /var/run/milter directory must be owned by mail * Mon Aug 25 2008 Stuart Gathman 0.8.10-1 -- log rcpt for SRS rejections - improved parsing into email and fullname (still 2 self test failures) - implement no-DSN CBV, reduce full DSNs -- check for porn words in MAIL FROM fullname -- ban IP for too many bad MAIL FROMs or RCPT TOs -- temperror policy in access -- no CBV for whitelisted MAIL FROM except permerror, softfail -- Allow explicitly whitelisted email from banned_users. -- configure gossip TTL * Mon Sep 24 2007 Stuart Gathman 0.8.9-1 - Use ifarch hack to build milter and milter-spf packages as noarch - Remove spf dependency from dsn.py, add dns.py @@ -298,167 +92,9 @@ rm -rf $RPM_BUILD_ROOT - 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 0.8.7-1 -- More lame bounce heuristics - SPF moved to pyspf RPM -- wiretap archive option -- Do plain CBV if missing template -- SMTP AUTH policy in access * Tue May 23 2006 Stuart Gathman 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 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 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 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 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 0.8.3-1 -- Keep screened honeypot mail, but optionally discard honeypot only mail. -- spf_accept_fail option for braindead SPF senders (treats fail like softfail) -- Consider SMTP AUTH connections internal. -- Send DSN for SPF errors corrected by extended processing. -- Send DSN before SCREENED mail is quarantined -- Option to set SPF policy via sendmail access map. -- Option to supply Sender header from MAIL FROM when missing. -- Use logging package to keep log lines atomic. -* Fri Jul 15 2005 Stuart Gathman 0.8.2-4 -- Limit each CNAME chain independently like PTR and MX -* Fri Jul 15 2005 Stuart Gathman 0.8.2-3 -- Limit CNAME lookups (regression) -* Fri Jul 15 2005 Stuart Gathman 0.8.2-2 -- Handle corrupt ZIP attachments -* Fri Jul 15 2005 Stuart Gathman 0.8.2-1 -- Strict processing limits per SPF RFC -- Fixed several parsing bugs under RFC -- Support official IANA SPF record (type99) -- Honeypot support (requires pydspam-1.1.9) -- Extended SPF processing results beyond strict RFC limits -- Support original SES for local bounce protection (requires pysrs-0.30.10) -- Callback exception processing option in milter module -* Thu Jun 16 2005 Stuart Gathman 0.8.1-1 -- Fix zip in zip loop in mime.py -- Fix HeaderParseError in bms.py header callback -- Check internal_domains for outgoing mail -- Fix inconsistent results from send_dsn -* Mon Jun 06 2005 Stuart Gathman 0.8.0-3 -- properly log pydspam exceptions -* Sat Jun 04 2005 Stuart Gathman 0.8.0-2 -- Include default softfail, strike3 templates -* Wed May 25 2005 Stuart Gathman 0.8.0-1 -- Move Milter module to subpackage. -- DSN support for Three strikes rule and SPF SOFTFAIL -- Move /*mime*/ and dynip to Milter subpackage -- Fix SPF unknown mechanism list not cleared -- Make banned extensions configurable. -- Option to scan zipfiles for bad extensions. -* Tue Feb 08 2005 Stuart Gathman 0.7.3-1.EL3 -- Support EL3 and Python2.4 (some scanning/defang support broken) -* Mon Aug 30 2004 Stuart Gathman 0.7.2-1 -- Fix various SPF bugs -- Recognize dynamic PTR names, and don't count them as authentication. -- Three strikes and yer out rule. -- Block softfail by default unless valid PTR or HELO -- Return unknown for null mechanism -- Return unknown for invalid ip address in mechanism -- Try best guess on HELO also -- Expand setreply for common errors -- make rhsbl.m4 hack available for sendmail.mc -* Sun Aug 22 2004 Stuart Gathman 0.7.1-1 -- Handle modifying mislabeled multipart messages without an exception -- Support setbacklog, setmlreply -- allow multi-recipient CBV -- return TEMPFAIL for SPF softfail -* Fri Jul 23 2004 Stuart Gathman 0.7.0-1 -- SPF check hello name -- Move pythonsock to /var/run/milter -- Move milter.cfg to /etc/mail/pymilter.cfg -- Check M$ style XML CID records by converting to SPF -- Recognize, but never match ip6 until we properly support it. -- Option to reject when no PTR and no SPF -* Fri Apr 09 2004 Stuart Gathman 0.6.9-1 -- Validate spf.py against test suite, and add Received-SPF support to spf.py -- Support best_guess for SPF -- Reject numeric hello names -- Preserve case of local part in sender -- Make libmilter timeout a config option -- Fix setup.py to work with python < 2.2.3 -* Tue Apr 06 2004 Stuart Gathman 0.6.8-3 -- Reject invalid SRS immediately for benefit of callback verifiers -- Fix include bug in spf.py -* Tue Apr 06 2004 Stuart Gathman 0.6.8-2 -- Bug in check_header -* Mon Apr 05 2004 Stuart Gathman 0.6.8-1 -- Don't report spoofed unless rcpt looks like SRS -- Check for bounce with multiple rcpts -- Make dspam see Received-SPF headers -- Make sysv init work with RH9 -* Thu Mar 25 2004 Stuart Gathman 0.6.7-3 -- Forgot to make spf_reject_neutral global in bms.py -* Wed Mar 24 2004 Stuart Gathman 0.6.7-2 -- Defang message/rfc822 content_type with boundary -- Support SPF delegation -- Reject neutral SPF result for selected domains -* Tue Mar 23 2004 Stuart Gathman 0.6.7-1 -- SRS forgery check. Detect thread resource starvation. -- Properly remove local socket with explicit type. -- Decode obfuscated subject headers. -* Wed Mar 11 2004 Stuart Gathman 0.6.6-2 -- init script bug with python2.3 -* Wed Mar 10 2004 Stuart Gathman 0.6.6-1 -- SPF checking, hello blacklist -* Mon Mar 08 2004 Stuart Gathman 0.6.5-2 -- memory leak in envfrom and envrcpt -* Mon Mar 01 2004 Stuart Gathman 0.6.5-1 -- progress notification -- memory leak in connect -- trusted relay -* Thu Feb 19 2004 Stuart Gathman 0.6.4-2 -- smart alias wildcard patch, compile for sendmail-8.12 -* Thu Dec 04 2003 Stuart Gathman 0.6.4-1 -- many fixes for dspam support -* Wed Oct 22 2003 Stuart Gathman 0.6.3 -- dspam SCREEN feature -- streamline dspam false positive handling -* Mon Sep 01 2003 Stuart Gathman 0.6.1 -- Full dspam support added -* Mon Aug 26 2003 Stuart Gathman -- Use New email module -* Fri Jun 27 2003 Stuart Gathman -- Add dspam module diff --git a/quarantine.txt b/quarantine.txt deleted file mode 100644 index b060d75..0000000 --- a/quarantine.txt +++ /dev/null @@ -1,29 +0,0 @@ -To: %(sender)s -From: postmaster@%(receiver)s -Subject: DELIVERY STATUS (POSSIBLE SPAM) -Auto-Submitted: auto-generated (content analysis) - -This is an automatically generated Delivery Status Notification. - -THIS IS A WARNING MESSAGE ONLY. - -YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. - -Delivery to the following recipients has been delayed. - - %(rcpt)s - -Subject: %(subject)s -Received-SPF: %(spf_result)s - -A statistical analysis of your message has classified it as junk mail, -and it has been quarantined. Eventually, the recipients will review -their quarantined mail and may notice your message. If your message is -important, please contact them via other means. You may also try sending -them a simple plain text message. - -If you need further assistance, please do not hesitate to contact me. - -Kind regards, - -postmaster@%(receiver)s diff --git a/softfail.txt b/softfail.txt deleted file mode 100644 index 62c9834..0000000 --- a/softfail.txt +++ /dev/null @@ -1,28 +0,0 @@ -To: %(sender)s -From: postmaster@%(receiver)s -Subject: SPF %(result)s (POSSIBLE FORGERY) -Auto-Submitted: auto-generated (configuration error) - -This is an automatically generated Delivery Status Notification. - -THIS IS A WARNING MESSAGE ONLY. - -YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. - -Delivery to the following recipients has been delayed. - - %(rcpt)s - -Subject: %(subject)s -Received-SPF: %(spf_result)s - -Your sender policy indicated that the above email was likely forged and that -feedback was desired for debugging. If you are sending from a foreign ISP, -then you may need to follow your home ISPs instructions for configuring -your outgoing mail server. - -If you need further assistance, please do not hesitate to contact me. - -Kind regards, - -postmaster@%(receiver)s diff --git a/spfmilter.cfg b/spfmilter.cfg deleted file mode 100644 index 52f4d34..0000000 --- a/spfmilter.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[milter] -# The socket used to communicate with sendmail -socketname = /var/run/milter/spfmiltersock -# Name of the milter given to sendmail -name = pyspffilter -# Trusted relays such as secondary MXes that should not have SPF checked. -;trusted_relay = -# Internal networks that should not have SPF checked. -internal_connect = 127.0.0.1,192.168.0.0/16,10.0.0.0/8 - -# See http://www.openspf.com for more info on SPF. -[spf] -# Use sendmail access map or similar format for detailed spf policy. -# SPF entries in the access map will override defaults. -access_file = /etc/mail/access.db -# Connections that get an SPF pass for a pretend MAIL FROM of -# postmaster@sometrustedforwarder.com skip SPF checks for the real MAIL FROM. -# This is for non-SRS forwarders. It is a simple implementation that -# is inefficient for more than a few entries. -;trusted_forwarder = careerbuilder.com diff --git a/spfmilter.py b/spfmilter.py deleted file mode 100644 index 3599d10..0000000 --- a/spfmilter.py +++ /dev/null @@ -1,253 +0,0 @@ -# A simple SPF milter. -# You must install pyspf for this to work. - -# http://www.sendmail.org/doc/sendmail-current/libmilter/docs/installation.html - -# Author: Stuart D. Gathman -# 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 . - 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 . - 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" diff --git a/spfmilter.rc b/spfmilter.rc deleted file mode 100755 index e61c3f0..0000000 --- a/spfmilter.rc +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash -# -# spfmilter This shell script takes care of starting and stopping spfmilter. -# -# chkconfig: 2345 80 30 -# description: a process that checks SPF for messages sent through sendmail. -# processname: spfmilter -# config: /etc/mail/spfmilter.cfg -# pidfile: /var/run/milter/spfmilter.pid - -python="python2.4" - -pidof() { - set - "" - if set - `ps -e -o pid,cmd | grep "${python} spfmilter.py"` && - [ "$2" != "grep" ]; then - echo $1 - return 0 - fi - return 1 -} - -# Source function library. -. /etc/rc.d/init.d/functions - -[ -x /usr/lib/pymilter/start.sh ] || exit 0 - -RETVAL=0 -prog="spfmilter" - -start() { - # Start daemons. - - echo -n "Starting $prog: " - if ! test -d /var/run/milter; then - mkdir -p /var/run/milter - chown mail:mail /var/run/milter - fi - daemon --check milter --user mail /usr/lib/pymilter/start.sh spfmilter - RETVAL=$? - echo - [ $RETVAL -eq 0 ] && touch /var/lock/subsys/spfmilter - return $RETVAL -} - -stop() { - # Stop daemons. - echo -n "Shutting down $prog: " - killproc milter - RETVAL=$? - echo - [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/spfmilter - return $RETVAL -} - -# See how we were called. -case "$1" in - start) - start - ;; - stop) - stop - ;; - restart|reload) - stop - start - RETVAL=$? - ;; - condrestart) - if [ -f /var/lock/subsys/spfmilter ]; then - stop - start - RETVAL=$? - fi - ;; - status) - status spfmilter - RETVAL=$? - ;; - *) - echo "Usage: $0 {start|stop|restart|condrestart|status}" - exit 1 -esac - -exit $RETVAL diff --git a/strike3.txt b/strike3.txt deleted file mode 100644 index 43f4ac1..0000000 --- a/strike3.txt +++ /dev/null @@ -1,69 +0,0 @@ -To: %(sender)s -From: postmaster@%(receiver)s -Subject: Critical mail server configuration error -Auto-Submitted: auto-generated (configuration error) - -This is an automatically generated Delivery Status Notification. - -THIS IS A WARNING MESSAGE ONLY. - -YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. - -Delivery to the following recipients has been delayed. - - %(rcpt)s - -Subject: %(subject)s - -Someone at IP address %(connectip)s sent an email claiming -to be from %(sender)s. - -If that wasn't you, then your domain, %(sender_domain)s, -was forged - i.e. used without your knowlege or authorization by -someone attempting to steal your mail identity. This is a very -serious problem, and you need to provide authentication for your -SMTP (email) servers to prevent criminals from forging your -domain. The simplest step is usually to publish an SPF record -with your Sender Policy. - -For more information, see: http://openspf.org - -I hate to annoy you with a DSN (Delivery Status -Notification) from a possibly forged email, but since you -have not published a sender policy, there is no other way -of bringing this to your attention. - -If it *was* you that sent the email, then your email domain -or configuration is in error. If you don't know anything -about mail servers, then pass this on to your SMTP (mail) -server administrator. We have accepted the email anyway, in -case it is important, but we couldn't find anything about -the mail submitter at %(connectip)s to distinguish it from a -zombie (compromised/infected computer - usually a Windows -PC). There was no PTR record for its IP address (PTR names -that contain the IP address don't count). RFC2821 requires -that your hello name be a FQN (Fully Qualified domain Name, -i.e. at least one dot) that resolves to the IP address of -the mail sender. In addition, just like for PTR, we don't -accept a helo name that contains the IP, since this doesn't -help to identify you. The hello name you used, -%(heloname)s, was invalid. - -Furthermore, there was no SPF record for the sending domain -%(sender_domain)s. We even tried to find its IP in any A or -MX records for your domain, but that failed also. We really -should reject mail from anonymous mail clients, but in case -it is important, we are accepting it anyway. - -We are sending you this message to alert you to the fact that - -Either - Someone is forging your domain. -Or - You have problems with your email configuration. -Or - Possibly both. - -If you need further assistance, please do not hesitate to -contact me again. - -Kind regards, - -postmaster@%(receiver)s diff --git a/temperror.txt b/temperror.txt deleted file mode 100644 index 1d1c9fa..0000000 --- a/temperror.txt +++ /dev/null @@ -1,33 +0,0 @@ -To: %(sender)s -From: postmaster@%(receiver)s -Subject: Critical DNS configuration error -Auto-Submitted: auto-generated (configuration error) - -This is an automatically generated Delivery Status Notification. - -THIS IS A WARNING MESSAGE ONLY. - -YOU DO *NOT* NEED TO RESEND YOUR MESSAGE. - -Delivery to the following recipients has been delayed. - - %(rcpt)s - -Subject: %(subject)s -Received-SPF: %(spf_result)s - -Your DNS server is not responding to TXT queries. In other words, -it is BROKEN. You need to get somebody to fix it ASAP. We -are attempting to do TXT queries to see if you have an SPF record. - -See http://openspf.org - -We are sending you this message to alert you to the fact that -you have problems with your DNS. - -If you need further assistance, please do not hesitate to -contact me again. - -Kind regards, - -postmaster@%(receiver)s diff --git a/test.py b/test.py index 8da1710..b52f367 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,4 @@ import unittest -import testbms import testmime import testsample import testutils @@ -7,7 +6,6 @@ import os def suite(): s = unittest.TestSuite() - s.addTest(testbms.suite()) s.addTest(testmime.suite()) s.addTest(testsample.suite()) s.addTest(testutils.suite()) diff --git a/testbms.py b/testbms.py deleted file mode 100644 index 50ee2df..0000000 --- a/testbms.py +++ /dev/null @@ -1,323 +0,0 @@ -import unittest -import doctest -import Milter -import bms -import mime -import rfc822 -import StringIO -import email -import sys -#import pdb - -class TestMilter(bms.bmsMilter): - - def __init__(self): - bms.bmsMilter.__init__(self) - self.logfp = open("test/milter.log","a") - self._delrcpt = [] # record deleted rcpts for testing - self._addrcpt = [] # record added rcpts for testing - - def log(self,*msg): - for i in msg: print >>self.logfp, i, - print >>self.logfp - - def getsymval(self,name): - if name == 'j': return 'test.milter.org' - return '' - - def replacebody(self,chunk): - if self._body: - self._body.write(chunk) - self.bodyreplaced = True - else: - raise IOError,"replacebody not called from eom()" - - # FIXME: rfc822 indexing does not really reflect the way chg/add header - # work for a milter - def chgheader(self,field,idx,value): - if not self._body: - raise IOError,"chgheader not called from eom()" - self.log('chgheader: %s[%d]=%s' % (field,idx,value)) - if value == '': - del self._msg[field] - else: - self._msg[field] = value - self.headerschanged = True - - def addheader(self,field,value,idx=-1): - if not self._body: - raise IOError,"addheader not called from eom()" - self.log('addheader: %s=%s' % (field,value)) - self._msg[field] = value - self.headerschanged = True - - def delrcpt(self,rcpt): - if not self._body: - raise IOError,"delrcpt not called from eom()" - self._delrcpt.append(rcpt) - - def addrcpt(self,rcpt): - if not self._body: - raise IOError,"addrcpt not called from eom()" - self._addrcpt.append(rcpt) - - def setreply(self,rcode,xcode,msg): - self.reply = (rcode,xcode,msg) - - def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com"): - self._body = None - self.bodyreplaced = False - self.headerschanged = False - self.reply = None - msg = rfc822.Message(fp) - rc = self.envfrom('<%s>'%sender) - if rc != Milter.CONTINUE: return rc - rc = self.envrcpt('<%s>'%rcpt) - if rc != Milter.CONTINUE: return rc - line = None - for h in msg.headers: - if h[:1].isspace(): - line = line + h - continue - if not line: - line = h - continue - s = line.split(': ',1) - if len(s) > 1: val = s[1].strip() - else: val = '' - rc = self.header(s[0],val) - if rc != Milter.CONTINUE: return rc - line = h - if line: - s = line.split(': ',1) - rc = self.header(s[0],s[1]) - if rc != Milter.CONTINUE: return rc - rc = self.eoh() - if rc != Milter.CONTINUE: return rc - while 1: - buf = fp.read(8192) - if len(buf) == 0: break - rc = self.body(buf) - if rc != Milter.CONTINUE: return rc - self._msg = msg - self._body = StringIO.StringIO() - rc = self.eom() - if self.bodyreplaced: - body = self._body.getvalue() - else: - msg.rewindbody() - body = msg.fp.read() - self._body = StringIO.StringIO() - self._body.writelines(msg.headers) - self._body.write('\n') - self._body.write(body) - return rc - - def feedMsg(self,fname,sender="spam@adv.com",rcpt="victim@lamb.com"): - fp = open('test/'+fname,'r') - rc = self.feedFile(fp,sender,rcpt) - fp.close() - return rc - - def connect(self,host='localhost'): - self._body = None - self.bodyreplaced = False - rc = bms.bmsMilter.connect(self,host,1,('1.2.3.4',1234)) - if rc != Milter.CONTINUE and rc != Milter.ACCEPT: - self.close() - return rc - rc = self.hello('spamrelay') - if rc != Milter.CONTINUE: - self.close() - return rc - -class BMSMilterTestCase(unittest.TestCase): - - def testDefang(self,fname='virus1'): - milter = TestMilter() - rc = milter.connect('testDefang') - self.assertEqual(rc,Milter.CONTINUE) - rc = milter.feedMsg(fname) - self.assertEqual(rc,Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") - fp = milter._body - open('test/'+fname+".tstout","w").write(fp.getvalue()) - #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) - fp.seek(0) - msg = mime.message_from_file(fp) - str = msg.get_payload(1).get_payload() - milter.log(str) - milter.close() - - # test some spams that crashed our parser - def testParse(self,fname='spam7'): - milter = TestMilter() - milter.connect('testParse') - rc = milter.feedMsg(fname) - self.assertEqual(rc,Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") - fp = milter._body - open('test/'+fname+".tstout","w").write(fp.getvalue()) - milter.connect('pro-send.com') - rc = milter.feedMsg('spam8') - self.assertEqual(rc,Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") - rc = milter.feedMsg('bounce') - self.assertEqual(rc,Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") - rc = milter.feedMsg('bounce1') - self.assertEqual(rc,Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") - milter.close() - - def testDefang2(self): - milter = TestMilter() - milter.connect('testDefang2') - rc = milter.feedMsg('samp1') - self.assertEqual(rc,Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") - rc = milter.feedMsg("virus3") - self.assertEqual(rc,Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") - fp = milter._body - open("test/virus3.tstout","w").write(fp.getvalue()) - #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read()) - rc = milter.feedMsg("virus6") - self.assertEqual(rc,Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") - self.failUnless(milter.headerschanged,"Message headers not adjusted") - fp = milter._body - open("test/virus6.tstout","w").write(fp.getvalue()) - milter.close() - - def testDefang3(self): - milter = TestMilter() - milter.connect('testDefang3') - # test script removal on complex HTML attachment - rc = milter.feedMsg('amazon') - self.assertEqual(rc,Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") - fp = milter._body - open("test/amazon.tstout","w").write(fp.getvalue()) - # test defanging Klez virus - rc = milter.feedMsg("virus13") - self.assertEqual(rc,Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") - fp = milter._body - open("test/virus13.tstout","w").write(fp.getvalue()) - # test script removal on quoted-printable HTML attachment - # sgmllib can't handle the syntax - rc = milter.feedMsg('spam44') - self.assertEqual(rc,Milter.ACCEPT) - self.failIf(milter.bodyreplaced,"Message body replaced") - fp = milter._body - open("test/spam44.tstout","w").write(fp.getvalue()) - milter.close() - - def testRFC822(self): - milter = TestMilter() - milter.connect('testRFC822') - # test encoded rfc822 attachment - #pdb.set_trace() - rc = milter.feedMsg('test8') - self.assertEqual(rc,Milter.ACCEPT) - # python2.4 doesn't scan encoded message attachments - if sys.hexversion < 0x02040000: - self.failUnless(milter.bodyreplaced,"Message body not replaced") - #self.failIf(milter.bodyreplaced,"Message body replaced") - fp = milter._body - open("test/test8.tstout","w").write(fp.getvalue()) - rc = milter.feedMsg('virus7') - self.assertEqual(rc,Milter.ACCEPT) - self.failUnless(milter.bodyreplaced,"Message body not replaced") - #self.failIf(milter.bodyreplaced,"Message body replaced") - fp = milter._body - open("test/virus7.tstout","w").write(fp.getvalue()) - - def testSmartAlias(self): - milter = TestMilter() - milter.connect('testSmartAlias') - # test smart alias feature - key = ('foo@example.com','baz@bat.com') - bms.smart_alias[key] = ['ham@eggs.com'] - rc = milter.feedMsg('test8',key[0],key[1]) - self.assertEqual(rc,Milter.ACCEPT) - self.failUnless(milter._delrcpt == ['']) - self.failUnless(milter._addrcpt == ['']) - # python2.4 email does not decode message attachments, so script - # is not replaced - if sys.hexversion < 0x02040000: - self.failUnless(milter.bodyreplaced,"Message body not replaced") - - def testBadBoundary(self): - milter = TestMilter() - milter.connect('testBadBoundary') - # test rfc822 attachment with invalid boundaries - #pdb.set_trace() - rc = milter.feedMsg('bound') - if sys.hexversion < 0x02040000: - # python2.4 adds invalid boundaries to decects list and makes - # payload a str - self.assertEqual(rc,Milter.REJECT) - self.assertEqual(milter.reply[0],'554') - #self.failUnless(milter.bodyreplaced,"Message body not replaced") - self.failIf(milter.bodyreplaced,"Message body replaced") - fp = milter._body - open("test/bound.tstout","w").write(fp.getvalue()) - - def testCompoundFilename(self): - milter = TestMilter() - milter.connect('testCompoundFilename') - # test rfc822 attachment with invalid boundaries - #pdb.set_trace() - rc = milter.feedMsg('test1') - self.assertEqual(rc,Milter.ACCEPT) - #self.failUnless(milter.bodyreplaced,"Message body not replaced") - self.failIf(milter.bodyreplaced,"Message body replaced") - fp = milter._body - open("test/test1.tstout","w").write(fp.getvalue()) - - def testFindsrs(self): - if not bms.srs: - import SRS - bms.srs = SRS.new(secret='test') - sender = bms.srs.forward('foo@bar.com','mail.example.com') - sndr = bms.findsrs(StringIO.StringIO( -"""Received: from [1.16.33.86] (helo=mail.example.com) - by bastion4.mail.zen.co.uk with smtp (Exim 4.50) id 1H3IBC-00013b-O9 - for foo@bar.com; Sat, 06 Jan 2007 20:30:17 +0000 -X-Mailer: "PyMilter-0.8.5" - <%s> foo -MIME-Version: 1.0 -Content-Type: text/plain -To: foo@bar.com -From: postmaster@mail.example.com -""" % sender - )) - self.assertEqual(sndr,'foo@bar.com') - -# def testReject(self): -# "Test content based spam rejection." -# milter = TestMilter() -# milter.connect('gogo-china.com') -# rc = milter.feedMsg('big5'); -# self.failUnless(rc == Milter.REJECT) -# milter.close(); - -def suite(): - s = unittest.makeSuite(BMSMilterTestCase,'test') - s.addTest(doctest.DocTestSuite(bms)) - return s - -if __name__ == '__main__': - if len(sys.argv) > 1: - for fname in sys.argv[1:]: - milter = TestMilter() - milter.connect('main') - fp = open(fname,'r') - rc = milter.feedFile(fp) - fp = milter._body - sys.stdout.write(fp.getvalue()) - else: - #unittest.main() - unittest.TextTestRunner().run(suite())