[Mailman-Users] Writing a custom handler

Mark Sapiro mark at msapiro.net
Tue Jul 2 21:17:12 CEST 2013


On 07/02/2013 11:38 AM, Chris Nulk wrote:
> 
> 
> I did forget about some of my other questions.  I plan on writing
> another custom handler for a list-specific issue.  Where would I look if
> I wanted to intercept messages related to subscribing, unsubscribing,
> and options processing?


These are messages to the -request, -join, -subscribe, -leave and
-unsubscribe addresses and are all processed by CommandRunner. Only
IncomingRunner processes a pipeline of handlers. There is no comparable
processing or custom handler option for CommandRunner. You would have to
modify Mailman/Queue/CommandRunner.py itself.


> How would I tell the difference between a
> subscribe message and an unsubscribe message? Also, is there a
> difference if the person does the subscribing or unsubscribing via the
> web?  Can I trap those?


The web functions are handled by modules in Mailman/Cgi. They don't
involve qrunners at all.



> Below is the latest update incorporating your suggestions.


See some inline comments.


> #!/usr/bin/env python
> #
> 
> """This is a custom handler that will check all the sender addresses of
> a message against a global ban list.  If any of the sender addresses are
> on the global ban list, the message will get logged and discarded.
> """
> 
> import sys
> import os
> 
> from Mailman import mm_cfg
> from Mailman import Utils
> from Mailman import Message
> from Mailman import Errors
> from Mailman.i18n import _
> from Mailman.Logging.Syslog import syslog
> 
> # Global variables
> #   Initialize the banlist
> banlist = []
> 
> #   Keep the Global Ban lists modification time
> #     set to -1 to indicate ban file hasn't been read
> ban_mtime = -1
> 
> # Define ban_file
> #   mm_cfg.GLOBALBANLIST_FILENAME is defined in mm_cfg and should
> #   be the full path to the file.
> ban_file = mm_cfg.GLOBALBANLIST_FILENAME
> 
> def process(mlist, msg, msgdata):
>     # Upstream pipeline handler marked message approved -

Or message contained an Approved: <password> header.


>     #   respect the decision
>     if msgdata.get('approved'):
>         return
> 
>     # ban_file gets its value from mm_cfg.GLOBALBANLIST_FILENAME. If
>     #   mm_cfg.GLOBALBANLIST_FILENAME is not defined neither is
>     #   ban_file, so simply return.
>     if not ban_file:
>         return
> 
>     # Read in the global ban list of email addresses
>     if Ban_File_Changed(ban_file, ban_mlist):


ban_mtime, not ban_mlist.


>         # Global Ban list has changed (or ban_mlist = -1),


ban_mtime.


>         #   read in the changes
>         rc = Read_GlobalBan_File(ban_file)
>         if not rc:
>             # Problems reading the GlobalBan list
>             return


Or just
        if not Read_GlobalBan_File(ban_file):
            return


>     # Check if banlist has anything, if not, no need to go further
>     if not banlist:
>         return
> 
>     # Go through possible senders.  Check if any of them are
>     #   on the global ban list
>     for sender in msg.get_senders():
>         if sender.lower() in banlist:
>             break
>     else:
>         # None of the sender addresses were in the global ban
>         #   list so return and continue with the next pipeline
>         #   handler
>         return
> 
>     # A sender was found on the global ban list.  Log it and
>     #   discard the message notifying the list owner
>     if sender:
>         # Log banned sender to the vette log
>         syslog('vette', '%s is banned by the global ban list', sender)
>         # Perform message discard
>         do_discard_globalban(mlist, msg, sender)
>     else:
>         assert 0, 'Bad sender in GlobalBan.py'
> 
> 
> # Stat the ban file to get the modification time and compare it to the
> #   last time the file was changed.  If a changed occured, update
> #   ban_mtime to current change time
> def Ban_File_Changed(ban_file, ban_mtime):
>     try:
>         statinfo = os.stat(ban_file)
>     except IOError, e:
>         # cannot stat the global ban list for whatever reason
>         # log it and continue with the next pipeline handler
>         syslog('error',
>                "Can't stat %s: %s" % (ban_file, e)
>                )
>         return False
>     except:
>         # unspecified error
>         # log it and continue with the next pipeline handler
>         syslog('error',
>                'ERROR: %s: %s' % (sys.exc_info()[0], sys.exc_info()[1])
>               )
>         return False


Do you really want to do this? The Mailman philosophy is all caught and
handled exceptions should be explicit. An unanticipated exception will
be caught at the top qrunner level which will log the error with
traceback in the 'error' log and shunt the message rather than
continuing processing with a message that triggered an unanticipated
exception.


>     # if ban_mtime = -1, statinfo.st_mtime should always be greater, this
>     #   is a special case for when the code is first loaded and run
>     if statinfo.st_mtime > ban_mtime:
>         ban_mtime = statinfo.st_mtime
>         return True
> 
>     # no change in ban file
>     return False
> 
> 
> # Read the Global Ban file and populate the banlist.
> def Read_GlobalBan_File(ban_file):
>     try:
>         with open(ban_file) as f:
>             for addr in f:
>                 # if addr is not in banlist, add it - to avoid duplicates
>                 if addr not in banlist:
>                     banlist.append(addr.lower().strip())
>     except IOError, e:
>         # cannot open the global ban list for whatever reason
>         # log it and continue with the next pipeline handler
>         syslog('error',
>                "Can't open %s: %s" % (ban_file, e)
>                )
>         return False
>     except:
>         # unspecified error
>         # log it and continue with the next pipeline handler
>         syslog('error',
>                'ERROR: %s: %s' % (sys.exc_info()[0], sys.exc_info()[1])
>               )
>         return False


See comment above about generic exceptions.


> 
>     # success
>     return True
> 
> 
> # copied almost verbatim from Mailman/Handlers/Moderate.py
> def do_discard_globalban(mlist, msg, sender):
>     # forward auto-discards to list owners?
>     if mlist.forward_auto_discards:
>         lang = mlist.preferred_language
>         nmsg = Message.UserNotification(mlist.GetOwnerEmail(),
>                                         mlist.GetBouncesEmail(),
>                                         _('Global Ban List Auto-discard
> notification'),
>                                         lang=lang)
>         nmsg.set_type('multipart/mixed')
>         text = MIMEText(Utils.wrap(_("""\
> The sender - %(sender)s - of the attached message is on the Global Ban
> list.  Therefore, the message
> has been automatically discarded.""")),
>                         _charset=Utils.GetCharSet(lang))
>         nmsg.attach(text)
>         nmsg.attach(MIMEMessage(msg))
>         nmsg.send(mlist)
>     # Discard the message
>     raise Errors.DiscardMessage

-- 
Mark Sapiro <mark at msapiro.net>        The highway is for gamblers,
San Francisco Bay Area, California    better use your sense - B. Dylan


More information about the Mailman-Users mailing list