diff options
| author | Barry Warsaw | 2015-03-02 21:01:24 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2015-03-02 21:01:24 -0500 |
| commit | e33d6d6b88d0340e7e9adbc8638db306b19e2631 (patch) | |
| tree | 66298ee966545f7737e95587034ff436e79acc1b /port_me | |
| parent | 9ba3450c3acf720f981dc499402e5a2616db2cba (diff) | |
| download | mailman-e33d6d6b88d0340e7e9adbc8638db306b19e2631.tar.gz mailman-e33d6d6b88d0340e7e9adbc8638db306b19e2631.tar.zst mailman-e33d6d6b88d0340e7e9adbc8638db306b19e2631.zip | |
Diffstat (limited to 'port_me')
| -rw-r--r-- | port_me/bumpdigests.py | 74 | ||||
| -rw-r--r-- | port_me/checkdbs.py | 209 | ||||
| -rw-r--r-- | port_me/config_list.py | 332 | ||||
| -rw-r--r-- | port_me/disabled.py | 201 | ||||
| -rw-r--r-- | port_me/export.py | 310 | ||||
| -rw-r--r-- | port_me/find_member.py | 135 | ||||
| -rw-r--r-- | port_me/gate_news.py | 244 | ||||
| -rw-r--r-- | port_me/list_owners.py | 90 | ||||
| -rw-r--r-- | port_me/senddigests.py | 83 | ||||
| -rw-r--r-- | port_me/show_config.py | 97 |
10 files changed, 1775 insertions, 0 deletions
diff --git a/port_me/bumpdigests.py b/port_me/bumpdigests.py new file mode 100644 index 000000000..f30772ca8 --- /dev/null +++ b/port_me/bumpdigests.py @@ -0,0 +1,74 @@ +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import sys +import optparse + +from mailman import errors +from mailman import MailList +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.version import MAILMAN_VERSION + +# Work around known problems with some RedHat cron daemons +import signal +signal.signal(signal.SIGCHLD, signal.SIG_DFL) + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] [listname ...] + +Increment the digest volume number and reset the digest number to one. All +the lists named on the command line are bumped. If no list names are given, +all lists are bumped.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + return opts, args, parser + + + +def main(): + opts, args, parser = parseargs() + config.load(opts.config) + + listnames = set(args or config.list_manager.names) + if not listnames: + print(_('Nothing to do.')) + sys.exit(0) + + for listname in listnames: + try: + # Be sure the list is locked + mlist = MailList.MailList(listname) + except errors.MMListError: + parser.print_help() + print(_('No such list: $listname'), file=sys.stderr) + sys.exit(1) + try: + mlist.bump_digest_volume() + finally: + mlist.Save() + mlist.Unlock() + + + +if __name__ == '__main__': + main() diff --git a/port_me/checkdbs.py b/port_me/checkdbs.py new file mode 100644 index 000000000..61aa6b6f1 --- /dev/null +++ b/port_me/checkdbs.py @@ -0,0 +1,209 @@ +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import sys +import time +import optparse + +from email.Charset import Charset + +from mailman import MailList +from mailman import Utils +from mailman.app.requests import handle_request +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.email.message import UserNotification +from mailman.initialize import initialize +from mailman.interfaces.requests import IListRequests, RequestType +from mailman.version import MAILMAN_VERSION + +# Work around known problems with some RedHat cron daemons +import signal +signal.signal(signal.SIGCHLD, signal.SIG_DFL) + +NL = u'\n' +now = time.time() + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] + +Check for pending admin requests and mail the list owners if necessary.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if args: + parser.print_help() + print(_('Unexpected arguments'), file=sys.stderr) + sys.exit(1) + return opts, args, parser + + + +def pending_requests(mlist): + # Must return a byte string + lcset = mlist.preferred_language.charset + pending = [] + first = True + requestsdb = IListRequests(mlist) + for request in requestsdb.of_type(RequestType.subscription): + if first: + pending.append(_('Pending subscriptions:')) + first = False + key, data = requestsdb.get_request(request.id) + when = data['when'] + addr = data['addr'] + fullname = data['fullname'] + passwd = data['passwd'] + digest = data['digest'] + lang = data['lang'] + if fullname: + if isinstance(fullname, unicode): + fullname = fullname.encode(lcset, 'replace') + fullname = ' (%s)' % fullname + pending.append(' %s%s %s' % (addr, fullname, time.ctime(when))) + first = True + for request in requestsdb.of_type(RequestType.held_message): + if first: + pending.append(_('\nPending posts:')) + first = False + key, data = requestsdb.get_request(request.id) + when = data['when'] + sender = data['sender'] + subject = data['subject'] + reason = data['reason'] + text = data['text'] + msgdata = data['msgdata'] + subject = Utils.oneline(subject, lcset) + date = time.ctime(when) + reason = _(reason) + pending.append(_("""\ +From: $sender on $date +Subject: $subject +Cause: $reason""")) + pending.append('') + # Coerce all items in pending to a Unicode so we can join them + upending = [] + charset = mlist.preferred_language.charset + for s in pending: + if isinstance(s, unicode): + upending.append(s) + else: + upending.append(unicode(s, charset, 'replace')) + # Make sure that the text we return from here can be encoded to a byte + # string in the charset of the list's language. This could fail if for + # example, the request was pended while the list's language was French, + # but then it was changed to English before checkdbs ran. + text = NL.join(upending) + charset = Charset(mlist.preferred_language.charset) + incodec = charset.input_codec or 'ascii' + outcodec = charset.output_codec or 'ascii' + if isinstance(text, unicode): + return text.encode(outcodec, 'replace') + # Be sure this is a byte string encodeable in the list's charset + utext = unicode(text, incodec, 'replace') + return utext.encode(outcodec, 'replace') + + + +def auto_discard(mlist): + # Discard old held messages + discard_count = 0 + expire = config.days(mlist.max_days_to_hold) + requestsdb = IListRequests(mlist) + heldmsgs = list(requestsdb.of_type(RequestType.held_message)) + if expire and heldmsgs: + for request in heldmsgs: + key, data = requestsdb.get_request(request.id) + if now - data['date'] > expire: + handle_request(mlist, request.id, config.DISCARD) + discard_count += 1 + mlist.Save() + return discard_count + + + +# Figure out epoch seconds of midnight at the start of today (or the given +# 3-tuple date of (year, month, day). +def midnight(date=None): + if date is None: + date = time.localtime()[:3] + # -1 for dst flag tells the library to figure it out + return time.mktime(date + (0,)*5 + (-1,)) + + +def main(): + opts, args, parser = parseargs() + initialize(opts.config) + + for name in config.list_manager.names: + # The list must be locked in order to open the requests database + mlist = MailList.MailList(name) + try: + count = IListRequests(mlist).count + # While we're at it, let's evict yesterday's autoresponse data + midnight_today = midnight() + evictions = [] + for sender in mlist.hold_and_cmd_autoresponses.keys(): + date, respcount = mlist.hold_and_cmd_autoresponses[sender] + if midnight(date) < midnight_today: + evictions.append(sender) + if evictions: + for sender in evictions: + del mlist.hold_and_cmd_autoresponses[sender] + # This is the only place we've changed the list's database + mlist.Save() + if count: + # Set the default language the the list's preferred language. + _.default = mlist.preferred_language + realname = mlist.real_name + discarded = auto_discard(mlist) + if discarded: + count = count - discarded + text = _('Notice: $discarded old request(s) ' + 'automatically expired.\n\n') + else: + text = '' + if count: + text += Utils.maketext( + 'checkdbs.txt', + {'count' : count, + 'mail_host': mlist.mail_host, + 'adminDB' : mlist.GetScriptURL('admindb', + absolute=1), + 'real_name': realname, + }, mlist=mlist) + text += '\n' + pending_requests(mlist) + subject = _('$count $realname moderator ' + 'request(s) waiting') + else: + subject = _('$realname moderator request check result') + msg = UserNotification(mlist.GetOwnerEmail(), + mlist.GetBouncesEmail(), + subject, text, + mlist.preferred_language) + msg.send(mlist, **{'tomoderators': True}) + finally: + mlist.Unlock() + + + +if __name__ == '__main__': + main() diff --git a/port_me/config_list.py b/port_me/config_list.py new file mode 100644 index 000000000..a0b2a54f4 --- /dev/null +++ b/port_me/config_list.py @@ -0,0 +1,332 @@ +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys +import time +import optparse + +from mailman import MailList +from mailman import errors +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.initialize import initialize +from mailman.utilities.string import wrap +from mailman.version import MAILMAN_VERSION + + +NL = '\n' +nonasciipat = re.compile(r'[\x80-\xff]') + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] listname + +Configure a list from a text file description, or dump a list's configuration +settings.""")) + parser.add_option('-i', '--inputfile', + metavar='FILENAME', default=None, type='string', + help=_("""\ +Configure the list by assigning each module-global variable in the file to an +attribute on the mailing list object, then save the list. The named file is +loaded with execfile() and must be legal Python code. Any variable that isn't +already an attribute of the list object is ignored (a warning message is +printed). See also the -c option. + +A special variable named 'mlist' is put into the globals during the execfile, +which is bound to the actual MailList object. This lets you do all manner of +bizarre thing to the list object, but BEWARE! Using this can severely (and +possibly irreparably) damage your mailing list! + +The may not be used with the -o option.""")) + parser.add_option('-o', '--outputfile', + metavar='FILENAME', default=None, type='string', + help=_("""\ +Instead of configuring the list, print out a mailing list's configuration +variables in a format suitable for input using this script. In this way, you +can easily capture the configuration settings for a particular list and +imprint those settings on another list. FILENAME is the file to output the +settings to. If FILENAME is `-', standard out is used. + +This may not be used with the -i option.""")) + parser.add_option('-c', '--checkonly', + default=False, action='store_true', help=_("""\ +With this option, the modified list is not actually changed. This is only +useful with the -i option.""")) + parser.add_option('-v', '--verbose', + default=False, action='store_true', help=_("""\ +Print the name of each attribute as it is being changed. This is only useful +with the -i option.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if len(args) > 1: + parser.print_help() + parser.error(_('Unexpected arguments')) + if not args: + parser.error(_('List name is required')) + return parser, opts, args + + + +def do_output(listname, outfile, parser): + closep = False + try: + if outfile == '-': + outfp = sys.stdout + else: + outfp = open(outfile, 'w') + closep = True + # Open the specified list unlocked, since we're only reading it. + try: + mlist = MailList.MailList(listname, lock=False) + except errors.MMListError: + parser.error(_('No such list: $listname')) + # Preamble for the config info. PEP 263 charset and capture time. + charset = mlist.preferred_language.charset + # Set the system's default language. + _.default = mlist.preferred_language.code + if not charset: + charset = 'us-ascii' + when = time.ctime(time.time()) + print >> outfp, _('''\ +# -*- python -*- +# -*- coding: $charset -*- +## "$listname" mailing list configuration settings +## captured on $when +''') + # Get all the list config info. All this stuff is accessible via the + # web interface. + for k in config.ADMIN_CATEGORIES: + subcats = mlist.GetConfigSubCategories(k) + if subcats is None: + do_list_categories(mlist, k, None, outfp) + else: + for subcat in [t[0] for t in subcats]: + do_list_categories(mlist, k, subcat, outfp) + finally: + if closep: + outfp.close() + + + +def do_list_categories(mlist, k, subcat, outfp): + info = mlist.GetConfigInfo(k, subcat) + label, gui = mlist.GetConfigCategories()[k] + if info is None: + return + charset = mlist.preferred_language.charset + print >> outfp, '##', k.capitalize(), _('options') + print >> outfp, '#' + # First, massage the descripton text, which could have obnoxious + # leading whitespace on second and subsequent lines due to + # triple-quoted string nonsense in the source code. + desc = NL.join([s.lstrip() for s in info[0].splitlines()]) + # Print out the category description + desc = wrap(desc) + for line in desc.splitlines(): + print >> outfp, '#', line + print >> outfp + for data in info[1:]: + if not isinstance(data, tuple): + continue + varname = data[0] + # Variable could be volatile + if varname[0] == '_': + continue + vtype = data[1] + # First, massage the descripton text, which could have + # obnoxious leading whitespace on second and subsequent lines + # due to triple-quoted string nonsense in the source code. + desc = NL.join([s.lstrip() for s in data[-1].splitlines()]) + # Now strip out all HTML tags + desc = re.sub('<.*?>', '', desc) + # And convert </> to <> + desc = re.sub('<', '<', desc) + desc = re.sub('>', '>', desc) + # Print out the variable description. + desc = wrap(desc) + for line in desc.split('\n'): + print >> outfp, '#', line + # munge the value based on its type + value = None + if hasattr(gui, 'getValue'): + value = gui.getValue(mlist, vtype, varname, data[2]) + if value is None and not varname.startswith('_'): + value = getattr(mlist, varname) + if vtype in (config.String, config.Text, config.FileUpload): + print >> outfp, varname, '=', + lines = value.splitlines() + if not lines: + print >> outfp, "''" + elif len(lines) == 1: + if charset != 'us-ascii' and nonasciipat.search(lines[0]): + # This is more readable for non-english list. + print >> outfp, '"' + lines[0].replace('"', '\\"') + '"' + else: + print >> outfp, repr(lines[0]) + else: + if charset == 'us-ascii' and nonasciipat.search(value): + # Normally, an english list should not have non-ascii char. + print >> outfp, repr(NL.join(lines)) + else: + outfp.write(' """') + outfp.write(NL.join(lines).replace('"', '\\"')) + outfp.write('"""\n') + elif vtype in (config.Radio, config.Toggle): + print >> outfp, '#' + print >> outfp, '#', _('legal values are:') + # TBD: This is disgusting, but it's special cased + # everywhere else anyway... + if varname == 'subscribe_policy' and \ + not config.ALLOW_OPEN_SUBSCRIBE: + i = 1 + else: + i = 0 + for choice in data[2]: + print >> outfp, '# ', i, '= "%s"' % choice + i += 1 + print >> outfp, varname, '=', repr(value) + else: + print >> outfp, varname, '=', repr(value) + print >> outfp + + + +def getPropertyMap(mlist): + guibyprop = {} + categories = mlist.GetConfigCategories() + for category, (label, gui) in categories.items(): + if not hasattr(gui, 'GetConfigInfo'): + continue + subcats = mlist.GetConfigSubCategories(category) + if subcats is None: + subcats = [(None, None)] + for subcat, sclabel in subcats: + for element in gui.GetConfigInfo(mlist, category, subcat): + if not isinstance(element, tuple): + continue + propname = element[0] + wtype = element[1] + guibyprop[propname] = (gui, wtype) + return guibyprop + + +class FakeDoc: + # Fake the error reporting API for the htmlformat.Document class + def addError(self, s, tag=None, *args): + if tag: + print >> sys.stderr, tag + print >> sys.stderr, s % args + + def set_language(self, val): + pass + + + +def do_input(listname, infile, checkonly, verbose, parser): + fakedoc = FakeDoc() + # Open the specified list locked, unless checkonly is set + try: + mlist = MailList.MailList(listname, lock=not checkonly) + except errors.MMListError as error: + parser.error(_('No such list "$listname"\n$error')) + savelist = False + guibyprop = getPropertyMap(mlist) + try: + globals = {'mlist': mlist} + # Any exception that occurs in execfile() will cause the list to not + # be saved, but any other problems are not save-fatal. + execfile(infile, globals) + savelist = True + for k, v in globals.items(): + if k in ('mlist', '__builtins__'): + continue + if not hasattr(mlist, k): + print >> sys.stderr, _('attribute "$k" ignored') + continue + if verbose: + print >> sys.stderr, _('attribute "$k" changed') + missing = [] + gui, wtype = guibyprop.get(k, (missing, missing)) + if gui is missing: + # This isn't an official property of the list, but that's + # okay, we'll just restore it the old fashioned way + print >> sys.stderr, _('Non-standard property restored: $k') + setattr(mlist, k, v) + else: + # BAW: This uses non-public methods. This logic taken from + # the guts of GUIBase.handleForm(). + try: + validval = gui._getValidValue(mlist, k, wtype, v) + except ValueError: + print >> sys.stderr, _('Invalid value for property: $k') + except errors.EmailAddressError: + print >> sys.stderr, _( + 'Bad email address for option $k: $v') + else: + # BAW: Horrible hack, but then this is special cased + # everywhere anyway. :( Privacy._setValue() knows that + # when ALLOW_OPEN_SUBSCRIBE is false, the web values are + # 0, 1, 2 but these really should be 1, 2, 3, so it adds + # one. But we really do provide [0..3] so we need to undo + # the hack that _setValue adds. :( :( + if k == 'subscribe_policy' and \ + not config.ALLOW_OPEN_SUBSCRIBE: + validval -= 1 + # BAW: Another horrible hack. This one is just too hard + # to fix in a principled way in Mailman 2.1 + elif k == 'new_member_options': + # Because this is a Checkbox, _getValidValue() + # transforms the value into a list of one item. + validval = validval[0] + validval = [bitfield for bitfield, bitval + in config.OPTINFO.items() + if validval & bitval] + gui._setValue(mlist, k, validval, fakedoc) + # BAW: when to do gui._postValidate()??? + finally: + if savelist and not checkonly: + mlist.Save() + mlist.Unlock() + + + +def main(): + parser, opts, args = parseargs() + initialize(opts.config) + listname = args[0] + + # Sanity check + if opts.inputfile and opts.outputfile: + parser.error(_('Only one of -i or -o is allowed')) + if not opts.inputfile and not opts.outputfile: + parser.error(_('One of -i or -o is required')) + + if opts.outputfile: + do_output(listname, opts.outputfile, parser) + else: + do_input(listname, opts.inputfile, opts.checkonly, + opts.verbose, parser) + + + +if __name__ == '__main__': + main() diff --git a/port_me/disabled.py b/port_me/disabled.py new file mode 100644 index 000000000..b190556c2 --- /dev/null +++ b/port_me/disabled.py @@ -0,0 +1,201 @@ +# Copyright (C) 2001-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import time +import logging +import optparse + +from mailman import MailList +from mailman import MemberAdaptor +from mailman import Pending +from mailman import loginit +from mailman.Bouncer import _BounceInfo +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.interfaces.member import NotAMemberError +from mailman.version import MAILMAN_VERSION + + +# Work around known problems with some RedHat cron daemons +import signal +signal.signal(signal.SIGCHLD, signal.SIG_DFL) + +ALL = (MemberAdaptor.BYBOUNCE, + MemberAdaptor.BYADMIN, + MemberAdaptor.BYUSER, + MemberAdaptor.UNKNOWN, + ) + + + +def who_callback(option, opt, value, parser): + dest = getattr(parser.values, option.dest) + if opt in ('-o', '--byadmin'): + dest.add(MemberAdaptor.BYADMIN) + elif opt in ('-m', '--byuser'): + dest.add(MemberAdaptor.BYUSER) + elif opt in ('-u', '--unknown'): + dest.add(MemberAdaptor.UNKNOWN) + elif opt in ('-b', '--notbybounce'): + dest.discard(MemberAdaptor.BYBOUNCE) + elif opt in ('-a', '--all'): + dest.update(ALL) +x5o + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] + +Process disabled members, recommended once per day. + +This script iterates through every mailing list looking for members whose +delivery is disabled. If they have been disabled due to bounces, they will +receive another notification, or they may be removed if they've received the +maximum number of notifications. + +Use the --byadmin, --byuser, and --unknown flags to also send notifications to +members whose accounts have been disabled for those reasons. Use --all to +send the notification to all disabled members.""")) + # This is the set of working flags for who to send notifications to. By + # default, we notify anybody who has been disable due to bounces. + parser.set_defaults(who=set([MemberAdaptor.BYBOUNCE])) + parser.add_option('-o', '--byadmin', + callback=who_callback, action='callback', dest='who', + help=_("""\ +Also send notifications to any member disabled by the list +owner/administrator.""")) + parser.add_option('-m', '--byuser', + callback=who_callback, action='callback', dest='who', + help=_("""\ +Also send notifications to any member who has disabled themself.""")) + parser.add_option('-u', '--unknown', + callback=who_callback, action='callback', dest='who', + help=_("""\ +Also send notifications to any member disabled for unknown reasons +(usually a legacy disabled address).""")) + parser.add_option('-b', '--notbybounce', + callback=who_callback, action='callback', dest='who', + help=_("""\ +Don't send notifications to members disabled because of bounces (the +default is to notify bounce disabled members).""")) + parser.add_option('-a', '--all', + callback=who_callback, action='callback', dest='who', + help=_('Send notifications to all disabled members')) + parser.add_option('-f', '--force', + default=False, action='store_true', + help=_("""\ +Send notifications to disabled members even if they're not due a new +notification yet.""")) + parser.add_option('-l', '--listname', + dest='listnames', action='append', default=[], + type='string', help=_("""\ +Process only the given list, otherwise do all lists.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + return opts, args, parser + + + +def main(): + opts, args, parser = parseargs() + config.load(opts.config) + + loginit.initialize(propagate=True) + elog = logging.getLogger('mailman.error') + blog = logging.getLogger('mailman.bounce') + + listnames = set(opts.listnames or config.list_manager.names) + who = tuple(opts.who) + + msg = _('[disabled by periodic sweep and cull, no message available]') + today = time.mktime(time.localtime()[:3] + (0,) * 6) + for listname in listnames: + # List of members to notify + notify = [] + mlist = MailList.MailList(listname) + try: + interval = mlist.bounce_you_are_disabled_warnings_interval + # Find all the members who are currently bouncing and see if + # they've reached the disable threshold but haven't yet been + # disabled. This is a sweep through the membership catching + # situations where they've bounced a bunch, then the list admin + # lowered the threshold, but we haven't (yet) seen more bounces + # from the member. Note: we won't worry about stale information + # or anything else since the normal bounce processing code will + # handle that. + disables = [] + for member in mlist.getBouncingMembers(): + if mlist.getDeliveryStatus(member) <> MemberAdaptor.ENABLED: + continue + info = mlist.getBounceInfo(member) + if info.score >= mlist.bounce_score_threshold: + disables.append((member, info)) + if disables: + for member, info in disables: + mlist.disableBouncingMember(member, info, msg) + # Go through all the members who have delivery disabled, and find + # those that are due to have another notification. If they are + # disabled for another reason than bouncing, and we're processing + # them (because of the command line switch) then they won't have a + # bounce info record. We can piggyback on that for all disable + # purposes. + members = mlist.getDeliveryStatusMembers(who) + for member in members: + info = mlist.getBounceInfo(member) + if not info: + # See if they are bounce disabled, or disabled for some + # other reason. + status = mlist.getDeliveryStatus(member) + if status == MemberAdaptor.BYBOUNCE: + elog.error( + '%s disabled BYBOUNCE lacks bounce info, list: %s', + member, mlist.internal_name()) + continue + info = _BounceInfo( + member, 0, today, + mlist.bounce_you_are_disabled_warnings, + mlist.pend_new(Pending.RE_ENABLE, + mlist.internal_name(), + member)) + mlist.setBounceInfo(member, info) + lastnotice = time.mktime(info.lastnotice + (0,) * 6) + if opts.force or today >= lastnotice + interval: + notify.append(member) + # Now, send notifications to anyone who is due + for member in notify: + blog.info('Notifying disabled member %s for list: %s', + member, mlist.internal_name()) + try: + mlist.sendNextNotification(member) + except NotAMemberError: + # There must have been some problem with the data we have + # on this member. Most likely it's that they don't have a + # password assigned. Log this and delete the member. + blog.info( + 'Cannot send disable notice to non-member: %s', + member) + mlist.ApprovedDeleteMember(member, 'cron/disabled') + mlist.Save() + finally: + mlist.Unlock() + + + +if __name__ == '__main__': + main() diff --git a/port_me/export.py b/port_me/export.py new file mode 100644 index 000000000..279abc36f --- /dev/null +++ b/port_me/export.py @@ -0,0 +1,310 @@ +# Copyright (C) 2006-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Export an XML representation of a mailing list.""" + +import sys +import codecs +import datetime +import optparse + +from xml.sax.saxutils import escape + +from mailman import Defaults +from mailman import errors +from mailman import MemberAdaptor +from mailman.MailList import MailList +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.initialize import initialize +from mailman.version import MAILMAN_VERSION + + +SPACE = ' ' + +TYPES = { + Defaults.Toggle : 'bool', + Defaults.Radio : 'radio', + Defaults.String : 'string', + Defaults.Text : 'text', + Defaults.Email : 'email', + Defaults.EmailList : 'email_list', + Defaults.Host : 'host', + Defaults.Number : 'number', + Defaults.FileUpload : 'upload', + Defaults.Select : 'select', + Defaults.Topics : 'topics', + Defaults.Checkbox : 'checkbox', + Defaults.EmailListEx : 'email_list_ex', + Defaults.HeaderFilter : 'header_filter', + } + + + +class Indenter: + def __init__(self, fp, indentwidth=4): + self._fp = fp + self._indent = 0 + self._width = indentwidth + + def indent(self): + self._indent += 1 + + def dedent(self): + self._indent -= 1 + assert self._indent >= 0 + + def write(self, s): + if s <> '\n': + self._fp.write(self._indent * self._width * ' ') + self._fp.write(s) + + + +class XMLDumper(object): + def __init__(self, fp): + self._fp = Indenter(fp) + self._tagbuffer = None + self._stack = [] + + def _makeattrs(self, tagattrs): + # The attribute values might contain angle brackets. They might also + # be None. + attrs = [] + for k, v in tagattrs.items(): + if v is None: + v = '' + else: + v = escape(str(v)) + attrs.append('%s="%s"' % (k, v)) + return SPACE.join(attrs) + + def _flush(self, more=True): + if not self._tagbuffer: + return + name, attributes = self._tagbuffer + self._tagbuffer = None + if attributes: + attrstr = ' ' + self._makeattrs(attributes) + else: + attrstr = '' + if more: + print >> self._fp, '<%s%s>' % (name, attrstr) + self._fp.indent() + self._stack.append(name) + else: + print >> self._fp, '<%s%s/>' % (name, attrstr) + + # Use this method when you know you have sub-elements. + def _push_element(self, _name, **_tagattrs): + self._flush() + self._tagbuffer = (_name, _tagattrs) + + def _pop_element(self, _name): + buffered = bool(self._tagbuffer) + self._flush(more=False) + if not buffered: + name = self._stack.pop() + assert name == _name, 'got: %s, expected: %s' % (_name, name) + self._fp.dedent() + print >> self._fp, '</%s>' % name + + # Use this method when you do not have sub-elements + def _element(self, _name, _value=None, **_attributes): + self._flush() + if _attributes: + attrs = ' ' + self._makeattrs(_attributes) + else: + attrs = '' + if _value is None: + print >> self._fp, '<%s%s/>' % (_name, attrs) + else: + # The value might contain angle brackets. + value = escape(_value.decode('utf-8')) + print >> self._fp, '<%s%s>%s</%s>' % (_name, attrs, value, _name) + + def _do_list_categories(self, mlist, k, subcat=None): + info = mlist.GetConfigInfo(k, subcat) + label, gui = mlist.GetConfigCategories()[k] + if info is None: + return + for data in info[1:]: + if not isinstance(data, tuple): + continue + varname = data[0] + # Variable could be volatile + if varname.startswith('_'): + continue + vtype = data[1] + # Munge the value based on its type + value = None + if hasattr(gui, 'getValue'): + value = gui.getValue(mlist, vtype, varname, data[2]) + if value is None: + value = getattr(mlist, varname) + widget_type = TYPES[vtype] + if isinstance(value, list): + self._push_element('option', name=varname, type=widget_type) + for v in value: + self._element('value', v) + self._pop_element('option') + else: + self._element('option', value, name=varname, type=widget_type) + + def _dump_list(self, mlist): + # Write list configuration values + self._push_element('list', name=mlist.fqdn_listname) + self._push_element('configuration') + self._element('option', + mlist.preferred_language, + name='preferred_language') + for k in config.ADMIN_CATEGORIES: + subcats = mlist.GetConfigSubCategories(k) + if subcats is None: + self._do_list_categories(mlist, k) + else: + for subcat in [t[0] for t in subcats]: + self._do_list_categories(mlist, k, subcat) + self._pop_element('configuration') + # Write membership + self._push_element('roster') + digesters = set(mlist.getDigestMemberKeys()) + for member in sorted(mlist.getMembers()): + attrs = dict(id=member) + cased = mlist.getMemberCPAddress(member) + if cased <> member: + attrs['original'] = cased + self._push_element('member', **attrs) + self._element('realname', mlist.getMemberName(member)) + self._element('password', mlist.getMemberPassword(member)) + self._element('language', mlist.getMemberLanguage(member)) + # Delivery status, combined with the type of delivery + attrs = {} + status = mlist.getDeliveryStatus(member) + if status == MemberAdaptor.ENABLED: + attrs['status'] = 'enabled' + else: + attrs['status'] = 'disabled' + attrs['reason'] = {MemberAdaptor.BYUSER : 'byuser', + MemberAdaptor.BYADMIN : 'byadmin', + MemberAdaptor.BYBOUNCE : 'bybounce', + }.get(mlist.getDeliveryStatus(member), + 'unknown') + if member in digesters: + if mlist.getMemberOption(member, Defaults.DisableMime): + attrs['delivery'] = 'plain' + else: + attrs['delivery'] = 'mime' + else: + attrs['delivery'] = 'regular' + changed = mlist.getDeliveryStatusChangeTime(member) + if changed: + when = datetime.datetime.fromtimestamp(changed) + attrs['changed'] = when.isoformat() + self._element('delivery', **attrs) + for option, flag in Defaults.OPTINFO.items(): + # Digest/Regular delivery flag must be handled separately + if option in ('digest', 'plain'): + continue + value = mlist.getMemberOption(member, flag) + self._element(option, value) + topics = mlist.getMemberTopics(member) + if not topics: + self._element('topics') + else: + self._push_element('topics') + for topic in topics: + self._element('topic', topic) + self._pop_element('topics') + self._pop_element('member') + self._pop_element('roster') + self._pop_element('list') + + def dump(self, listnames): + print >> self._fp, '<?xml version="1.0" encoding="UTF-8"?>' + self._push_element('mailman', **{ + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:noNamespaceSchemaLocation': 'ssi-1.0.xsd', + }) + for listname in sorted(listnames): + try: + mlist = MailList(listname, lock=False) + except errors.MMUnknownListError: + print >> sys.stderr, _('No such list: $listname') + continue + self._dump_list(mlist) + self._pop_element('mailman') + + def close(self): + while self._stack: + self._pop_element() + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] + +Export the configuration and members of a mailing list in XML format.""")) + parser.add_option('-o', '--outputfile', + metavar='FILENAME', default=None, type='string', + help=_("""\ +Output XML to FILENAME. If not given, or if FILENAME is '-', standard out is +used.""")) + parser.add_option('-l', '--listname', + default=[], action='append', type='string', + metavar='LISTNAME', dest='listnames', help=_("""\ +The list to include in the output. If not given, then all mailing lists are +included in the XML output. Multiple -l flags may be given.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if args: + parser.print_help() + parser.error(_('Unexpected arguments')) + return parser, opts, args + + + +def main(): + parser, opts, args = parseargs() + initialize(opts.config) + + close = False + if opts.outputfile in (None, '-'): + writer = codecs.getwriter('utf-8') + fp = writer(sys.stdout) + else: + fp = codecs.open(opts.outputfile, 'w', 'utf-8') + close = True + + try: + dumper = XMLDumper(fp) + if opts.listnames: + listnames = [] + for listname in opts.listnames: + if '@' not in listname: + listname = '%s@%s' % (listname, config.DEFAULT_EMAIL_HOST) + listnames.append(listname) + else: + listnames = config.list_manager.names + dumper.dump(listnames) + dumper.close() + finally: + if close: + fp.close() diff --git a/port_me/find_member.py b/port_me/find_member.py new file mode 100644 index 000000000..349af8247 --- /dev/null +++ b/port_me/find_member.py @@ -0,0 +1,135 @@ +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys +import optparse + +from mailman import errors +from mailman import MailList +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.version import MAILMAN_VERSION + + +AS_MEMBER = 0x01 +AS_OWNER = 0x02 + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] regex [regex ...] + +Find all lists that a member's address is on. + +The interaction between -l and -x (see below) is as follows. If any -l option +is given then only the named list will be included in the search. If any -x +option is given but no -l option is given, then all lists will be search +except those specifically excluded. + +Regular expression syntax uses the Python 're' module. Complete +specifications are at: + +http://www.python.org/doc/current/lib/module-re.html + +Address matches are case-insensitive, but case-preserved addresses are +displayed.""")) + parser.add_option('-l', '--listname', + type='string', default=[], action='append', + dest='listnames', + help=_('Include only the named list in the search')) + parser.add_option('-x', '--exclude', + type='string', default=[], action='append', + dest='excludes', + help=_('Exclude the named list from the search')) + parser.add_option('-w', '--owners', + default=False, action='store_true', + help=_('Search list owners as well as members')) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if not args: + parser.print_help() + print >> sys.stderr, _('Search regular expression required') + sys.exit(1) + return parser, opts, args + + + +def main(): + parser, opts, args = parseargs() + config.load(opts.config) + + listnames = opts.listnames or config.list_manager.names + includes = set(listname.lower() for listname in listnames) + excludes = set(listname.lower() for listname in opts.excludes) + listnames = includes - excludes + + if not listnames: + print _('No lists to search') + return + + cres = [] + for r in args: + cres.append(re.compile(r, re.IGNORECASE)) + # dictionary of {address, (listname, ownerp)} + matches = {} + for listname in listnames: + try: + mlist = MailList.MailList(listname, lock=False) + except errors.MMListError: + print _('No such list: $listname') + continue + if opts.owners: + owners = mlist.owner + else: + owners = [] + for cre in cres: + for member in mlist.getMembers(): + if cre.search(member): + addr = mlist.getMemberCPAddress(member) + entries = matches.get(addr, {}) + aswhat = entries.get(listname, 0) + aswhat |= AS_MEMBER + entries[listname] = aswhat + matches[addr] = entries + for owner in owners: + if cre.search(owner): + entries = matches.get(owner, {}) + aswhat = entries.get(listname, 0) + aswhat |= AS_OWNER + entries[listname] = aswhat + matches[owner] = entries + addrs = matches.keys() + addrs.sort() + for k in addrs: + hits = matches[k] + lists = hits.keys() + print k, _('found in:') + for name in lists: + aswhat = hits[name] + if aswhat & AS_MEMBER: + print ' ', name + if aswhat & AS_OWNER: + print ' ', name, _('(as owner)') + + + +if __name__ == '__main__': + main() diff --git a/port_me/gate_news.py b/port_me/gate_news.py new file mode 100644 index 000000000..72568cd1b --- /dev/null +++ b/port_me/gate_news.py @@ -0,0 +1,244 @@ +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import time +import socket +import logging +import nntplib +import optparse +import email.Errors + +from email.Parser import Parser +from flufl.lock import Lock, TimeOutError +from lazr.config import as_host_port + +from mailman import MailList +from mailman import Message +from mailman import loginit +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.core.switchboard import Switchboard +from mailman.version import MAILMAN_VERSION + +# Work around known problems with some RedHat cron daemons +import signal +signal.signal(signal.SIGCHLD, signal.SIG_DFL) + +NL = '\n' + +log = None + +class _ContinueLoop(Exception): + pass + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] + +Poll the NNTP servers for messages to be gatewayed to mailing lists.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if args: + parser.print_help() + print >> sys.stderr, _('Unexpected arguments') + sys.exit(1) + return opts, args, parser + + + +_hostcache = {} + +def open_newsgroup(mlist): + # Split host:port if given. + nntp_host, nntp_port = as_host_port(mlist.nntp_host, default_port=119) + # Open up a "mode reader" connection to nntp server. This will be shared + # for all the gated lists having the same nntp_host. + conn = _hostcache.get(mlist.nntp_host) + if conn is None: + try: + conn = nntplib.NNTP(nntp_host, nntp_port, + readermode=True, + user=config.NNTP_USERNAME, + password=config.NNTP_PASSWORD) + except (socket.error, nntplib.NNTPError, IOError) as e: + log.error('error opening connection to nntp_host: %s\n%s', + mlist.nntp_host, e) + raise + _hostcache[mlist.nntp_host] = conn + # Get the GROUP information for the list, but we're only really interested + # in the first article number and the last article number + r, c, f, l, n = conn.group(mlist.linked_newsgroup) + return conn, int(f), int(l) + + +def clearcache(): + for conn in set(_hostcache.values()): + conn.quit() + _hostcache.clear() + + + +# This function requires the list to be locked. +def poll_newsgroup(mlist, conn, first, last, glock): + listname = mlist.internal_name() + # NEWNEWS is not portable and has synchronization issues. + for num in range(first, last): + glock.refresh() + try: + headers = conn.head(repr(num))[3] + found_to = False + beenthere = False + for header in headers: + i = header.find(':') + value = header[:i].lower() + if i > 0 and value == 'to': + found_to = True + # FIXME 2010-02-16 barry use List-Post header. + if value <> 'x-beenthere': + continue + if header[i:] == ': %s' % mlist.posting_address: + beenthere = True + break + if not beenthere: + body = conn.body(repr(num))[3] + # Usenet originated messages will not have a Unix envelope + # (i.e. "From " header). This breaks Pipermail archiving, so + # we will synthesize one. Be sure to use the format searched + # for by mailbox.UnixMailbox._isrealfromline(). BAW: We use + # the -bounces address here in case any downstream clients use + # the envelope sender for bounces; I'm not sure about this, + # but it's the closest to the old semantics. + lines = ['From %s %s' % (mlist.GetBouncesEmail(), + time.ctime(time.time()))] + lines.extend(headers) + lines.append('') + lines.extend(body) + lines.append('') + p = Parser(Message.Message) + try: + msg = p.parsestr(NL.join(lines)) + except email.Errors.MessageError as e: + log.error('email package exception for %s:%d\n%s', + mlist.linked_newsgroup, num, e) + raise _ContinueLoop + if found_to: + del msg['X-Originally-To'] + msg['X-Originally-To'] = msg['To'] + del msg['To'] + msg['To'] = mlist.posting_address + # Post the message to the locked list + inq = Switchboard(config.INQUEUE_DIR) + inq.enqueue(msg, + listid=mlist.list_id, + fromusenet=True) + log.info('posted to list %s: %7d', listname, num) + except nntplib.NNTPError as e: + log.exception('NNTP error for list %s: %7d', listname, num) + except _ContinueLoop: + continue + # Even if we don't post the message because it was seen on the + # list already, update the watermark + mlist.usenet_watermark = num + + + +def process_lists(glock): + for listname in config.list_manager.names: + glock.refresh() + # Open the list unlocked just to check to see if it is gating news to + # mail. If not, we're done with the list. Otherwise, lock the list + # and gate the group. + mlist = MailList.MailList(listname, lock=False) + if not mlist.gateway_to_mail: + continue + # Get the list's watermark, i.e. the last article number that we gated + # from news to mail. None means that this list has never polled its + # newsgroup and that we should do a catch up. + watermark = getattr(mlist, 'usenet_watermark', None) + # Open the newsgroup, but let most exceptions percolate up. + try: + conn, first, last = open_newsgroup(mlist) + except (socket.error, nntplib.NNTPError): + break + log.info('%s: [%d..%d]', listname, first, last) + try: + try: + if watermark is None: + mlist.Lock(timeout=config.LIST_LOCK_TIMEOUT) + # This is the first time we've tried to gate this + # newsgroup. We essentially do a mass catch-up, otherwise + # we'd flood the mailing list. + mlist.usenet_watermark = last + log.info('%s caught up to article %d', listname, last) + else: + # The list has been polled previously, so now we simply + # grab all the messages on the newsgroup that have not + # been seen by the mailing list. The first such article + # is the maximum of the lowest article available in the + # newsgroup and the watermark. It's possible that some + # articles have been expired since the last time gate_news + # has run. Not much we can do about that. + start = max(watermark + 1, first) + if start > last: + log.info('nothing new for list %s', listname) + else: + mlist.Lock(timeout=config.LIST_LOCK_TIMEOUT) + log.info('gating %s articles [%d..%d]', + listname, start, last) + # Use last+1 because poll_newsgroup() employes a for + # loop over range, and this will not include the last + # element in the list. + poll_newsgroup(mlist, conn, start, last + 1, glock) + except TimeOutError: + log.error('Could not acquire list lock: %s', listname) + finally: + if mlist.Locked(): + mlist.Save() + mlist.Unlock() + log.info('%s watermark: %d', listname, mlist.usenet_watermark) + + + +def main(): + opts, args, parser = parseargs() + config.load(opts.config) + + GATENEWS_LOCK_FILE = os.path.join(config.LOCK_DIR, 'gate_news.lock') + LOCK_LIFETIME = config.hours(2) + + loginit.initialize(propagate=True) + log = logging.getLogger('mailman.fromusenet') + + try: + with Lock(GATENEWS_LOCK_FILE, + # It's okay to hijack this + lifetime=LOCK_LIFETIME) as lock: + process_lists(lock) + clearcache() + except TimeOutError: + log.error('Could not acquire gate_news lock') + + + +if __name__ == '__main__': + main() diff --git a/port_me/list_owners.py b/port_me/list_owners.py new file mode 100644 index 000000000..5b5fca2bf --- /dev/null +++ b/port_me/list_owners.py @@ -0,0 +1,90 @@ +# Copyright (C) 2002-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import sys +import optparse + +from zope.component import getUtility + +from mailman.MailList import MailList +from mailman.core.i18n import _ +from mailman.initialize import initialize +from mailman.interfaces.listmanager import IListManager +from mailman.version import MAILMAN_VERSION + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] [listname ...] + +List the owners of a mailing list, or all mailing lists if no list names are +given.""")) + parser.add_option('-w', '--with-listnames', + default=False, action='store_true', + help=_("""\ +Group the owners by list names and include the list names in the output. +Otherwise, the owners will be sorted and uniquified based on the email +address.""")) + parser.add_option('-m', '--moderators', + default=False, action='store_true', + help=_('Include the list moderators in the output.')) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + return parser, opts, args + + + +def main(): + parser, opts, args = parseargs() + initialize(opts.config) + + list_manager = getUtility(IListManager) + listnames = set(args or list_manager.names) + bylist = {} + + for listname in listnames: + mlist = list_manager.get(listname) + addrs = [addr.address for addr in mlist.owners.addresses] + if opts.moderators: + addrs.extend([addr.address for addr in mlist.moderators.addresses]) + bylist[listname] = addrs + + if opts.with_listnames: + for listname in listnames: + unique = set() + for addr in bylist[listname]: + unique.add(addr) + keys = list(unique) + keys.sort() + print listname + for k in keys: + print '\t', k + else: + unique = set() + for listname in listnames: + for addr in bylist[listname]: + unique.add(addr) + for k in sorted(unique): + print k + + + +if __name__ == '__main__': + main() diff --git a/port_me/senddigests.py b/port_me/senddigests.py new file mode 100644 index 000000000..59c03de2d --- /dev/null +++ b/port_me/senddigests.py @@ -0,0 +1,83 @@ +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import optparse + +from mailman import MailList +from mailman.core.i18n import _ +from mailman.initialize import initialize +from mailman.version import MAILMAN_VERSION + +# Work around known problems with some RedHat cron daemons +import signal +signal.signal(signal.SIGCHLD, signal.SIG_DFL) + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%prog [options] + +Dispatch digests for lists w/pending messages and digest_send_periodic +set.""")) + parser.add_option('-l', '--listname', + type='string', default=[], action='append', + dest='listnames', help=_("""\ +Send the digest for the given list only, otherwise the digests for all +lists are sent out. Multiple -l options may be given.""")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + if args: + parser.print_help() + print >> sys.stderr, _('Unexpected arguments') + sys.exit(1) + return opts, args, parser + + + +def main(): + opts, args, parser = parseargs() + initialize(opts.config) + + for listname in set(opts.listnames or config.list_manager.names): + mlist = MailList.MailList(listname, lock=False) + if mlist.digest_send_periodic: + mlist.Lock() + try: + try: + mlist.send_digest_now() + mlist.Save() + # We are unable to predict what exception may occur in digest + # processing and we don't want to lose the other digests, so + # we catch everything. + except Exception as errmsg: + print >> sys.stderr, \ + 'List: %s: problem processing %s:\n%s' % \ + (listname, + os.path.join(mlist.data_path, 'digest.mbox'), + errmsg) + finally: + mlist.Unlock() + + + +if __name__ == '__main__': + main() diff --git a/port_me/show_config.py b/port_me/show_config.py new file mode 100644 index 000000000..290840ae3 --- /dev/null +++ b/port_me/show_config.py @@ -0,0 +1,97 @@ +# Copyright (C) 2006-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys +import pprint +import optparse + +from mailman.configuration import config +from mailman.core.i18n import _ +from mailman.version import MAILMAN_VERSION + + +# List of names never to show even if --verbose +NEVER_SHOW = ['__builtins__', '__doc__'] + + + +def parseargs(): + parser = optparse.OptionParser(version=MAILMAN_VERSION, + usage=_("""\ +%%prog [options] [pattern ...] + +Show the values of various Defaults.py/mailman.cfg variables. +If one or more patterns are given, show only those variables +whose names match a pattern""")) + parser.add_option('-v', '--verbose', + default=False, action='store_true', + help=_( +"Show all configuration names, not just 'settings'.")) + parser.add_option('-i', '--ignorecase', + default=False, action='store_true', + help=_("Match patterns case-insensitively.")) + parser.add_option('-C', '--config', + help=_('Alternative configuration file to use')) + opts, args = parser.parse_args() + return parser, opts, args + + + +def main(): + parser, opts, args = parseargs() + + patterns = [] + if opts.ignorecase: + flag = re.IGNORECASE + else: + flag = 0 + for pattern in args: + patterns.append(re.compile(pattern, flag)) + + pp = pprint.PrettyPrinter(indent=4) + config.load(opts.config) + names = config.__dict__.keys() + names.sort() + for name in names: + if name in NEVER_SHOW: + continue + if not opts.verbose: + if name.startswith('_') or re.search('[a-z]', name): + continue + if patterns: + hit = False + for pattern in patterns: + if pattern.search(name): + hit = True + break + if not hit: + continue + value = config.__dict__[name] + if isinstance(value, str): + if re.search('\n', value): + print '%s = """%s"""' %(name, value) + else: + print "%s = '%s'" % (name, value) + else: + print '%s = ' % name, + pp.pprint(value) + + + +if __name__ == '__main__': + main() |
