[Mailman-Users] fallen behind in moderating? handy script

Greg Stein gstein at lyra.org
Sun May 15 03:13:44 CEST 2005


Oops. The message to mailman-developers was rejected. Maybe mailman-users
is more appropriate anyways...

----- Forwarded message from Greg Stein <gstein at lyra.org> -----

From: Greg Stein <gstein at lyra.org>
Subject: fallen behind in moderating? handy script
To: mailman-developers at python.org
Date: Sat, 14 May 2005 18:07:21 -0700

A number of lists on my mailman machine have fallen *way* behind in their
moderation queues. Once the queue gets to be too large (many hundreds),
the web form doesn't really work any more. But you still have to get the
spam crap outta there. And in an efficient manner.

I'm not sure it is in the latest distribution (seems like I recall it was
removed), but there is a bin/discard script that I find useful.
Previously, I would search all the heldmsg files for certain patterns and
pipe the results into xargs discard. Kind of like this:

$ cd install/data
$ ls | fgrep heldmsg | xargs fgrep -l -i mortgage | xargs ../bin/discard

That helped, but wasn't very efficient and it still leaves a ton of other
kinds of spam behind.

So... I wrote a little Python/curses app to quickly zoom through held
messages for a list. I *just* wrote this sucker this afternoon, and it has
very few comments, robustness, etc. But maybe somebody else can find it
useful, so I'm posting it here (attached).

I run it like this:

$ cd install/data
$ ../bin/manage LISTNAME
$ cat discard.LISTNAME | xargs ../bin/discard

The app presents a list of all the held messages down the left pane, and
the From/Subject/Body fields in the right pane. Hitting space bar toggles
the delete status and moves you to the next one. 'd' marks it for delete.
'u' undeletes it. 'p/UP moves up a file. 'n'/DOWN moves to the next file.
'q' will exit the app and write out the discard.LISTNAME file. That's
about it.

Enjoy!

Cheers,
-g

-- 
Greg Stein, http://www.lyra.org/

----- End forwarded message -----

-- 
Greg Stein, http://www.lyra.org/
-------------- next part --------------
#!/usr/local/bin/python

import sys
import cPickle
import os
import re
import curses

import paths

cre = re.compile(r'heldmsg-(?P<listname>.*)-(?P<id>[0-9]+)\.(pck|txt)$')

def usage(code=1):
  print 'USAGE: %s LISTNAME' % sys.argv[0]
  sys.exit(code)

def main():
  if len(sys.argv) != 2:
    usage()

  listname = sys.argv[1]
  messages = [ ]
  for fname in os.listdir(os.path.join(paths.prefix, 'data')):
    m = cre.match(fname)
    if m and m.group(1) == listname:
      # message number, filename, flagged for discard
      messages.append([int(m.group(2)), fname, False])

  messages.sort()

  curses.wrapper(manage, messages)
  
  f = open('discard.' + listname, 'w')
  for idx, fname, flag in messages:
    if flag:
      f.write(fname + '\n')

def manage(main_win, messages):
  m_height, m_width = main_win.getmaxyx()

#  curses.use_default_colors()

  curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
  bg = curses.color_pair(1)
  main_win.bkgd(ord(' '), bg)
  for i in range(m_height):
    main_win.addstr(i, 0, ' ' * (m_width - 1))
  main_win.redrawwin()

  main_win.vline(0, 8, curses.ACS_VLINE, m_height)

  header_win = main_win.derwin(1, 8, 0, 0)
  header_win.syncok(True)
  header_win.addstr(0, 0, 'Files', curses.A_BOLD)

  files_win = main_win.derwin(m_height - 1, 8, 1, 0)
  files_win.syncok(True)
  
  from_win = main_win.derwin(1, m_width - 9, 0, 9)
  from_win.syncok(True)
  from_win.addstr(0, 0, 'From:', curses.A_BOLD)

  subject_win = main_win.derwin(1, m_width - 9, 1, 9)
  subject_win.syncok(True)
  subject_win.addstr(0, 0, 'Subject:', curses.A_BOLD)

  body_win = main_win.derwin(m_height - 2, m_width - 9, 2, 9)
  body_win.syncok(True)

  list_offset = 0
  position = 0

  while 1:
    files_win.erase()
    for i in range(min(m_height - 1, len(messages) - list_offset)):
      files_win.addstr(i, 2, str(messages[i + list_offset][0]))
      if messages[i + list_offset][2]:
	files_win.addch(i, 0, ord('*'))
    
    msg = cPickle.load(open(messages[position][1]))

    from_win.move(0, 9)
    from_win.clrtoeol()
    from_win.addstr(get_header(msg, 'from', m_width - 10 - 9))
    
    subject_win.move(0, 9)
    subject_win.clrtoeol()
    subject_win.addstr(get_header(msg, 'subject', m_width - 10 - 9))
    
    body_win.erase()
    while msg.is_multipart():
      msg = msg.get_payload(0)

    body = msg.get_payload(decode=True)
    lines = body.splitlines()
    for i in range(min(m_height - 2, len(lines))):
      body_win.addstr(i, 0, lines[i][:m_width - 1 - 9].translate(table))

    main_win.refresh()
    ch = main_win.getch(1 + position - list_offset, 0)
    
    if ch == ord('q'):
      return
    if ch == 12:  # ctrl-l
      main_win.redrawwin()
      main_win.touchwin()
    elif ch == ord(' '):
      messages[position][2] = not messages[position][2]
      position = position + 1
    elif ch == ord('d'):
      messages[position][2] = True
      position = position + 1
    elif ch == ord('u'):
      messages[position][2] = False
      position = position + 1
    elif ch in (curses.KEY_UP, ord('p')):
      position = position - 1
    elif ch in (curses.KEY_DOWN, ord('n')):
      position = position + 1
      
    # bound the cursor to the size of the list.
    if position < 0:
      position = 0
    elif position >= len(messages):
      position = len(messages) - 1
      
    # scroll the file list if necessary
    if position < list_offset:
      list_offset = list_offset - (m_height - 4)
      if list_offset < 0:
	list_offset = 0
    elif position - list_offset > m_height - 2:
      list_offset = list_offset + (m_height - 4)

def get_header(msg, header, clip):
  s = msg[header]
  if s is None:
      return '<not present>'
  return s[:clip].translate(table)

table = ''.join([(c < 32 or c > 127) and '?' or chr(c) for c in range(256)])

if __name__ == '__main__':
  main()


More information about the Mailman-Users mailing list