#! /usr/bin/env python # # Copyright (C) 2016 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. import os import re import errno import cPickle import argparse import paths from Mailman import Utils from Mailman import mm_cfg from Mailman import Errors from Mailman import Pending from Mailman.MailList import MailList width = int(os.environ.get('COLUMNS', 80)) - 2 def parseargs(): parser = argparse.ArgumentParser( description=Utils.wrap("""This script removes an address from the installation. I.e. for every list, if the address is a member, it is removed. If there are any posts or (un)subscription requests from the address waiting moderator action, they are removed.""", column=width), epilog=Utils.wrap("""This script must be put in Mailman's bin/ directory. If a malicious bot or user manages to subscribe and or pend subscriptions to multiple lists in an installation, this script can be used to remove the address from some or all lists and discard any posts or subscriptions for the address awaiting moderator action and optionally discard any subscripions for the address awaiting user confirmation.""", column=width), formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('-l', '--list', dest='lists', action='append', metavar='LIST', help="""\ The name of the list to do. May be repeated for multiple lists. The default is to do all lists""") parser.add_argument('address', type=str, help="""\ The address to be removed. This may be a literal address or a regexp beginning with a '^' to act on all matching addresses.""") parser.add_argument('-d', '--dry-run', dest='dryrun', action='store_true', help="""\ Don't actually make changes, just print what would be done.""") parser.add_argument('-p', '--pending', dest='pending', action='store_true', help="""\ Delete pending subscripton confirmations too if any.""") parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help="""\ Print what's being done.""") return parser.parse_args() def match(member, addr): if isinstance(addr, str): return member.lower() == addr.lower() else: return addr.search(member) def main(): ns = parseargs() if ns.address.startswith('^'): try: addr = re.compile(ns.address, re.IGNORECASE) except re.error, e: print '%s is not a valid regular expression: %s' % ( ns.address, e) exit(1) else: try: addr = ns.address.lower() Utils.ValidateEmail(addr) except Errors.EmailAddressError: print '%s is not a valid email address' % ns.address exit(1) lists = ns.lists or Utils.list_names() for l in lists: try: mlist = MailList(l, lock=True) changed = False for member in mlist.getMembers(): if match(member, addr): if not ns.dryrun: mlist.ApprovedDeleteMember(member, whence = 'erase', admin_notif=False, userack=False ) changed = True if ns.dryrun or ns.verbose: print '%s removed from %s' % (member, mlist.real_name) for id in (mlist.GetHeldMessageIds() + mlist.GetSubscriptionIds() + mlist.GetUnsubscriptionIds() ): if match(mlist.GetRecord(id)[1], addr): address = mlist.GetRecord(id)[1] if not ns.dryrun: changed = True mlist.HandleRequest(id, mm_cfg.DISCARD, comment='erase' ) if ns.dryrun or ns.verbose: print 'Deleted %s request id %d for %s' % ( mlist.real_name, id, address, ) if ns.pending: # There are no public methods to read the pending db (sigh) try: fp = open(os.path.join(mlist.fullpath(), 'pending.pck')) except IOError, e: if e.errno <> errno.ENOENT: raise db = {'evictions': {}} else: try: db = cPickle.load(fp) except: db = {'evictions': {}} finally: fp.close() for cookie, data in db.items(): if cookie in ('evictions', 'version'): continue if (data[0] == Pending.SUBSCRIPTION and match(data[1].address, addr)): if not ns.dryrun: changed = True # This returns the data, but all we want is # the expunge. mlist.pend_confirm(cookie, expunge=True) if ns.dryrun or ns.verbose: print 'Deleted %s pending sub for %s' % ( mlist.real_name, data[1].address, ) if changed: mlist.Save() finally: mlist.Unlock() if __name__ == '__main__': main()