Ages ago (well, back in March and April), I discussed and wrote a patch
for TMDA that would allow us to install hooks that can be invoked upon
releasing, confirming, deleting, whitelisting, or blacklisting messages.

I posted an early release of this here for comments and discussion, and
since then, I've been regularly using a somewhat debugged version of the
same thing.  I use it to feed rejected and accepted messages to a
bayesian spam/non-spam learning procedure.

It's been working fine, and I recently realized that I never followed
through on posting a final version.  So, here I am with it, requesting
that it be evaluated and (hopefully) applied to the TMDA code base.

With this patch we can install any of the following hooks:

  PENDING_RELEASE_ACTION_HOOK
  PENDING_CONFIRM_ACTION_HOOK
  PENDING_DELETE_ACTION_HOOK
  PENDING_WHITELIST_ACTION_HOOK
  PENDING_BLACKLIST_ACTION_HOOK

In each case, these contain the full path name of an executable that
will be invoked for each message that is released, confirmed, deleted,
whitelisted, and blacklisted, respectively.  For all of these which are
not set to None, the specified hook executable will be invoked as
follows:

  /path/to/hook message-file

... where "message-file" is the pathname of the message file in the TMDA
pending directory.  Also, before this hook is invoked, all the variables
defined in Defaults.py, /etc/tmdarc, and your local .tmda file are put
into the environment, so they can be accessed easily within the hook
program.

Anyway, here's the patch.  It works against tmda-1.1.11 (am I out of
date?).

(by the way, this patch also contains try/except blocks around the
invocations of "Util.headers_as_raw_string" and
"Util.body_as_raw_string" in tmda-rfilter, but I don't recall why I did
that ... maybe because some messages had content which blew up
tmda-rfilter ... ???)

--- ./bin/tmda-rfilter.orig	2007-04-01 17:59:00.000000000 -0400
+++ ./bin/tmda-rfilter	2007-09-18 18:39:09.000000000 -0400
@@ -217,5 +217,8 @@
 
 # Original message headers as a raw string.
-orig_msgin_headers_as_raw_string = Util.headers_as_raw_string(msgin)
+try:
+    orig_msgin_headers_as_raw_string = Util.headers_as_raw_string(msgin)
+except:
+    sys.exit(0)
 
 # Original message body.
@@ -223,5 +226,8 @@
 
 # Original message body as a raw string.
-orig_msgin_body_as_raw_string = Util.body_as_raw_string(msgin)
+try:
+    orig_msgin_body_as_raw_string = Util.body_as_raw_string(msgin)
+except:
+    sys.exit(0)
 
 # Calculate the incoming message size.
@@ -349,5 +355,4 @@
 	msgin['X-TMDA-Action'] = action_msg
 
-
 def autorespond_to_sender(sender):
     """Return true if TMDA should auto-respond to this sender."""
@@ -553,4 +558,9 @@
             if Defaults.CONFIRM_ACCEPT_NOTIFY:
                 bouncegen('accept', template='confirm_accept.txt')
+            # If there is a pending confirm hook, attempt to pipe
+            # the message through it.
+            Util.try_action_hook(Util.msg_as_string(msg),
+                                 confirmed_mailid,
+                                 Defaults.PENDING_CONFIRM_ACTION_HOOK)
             # Release the message for delivery if we get this far.
             release_pending(confirm_timestamp, confirm_pid, msg)
--- ./TMDA/Util.py.orig	2007-09-13 17:17:11.000000000 -0400
+++ ./TMDA/Util.py	2007-09-18 18:28:10.000000000 -0400
@@ -1010,4 +1010,51 @@
 
 
+DefaultVarPat = re.compile('^[A-Z][A-Z0-9_]*$')
+
+def try_action_hook(msgstring, msgid, hook):
+    """Attempt to run an action hook"""
+    if hook is None:
+        return
+    # Put all appropriate Defaults variables into the environment.
+    # By "appropriate", I mean all variables that are defined in
+    # Defaults.py, not all the other stuff that also exists in
+    # vars(Defaults).  I filter everything through DefaultVarPat,
+    # above, which gets rid of the stuff in vars(Defaults) shouldn't
+    # go into the environment.
+    import Defaults
+    for (k, v) in vars(Defaults).items():
+        if DefaultVarPat.search(k):
+            if v is None:
+                try:
+                    del os.environ[k]
+                except:
+                    # If we're here, the key is not in the current
+                    # environment.  This try/except block is more
+                    # efficient (methinks) than doing an explicit test
+                    # of existence in the environment before trying to
+                    # do a delete.
+                    pass
+            else:
+                os.environ[k] = str(v)
+    cmd = '%s %s' % (hook, os.path.join(Defaults.PENDING_DIR, msgid + '.msg'))
+    try:
+        # Pipe the message into the hook's stdin and let
+        # the hook's stdout and stderr go out unimpeded.
+        # I do this instead of Util.pipecmd, because the
+        # latter deadlocks.  This method has never deadlocked
+        # on me for tens of thousands of messages over a six
+        # month period.
+        fd = os.popen(cmd, 'w')
+	fd.write(msgstring)
+        fd.flush()
+        r = fd.close()
+        if r is None:
+            return 0
+        else:
+            return r
+    except Exception, txt:
+        raise IOError, \
+              'failure invoking hook "%s" (%s)' % (cmd, txt)
+
 class DevnullOutput:
     def write(self, msg): pass
--- ./TMDA/Pending.py.orig	2007-04-01 13:06:15.000000000 -0400
+++ ./TMDA/Pending.py	2007-09-13 19:19:57.000000000 -0400
@@ -28,4 +28,5 @@
 import email
 import os
+import re
 import sys
 import time
@@ -40,5 +41,4 @@
 Q = Q.init()
 
-
 class Queue:
     """A simple pending queue."""
@@ -369,4 +369,7 @@
         """Release a message from the pending queue."""
         import Cookie
+        # If there is a pending release hook, attempt to pipe
+        # the message through it.
+        Util.try_action_hook(self.show(), self.msgid, Defaults.PENDING_RELEASE_ACTION_HOOK)
         if Defaults.PENDING_RELEASE_APPEND:
             Util.append_to_file(self.append_address,
@@ -401,4 +404,7 @@
     def delete(self):
         """Delete a message from the pending queue."""
+        # If there is a pending delete hook, attempt to pipe
+        # the message through it.
+        Util.try_action_hook(self.show(), self.msgid, Defaults.PENDING_DELETE_ACTION_HOOK)
         if Defaults.PENDING_DELETE_APPEND:
             Util.append_to_file(self.append_address,
@@ -408,4 +414,7 @@
     def whitelist(self):
         """Whitelist the message sender."""
+        # If there is a pending whitelist hook, attempt to pipe
+        # the message through it.
+        Util.try_action_hook(self.show(), self.msgid, Defaults.PENDING_WHITELIST_ACTION_HOOK)
         if Defaults.PENDING_WHITELIST_APPEND:
             Util.append_to_file(self.append_address,
@@ -419,4 +428,7 @@
     def blacklist(self):
         """Blacklist the message sender."""
+        # If there is a pending blacklist hook, attempt to pipe
+        # the message through it.
+        Util.try_action_hook(self.show(), self.msgid, Defaults.PENDING_BLACKLIST_ACTION_HOOK)
         if Defaults.PENDING_BLACKLIST_APPEND:
             Util.append_to_file(self.append_address,
--- ./TMDA/Defaults.py.orig	2007-04-01 12:51:53.000000000 -0400
+++ ./TMDA/Defaults.py	2007-04-01 18:03:12.000000000 -0400
@@ -1213,4 +1213,99 @@
     PENDING_WHITELIST_RELEASE = 1
 
+# PENDING_RELEASE_ACTION_HOOK
+# Path of an optional executable that will be run when a message
+# is being released from the pending queue, right before this
+# release takes place.
+#
+# It will be run as follows:
+#
+# /path/to/executable /path/to/pending-message-file
+#
+# The message text will be available in this executable's stdin.
+#
+# When this executable is invoked, all TMDA configuration
+# variables will be available in the environment in the
+# form of strings.
+#
+# Default is None
+if not vars().has_key('PENDING_RELEASE_ACTION_HOOK'):
+    PENDING_RELEASE_ACTION_HOOK = None
+
+# PENDING_CONFIRM_ACTION_HOOK
+# Path of an optional executable that will be run when a message
+# is being confirmed from the pending queue, right before this
+# confirmation takes place.
+#
+# It will be run as follows:
+#
+# /path/to/executable /path/to/pending-message-file
+#
+# The message text will be available in this executable's stdin.
+#
+# When this executable is invoked, all TMDA configuration
+# variables will be available in the environment in the
+# form of strings.
+#
+# Default is None
+if not vars().has_key('PENDING_CONFIRM_ACTION_HOOK'):
+    PENDING_CONFIRM_ACTION_HOOK = None
+
+# PENDING_DELETE_ACTION_HOOK
+# Path of an optional executable that will be run when a message
+# is being deleted from the pending queue, right before this
+# deletion takes place.
+#
+# It will be run as follows:
+#
+# /path/to/executable /path/to/pending-message-file
+#
+# The message text will be available in this executable's stdin.
+#
+# When this executable is invoked, all TMDA configuration
+# variables will be available in the environment in the
+# form of strings.
+#
+# Default is None
+if not vars().has_key('PENDING_DELETE_ACTION_HOOK'):
+    PENDING_DELETE_ACTION_HOOK = None
+
+# PENDING_WHITELIST_ACTION_HOOK
+# Path of an optional executable that will be run when a message
+# is being whitelisted from the pending queue, right before this
+# whitelisting takes place.
+#
+# It will be run as follows:
+#
+# /path/to/executable /path/to/pending-message-file
+#
+# The message text will be available in this executable's stdin.
+#
+# When this executable is invoked, all TMDA configuration
+# variables will be available in the environment in the
+# form of strings.
+#
+# Default is None
+if not vars().has_key('PENDING_WHITELIST_ACTION_HOOK'):
+    PENDING_WHITELIST_ACTION_HOOK = None
+
+# PENDING_BLACKLIST_ACTION_HOOK
+# Path of an optional executable that will be run when a message
+# is being blacklisted from the pending queue, right before this
+# blacklisting takes place.
+#
+# It will be run as follows:
+#
+# /path/to/executable /path/to/pending-message-file
+#
+# The message text will be available in this executable's stdin.
+#
+# When this executable is invoked, all TMDA configuration
+# variables will be available in the environment in the
+# form of strings.
+#
+# Default is None
+if not vars().has_key('PENDING_BLACKLIST_ACTION_HOOK'):
+    PENDING_BLACKLIST_ACTION_HOOK = None
+
 # ADDED_HEADERS_CLIENT
 # A Python dictionary containing one or more header:value string pairs
@@ -1599,4 +1694,9 @@
     'PENDING_RELEASE_APPEND': None,
     'PENDING_WHITELIST_APPEND': None,
+    'PENDING_RELEASE_ACTION_HOOK' : None,
+    'PENDING_CONFIRM_ACTION_HOOK' : None,
+    'PENDING_DELETE_ACTION_HOOK' : None,
+    'PENDING_WHITELIST_ACTION_HOOK' : None,
+    'PENDING_BLACKLIST_ACTION_HOOK' : None,
     'RESPONSE_DIR': None,
     'SENDMAIL_PROGRAM': None,

-- 
 Lloyd Zusman
 [EMAIL PROTECTED]
 God bless you.
_____________________________________________
tmda-users mailing list (tmda-users@tmda.net)
http://tmda.net/lists/listinfo/tmda-users

Reply via email to