From 33aeefa19f1a1e8a92480dfd9014452abd215a7c Mon Sep 17 00:00:00 2001
From: Stuart Gathman <stuart@gathman.org>
Date: Sun, 1 Oct 2006 01:44:06 +0000
Subject: [PATCH] case_sensitive_localpart option, more delayed bounce
 heuristics, optional smart_alias section.

---
 NEWS        |  9 +++++++--
 TODO        | 14 +++++++++----
 bms.py      | 57 +++++++++++++++++++++++++++++++++++++++--------------
 milter.cfg  | 11 ++++++++---
 milter.spec |  1 +
 5 files changed, 68 insertions(+), 24 deletions(-)

diff --git a/NEWS b/NEWS
index a994cbf..f55d63d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,11 @@
 Here is a history of user visible changes to Python milter.
-
-0.8.6	Delay reject of unsigned RCPT for postmaster and abuse only
+0.8.6	Support CBV timeout
+	Support fail template, headers in templates
+	Create GOSSiP record only when connection will procede to DATA.
+	More SPF lax heuristics
+	Don't require SPF pass for white/black listing mail from trusted relay.
+	Support localpart wildcard for white and black lists.
+	Delay reject of unsigned RCPT for postmaster and abuse only
 	Fix dsn reporting of hard permerror
 	Resolve FIXME for wrap_close in miltermodule.c
 	Add Message-ID to DSNs
diff --git a/TODO b/TODO
index e29d33e..51dd3de 100644
--- a/TODO
+++ b/TODO
@@ -1,10 +1,13 @@
+Reporting explanation for failure should show source of sender
+provided explanation.
+
 Reports PROBATION even when rejecting message (works, but confusing in log).
 
 Bug in Auto-whitelist.  Recent Auto-whitelist doesn't override expired entry.
 
-Delayed_failure detection needs to handle multi-line header fields.  Also,
-delayed_failure should be recognized when addressed to postmaster@helodomain
-Idea: load headers into message object, and use header array.
+DONE Delayed_failure detection needs to handle multi-line header fields.  
+Also, delayed_failure should be recognized when addressed to
+postmaster@helodomain 
 
 Need to use wildcards in blacklist.log: *.madcowsrecord.net
 Need to exclude emails like !*-admin@example.com in whitelist_sender.
@@ -42,7 +45,10 @@ data structure as autowhitelist.log.  Add emails blacklisted via CBV
 so that they are remembered across milter restarts.
 
 Make all dictionaries work like honeypot.  Do not train as ham unless
-whitelisted.  Train on blacklisted messages, or spam feedback.
+whitelisted.  Train on blacklisted messages, or spam feedback.  This
+can be called Train On Error.  Should be possible to startup
+with training on everything to get dictionary built fast, then switch
+to train on error to minimize labor.
 
 Allow unsigned DSNs from selected domains (that don't accept signed MFROM,
 e.g. verizon.net).
diff --git a/bms.py b/bms.py
index 64ce7a1..d349ba8 100644
--- a/bms.py
+++ b/bms.py
@@ -1,6 +1,9 @@
 #!/usr/bin/env python
 # A simple milter that has grown quite a bit.
 # $Log$
+# Revision 1.66  2006/07/26 16:42:26  customdesigned
+# Support CBV timeout
+#
 # Revision 1.65  2006/06/21 22:22:00  customdesigned
 # Handle multi-line headers in delayed dsns.
 #
@@ -215,7 +218,7 @@ import mime
 import email.Errors
 import Milter
 import tempfile
-import ConfigParser
+from ConfigParser import ConfigParser
 import time
 import socket
 import struct
@@ -254,6 +257,7 @@ ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$')
 # If found, we blacklist that recipient.
 subjpats = (
  r'^failure notice',
+ r'^subjectbounce',
  r'^returned mail',
  r'^undeliver',
  r'^delivery\b.*\bfailure',
@@ -281,6 +285,7 @@ block_forward = {}
 hide_path = ()
 log_headers = False
 block_chinese = False
+case_sensitive_localpart = False
 spam_words = ()
 porn_words = ()
 banned_exts = mime.extlist.split(',')
@@ -328,8 +333,15 @@ milter_log = logging.getLogger('milter')
 if gossip:
   gossip_node = Gossip('gossip4.db',120)
 
-class MilterConfigParser(ConfigParser.ConfigParser):
+class MilterConfigParser(ConfigParser):
+
+  def __init__(self,defaults):
+    ConfigParser.__init__(self)
+    self.defaults = defaults
 
+  def get(self,sect,opt):
+    return ConfigParser.get(self,sect,opt,vars=self.defaults)
+    
   def getlist(self,sect,opt):
     if self.has_option(sect,opt):
       return [q.strip() for q in self.get(sect,opt).split(',')]
@@ -343,11 +355,11 @@ class MilterConfigParser(ConfigParser.ConfigParser):
     for q in s.split(','):
       q = q.strip()
       if q.startswith('file:'):
-        domain = q[5:]
+        domain = q[5:].lower()
 	d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
       else:
 	user,domain = q.split('@')
-	d.setdefault(domain,[]).append(user)
+	d.setdefault(domain.lower(),[]).append(user)
     return d
   
   def getaddrdict(self,sect,opt):
@@ -390,7 +402,8 @@ def read_config(list):
     'reject_noptr': 'no',
     'supply_sender': 'no',
     'best_guess': 'no',
-    'dspam_internal': 'yes'
+    'dspam_internal': 'yes',
+    'case_sensitive_localpart': 'no'
   })
   cp.read(list)
 
@@ -398,6 +411,7 @@ def read_config(list):
   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
   socketname = cp.get('milter','socket')
   timeout = cp.getint('milter','timeout')
   check_user = cp.getaddrset('milter','check_user')
@@ -406,6 +420,7 @@ def read_config(list):
   internal_domains = cp.getlist('milter','internal_domains')
   trusted_relay = cp.getlist('milter','trusted_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
@@ -439,13 +454,19 @@ def read_config(list):
   if wiretap_dest: wiretap_dest = '<%s>' % wiretap_dest
 
   global smart_alias
-  for sa in cp.getlist('wiretap','smart_alias'):
-    sm = cp.getlist('wiretap',sa)
+  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)
-    key = (sm[0],sm[1])
+    if case_sensitive_localpart:
+      key = (sm[0],sm[1])
+    else:
+      key = (sm[0].lower(),sm[1].lower())
     smart_alias[key] = sm[2:]
 
   # dspam section
@@ -829,12 +850,18 @@ class bmsMilter(Milter.Milter):
 
   def smart_alias(self,to):
     if smart_alias:
-      t = parse_addr(to.lower())
+      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]
-      cf = self.canon_from
+      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]
@@ -1582,10 +1609,10 @@ class bmsMilter(Milter.Milter):
 	    if ln[0].isspace() and ln[0] != '\n':
 	      lastln += ln
 	      continue
-	    name,val = lastln.rstrip().split(None,1)
-	    pos = val.find('<SRS')
-	    if pos >= 0:
-	      try:
+	    try:
+	      name,val = lastln.rstrip().split(None,1)
+	      pos = val.find('<SRS')
+	      if pos >= 0:
 		sender = srs.reverse(val[pos+1:-1])
 		cbv_cache[sender] = 500,self.delayed_failure,time.time()
 		try:
@@ -1597,7 +1624,7 @@ class bmsMilter(Milter.Milter):
 		self.tempname = None
 		self.log('BLACKLIST:',sender,fname)
 		return Milter.DISCARD
-	      except: continue
+	    except: continue
 	  lnl = ln.lower()
 	  for k in ('message-id','x-mailer','sender'):
 	    if lnl.startswith(k):
diff --git a/milter.cfg b/milter.cfg
index 2b6df04..9e75f7f 100644
--- a/milter.cfg
+++ b/milter.cfg
@@ -31,6 +31,9 @@ log_headers = 0
 # Reject mail for domains mentioned unless user is mentioned here also
 ;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com
 
+# Treat localparts in milter.cfg as case-insensitive
+case_sensitive_localpart = true
+
 # features intended to filter or block incoming mail
 [defang]
 
@@ -125,11 +128,13 @@ blind = 1
 # discard outgoing mail without alerting sender
 # can be used in conjunction with wiretap to censor outgoing mail
 ;discard_users = canned@bigcorp.com
+
 #
 # smart aliases trigger on both sender and recipient
+#   alias = sender, recipient[, destination]
 #
-;smart_alias = copycust,walter,spy1,spy2
-# multiple wiretap monitors
+[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 
@@ -142,7 +147,7 @@ blind = 1
 ;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com,
 ;	walter@bigcorp.com
 ;bulk = soruce@telex.com,bob@jsconnor.com
-;bulk = soruce@telex.com,larry@jsconnor.com
+;bulk1 = soruce@telex.com,larry@jsconnor.com,bulk
 
 # See http://bmsi.com/python/dspam.html
 [dspam]
diff --git a/milter.spec b/milter.spec
index 37a46fc..96af07d 100644
--- a/milter.spec
+++ b/milter.spec
@@ -177,6 +177,7 @@ rm -rf $RPM_BUILD_ROOT
 
 %changelog
 * Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
+- Support CBV timeout
 - Support fail template, headers in templates
 - Create GOSSiP record only when connection will procede to DATA.
 - More SPF lax heuristics
-- 
GitLab