Update of /cvsroot/tmda/tmda/bin
In directory usw-pr-cvs1:/tmp/cvs-serv17737/bin

Modified Files:
        ChangeLog tmda-rfilter 
Log Message:
New feature: auto-response rate limiting.  Meant as a last resort to
prevent mail loops between TMDA and broken remote auto-responders, you
can now specify the maximum number of automatic responses sent to a
given sender in a day by setting MAX_AUTORESPONSES_PER_DAY.

The default at this point is 50.  Disable with 0.

A new directory 'responses' will be created under DATADIR to store the
rate limiting information.  This can be changed by setting
RESPONSE_DIR.


Index: ChangeLog
===================================================================
RCS file: /cvsroot/tmda/tmda/bin/ChangeLog,v
retrieving revision 1.224
retrieving revision 1.225
diff -u -r1.224 -r1.225
--- ChangeLog   8 Aug 2002 16:49:46 -0000       1.224
+++ ChangeLog   21 Aug 2002 22:37:42 -0000      1.225
@@ -1,3 +1,9 @@
+2002-08-21  Jason R. Mastaler  <[EMAIL PROTECTED]>
+
+       * tmda-rfilter (autorespond_to_sender): New function.  Determine
+       whether to auto-respond based on detection of mailing list/bounce
+       messages as well if the auto-response rate limit has been reached.
+       
 2002-08-08  Jason R. Mastaler  <[EMAIL PROTECTED]>
 
        * tmda-rfilter (logit): Move guts into TMDA/MessageLogger.

Index: tmda-rfilter
===================================================================
RCS file: /cvsroot/tmda/tmda/bin/tmda-rfilter,v
retrieving revision 1.60
retrieving revision 1.61
diff -u -r1.60 -r1.61
--- tmda-rfilter        11 Aug 2002 00:56:55 -0000      1.60
+++ tmda-rfilter        21 Aug 2002 22:37:42 -0000      1.61
@@ -239,37 +239,8 @@
 # The directory of pending messages.
 pendingdir = os.path.join(Defaults.DATADIR, 'pending')
 
+# Catchall variable to enable/disable auto-responses.
 auto_reply = 1
-# - Don't respond if:
-#   - SENDER is <>
-if auto_reply and envelope_sender == '<>':
-    auto_reply = 0
-#   - SENDER is <#@[]>
-if auto_reply and envelope_sender == '<#@[]>':
-    auto_reply = 0
-#   - SENDER starts with "mailer-daemon"
-if auto_reply and envelope_sender.lower().startswith('mailer-daemon'):
-    auto_reply = 0
-#   - header "List-ID:" (as per RFC 2919)
-#   - header "Mailing-List:"
-#   - header "X-Mailing-List:"
-#   - header "X-ML-Name:"
-#   - header "List-(Help|Unsubscribe|Subscribe|Post|Owner|Archive):"
-#     (as per RFC 2369)
-if auto_reply:
-    list_headers = ['list-id', 'list-help', 'list-subscribe',
-                    'list-unsubscribe', 'list-post', 'list-owner',
-                    'list-archive', 'mailing-list', 'x-mailing-list',
-                    'x-ml-name']
-    for hdr in list_headers:
-        if message_headers.has_key(hdr):
-            auto_reply = 0
-            break
-#   - header "Precedence:" value junk, bulk, or list
-if auto_reply:
-    precedence = message_headers.getheader('precedence', None)
-    if precedence and precedence.lower() in ('bulk', 'junk', 'list'):
-        auto_reply = 0
 
 
 ###########
@@ -291,9 +262,87 @@
         logger.write()
 
 
+def normalize_sender(sender):
+    """Return a normalized version of the given sender address for use
+    in ~/.tmda/responses.
+
+    - Any / characters are replaced with : to prevent creation of files
+      outside the directory.
+    - Spaces are replaced with underscores.
+    - The address is lowercased.
+    """
+    sender = sender.replace(' ', '_')
+    sender = sender.replace('/', ':')
+    return sender.lower()
+
+
+def autorespond_to_sender(sender):
+    """Return true if TMDA should auto-respond to this sender."""
+    # Try and detect a bounce message.
+    if envelope_sender == '<>' or \
+           envelope_sender == '<#@[]>' or \
+           envelope_sender.lower().startswith('mailer-daemon'):
+        logit ('DROP (envelope sender = %s)' % envelope_sender)
+        return 0
+    # Try and detect a mailing list message.
+    #   - header "List-ID:" (as per RFC 2919)
+    #   - header "Mailing-List:"
+    #   - header "X-Mailing-List:"
+    #   - header "X-ML-Name:"
+    #   - header "List-(Help|Unsubscribe|Subscribe|Post|Owner|Archive):"
+    #     (as per RFC 2369)
+    guilty_header = None
+    list_headers = ['List-Id', 'List-Help', 'List-Subscribe',
+                    'List-Unsubscribe', 'List-Post', 'List-Owner',
+                    'List-Archive', 'Mailing-List', 'X-Mailing-List',
+                    'X-Ml-Name']
+    for hdr in list_headers:
+        if message_headers.has_key(hdr):
+            guilty_header = '%s: %s' % (hdr, message_headers.getheader(hdr))
+    #   - header "Precedence:" value junk, bulk, or list
+    precedence = message_headers.getheader('precedence', None)
+    if precedence and \
+           precedence.lower() in ('bulk', 'junk', 'list') and \
+           guilty_header is None:
+        guilty_header = 'Precedence: %s' % precedence
+    if guilty_header:
+        logit ('DROP (%s)' % guilty_header)
+        return 0
+    # Auto-response rate limiting.  Algorithm based on Bruce Guenter's
+    # qmail-autoresponder (http://untroubled.org/qmail-autoresponder/).
+    # See qmail-autoresponder(1) for more details.
+    if Defaults.MAX_AUTORESPONSES_PER_DAY == 0:
+        return 1
+    if os.path.isdir(Defaults.RESPONSE_DIR):
+        os.chdir(Defaults.RESPONSE_DIR)
+        files = os.listdir('.')
+        sndrlist = []
+        for file in files:
+            # Ignore foreign files.
+            try:
+                timestamp, pid, address = file.split('.', 2)
+            except ValueError:
+                continue
+            # If file is more than one day old, delete it and continue.
+            now = int(time.time())
+            if now > (int(timestamp) + Util.seconds('1d')):
+                os.unlink(file)
+                continue
+            else:
+                sndrlist.append(address)
+        # Count remaining occurrences of this sender, and don't
+        # respond if that number it exceeds our threshold.
+        if sndrlist.count(normalize_sender(sender)) >= \
+               Defaults.MAX_AUTORESPONSES_PER_DAY:
+            logit('DROP (%s = %s)' % ('MAX_AUTORESPONSES_PER_DAY',
+                                      Defaults.MAX_AUTORESPONSES_PER_DAY))
+            return 0
+    return 1
+
+
 def send_bounce(bounce_message, **vars):
     """Send a confirmation message back to the sender."""
-    if auto_reply:
+    if autorespond_to_sender(envelope_sender) and auto_reply:
         bounce_message = cStringIO.StringIO(bounce_message)
         headers = rfc822.Message(bounce_message)
         # Add some headers.
@@ -323,6 +372,17 @@
         body = bounce_message.read()
         Util.sendmail(headers, body,
                       envelope_sender, Defaults.BOUNCE_ENV_SENDER)
+        # Optionally, record this auto-response.
+        if Defaults.MAX_AUTORESPONSES_PER_DAY != 0:
+            response_filename = '%s.%s.%s' % (int(time.time()),
+                                              Defaults.PID,
+                                              normalize_sender(envelope_sender))
+            # Create ~/.tmda/responses if necessary.
+            if not os.path.exists(Defaults.RESPONSE_DIR):
+                os.makedirs(Defaults.RESPONSE_DIR, 0700)
+            fp = open(os.path.join(Defaults.RESPONSE_DIR,
+                                   response_filename), 'w')
+            fp.close()
 
 
 def send_cc(address):

_______________________________________
tmda-cvs mailing list
http://tmda.net/lists/listinfo/tmda-cvs

Reply via email to