From e33d6d6b88d0340e7e9adbc8638db306b19e2631 Mon Sep 17 00:00:00 2001
From: Barry Warsaw
Date: Mon, 2 Mar 2015 21:01:24 -0500
Subject: Move some old bin scripts to a holding tank.
---
port_me/bumpdigests.py | 74 +++++++++++
port_me/checkdbs.py | 209 +++++++++++++++++++++++++++++++
port_me/config_list.py | 332 +++++++++++++++++++++++++++++++++++++++++++++++++
port_me/disabled.py | 201 ++++++++++++++++++++++++++++++
port_me/export.py | 310 +++++++++++++++++++++++++++++++++++++++++++++
port_me/find_member.py | 135 ++++++++++++++++++++
port_me/gate_news.py | 244 ++++++++++++++++++++++++++++++++++++
port_me/list_owners.py | 90 ++++++++++++++
port_me/senddigests.py | 83 +++++++++++++
port_me/show_config.py | 97 +++++++++++++++
10 files changed, 1775 insertions(+)
create mode 100644 port_me/bumpdigests.py
create mode 100644 port_me/checkdbs.py
create mode 100644 port_me/config_list.py
create mode 100644 port_me/disabled.py
create mode 100644 port_me/export.py
create mode 100644 port_me/find_member.py
create mode 100644 port_me/gate_news.py
create mode 100644 port_me/list_owners.py
create mode 100644 port_me/senddigests.py
create mode 100644 port_me/show_config.py
(limited to 'port_me')
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 .
+
+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 .
+
+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 .
+
+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 .
+
+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 .
+
+"""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, ''
+ 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 .
+
+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 .
+
+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 .
+
+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 .
+
+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 .
+
+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()
--
cgit v1.2.3-70-g09d2