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())