summaryrefslogtreecommitdiff
path: root/src/mailman/bin
diff options
context:
space:
mode:
authorBarry Warsaw2009-01-25 13:01:41 -0500
committerBarry Warsaw2009-01-25 13:01:41 -0500
commiteefd06f1b88b8ecbb23a9013cd223b72ca85c20d (patch)
tree72c947fe16fce0e07e996ee74020b26585d7e846 /src/mailman/bin
parent07871212f74498abd56bef3919bf3e029eb8b930 (diff)
downloadmailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.gz
mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.zst
mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.zip
Diffstat (limited to 'src/mailman/bin')
-rw-r--r--src/mailman/bin/__init__.py61
-rw-r--r--src/mailman/bin/add_members.py186
-rw-r--r--src/mailman/bin/arch.py151
-rw-r--r--src/mailman/bin/bumpdigests.py74
-rw-r--r--src/mailman/bin/check_perms.py408
-rw-r--r--src/mailman/bin/checkdbs.py199
-rw-r--r--src/mailman/bin/cleanarch.py133
-rw-r--r--src/mailman/bin/config_list.py332
-rw-r--r--src/mailman/bin/create_list.py129
-rw-r--r--src/mailman/bin/disabled.py201
-rw-r--r--src/mailman/bin/docs/master.txt49
-rw-r--r--src/mailman/bin/dumpdb.py88
-rw-r--r--src/mailman/bin/export.py310
-rw-r--r--src/mailman/bin/find_member.py135
-rw-r--r--src/mailman/bin/gate_news.py243
-rw-r--r--src/mailman/bin/genaliases.py64
-rw-r--r--src/mailman/bin/import.py315
-rw-r--r--src/mailman/bin/inject.py89
-rw-r--r--src/mailman/bin/list_lists.py104
-rw-r--r--src/mailman/bin/list_members.py201
-rw-r--r--src/mailman/bin/list_owners.py88
-rw-r--r--src/mailman/bin/mailmanctl.py232
-rw-r--r--src/mailman/bin/master.py452
-rw-r--r--src/mailman/bin/mmsitepass.py113
-rw-r--r--src/mailman/bin/nightly_gzip.py117
-rw-r--r--src/mailman/bin/qrunner.py269
-rw-r--r--src/mailman/bin/remove_list.py83
-rw-r--r--src/mailman/bin/senddigests.py83
-rw-r--r--src/mailman/bin/set_members.py189
-rw-r--r--src/mailman/bin/show_config.py97
-rw-r--r--src/mailman/bin/show_qfiles.py91
-rw-r--r--src/mailman/bin/unshunt.py51
-rw-r--r--src/mailman/bin/update.py660
-rw-r--r--src/mailman/bin/version.py46
-rw-r--r--src/mailman/bin/withlist.py220
35 files changed, 6263 insertions, 0 deletions
diff --git a/src/mailman/bin/__init__.py b/src/mailman/bin/__init__.py
new file mode 100644
index 000000000..d61693c5e
--- /dev/null
+++ b/src/mailman/bin/__init__.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2007-2009 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/>.
+
+__all__ = [
+ 'add_members',
+ 'arch',
+ 'bounces',
+ 'bumpdigests',
+ 'check_perms',
+ 'checkdbs',
+ 'cleanarch',
+ 'config_list',
+ 'confirm',
+ 'create_list',
+ 'disabled',
+ 'dumpdb',
+ 'export',
+ 'find_member',
+ 'gate_news',
+ 'genaliases',
+ 'import',
+ 'inject',
+ 'join',
+ 'leave',
+ 'list_lists',
+ 'list_members',
+ 'list_owners',
+ 'mailmanctl',
+ 'make_instance',
+ 'master',
+ 'mmsitepass',
+ 'nightly_gzip',
+ 'owner',
+ 'post',
+ 'qrunner',
+ 'remove_list',
+ 'request',
+ 'senddigests',
+ 'set_members',
+ 'show_config',
+ 'show_qfiles',
+ 'testall',
+ 'unshunt',
+ 'update',
+ 'version',
+ 'withlist',
+ ]
diff --git a/src/mailman/bin/add_members.py b/src/mailman/bin/add_members.py
new file mode 100644
index 000000000..9c87f4af9
--- /dev/null
+++ b/src/mailman/bin/add_members.py
@@ -0,0 +1,186 @@
+# Copyright (C) 1998-2009 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 codecs
+
+from cStringIO import StringIO
+from email.utils import parseaddr
+
+from mailman import Message
+from mailman import Utils
+from mailman import i18n
+from mailman.app.membership import add_member
+from mailman.config import config
+from mailman.core import errors
+from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode
+from mailman.options import SingleMailingListOptions
+
+_ = i18n._
+
+
+
+class ScriptOptions(SingleMailingListOptions):
+ usage=_("""\
+%prog [options]
+
+Add members to a list. 'listname' is the name of the Mailman list you are
+adding members to; the list must already exist.
+
+You must supply at least one of -r and -d options. At most one of the
+files can be '-'.
+""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-r', '--regular-members-file',
+ type='string', dest='regular', help=_("""\
+A file containing addresses of the members to be added, one address per line.
+This list of people become non-digest members. If file is '-', read addresses
+from stdin."""))
+ self.parser.add_option(
+ '-d', '--digest-members-file',
+ type='string', dest='digest', help=_("""\
+Similar to -r, but these people become digest members."""))
+ self.parser.add_option(
+ '-w', '--welcome-msg',
+ type='yesno', metavar='<y|n>', help=_("""\
+Set whether or not to send the list members a welcome message, overriding
+whatever the list's 'send_welcome_msg' setting is."""))
+ self.parser.add_option(
+ '-a', '--admin-notify',
+ type='yesno', metavar='<y|n>', help=_("""\
+Set whether or not to send the list administrators a notification on the
+success/failure of these subscriptions, overriding whatever the list's
+'admin_notify_mchanges' setting is."""))
+
+ def sanity_check(self):
+ if not self.options.listname:
+ self.parser.error(_('Missing listname'))
+ if len(self.arguments) > 0:
+ self.parser.print_error(_('Unexpected arguments'))
+ if self.options.regular is None and self.options.digest is None:
+ parser.error(_('At least one of -r or -d is required'))
+ if self.options.regular == '-' and self.options.digest == '-':
+ parser.error(_("-r and -d cannot both be '-'"))
+
+
+
+def readfile(filename):
+ if filename == '-':
+ fp = sys.stdin
+ else:
+ # XXX Need to specify other encodings.
+ fp = codecs.open(filename, encoding='utf-8')
+ # Strip all the lines of whitespace and discard blank lines
+ try:
+ return set(line.strip() for line in fp if line)
+ finally:
+ if fp is not sys.stdin:
+ fp.close()
+
+
+
+class Tee:
+ def __init__(self, outfp):
+ self._outfp = outfp
+
+ def write(self, msg):
+ sys.stdout.write(msg)
+ self._outfp.write(msg)
+
+
+
+def addall(mlist, subscribers, delivery_mode, ack, admin_notify, outfp):
+ tee = Tee(outfp)
+ for subscriber in subscribers:
+ try:
+ fullname, address = parseaddr(subscriber)
+ # Watch out for the empty 8-bit string.
+ if not fullname:
+ fullname = u''
+ password = Utils.MakeRandomPassword()
+ add_member(mlist, address, fullname, password, delivery_mode,
+ unicode(config.mailman.default_language))
+ # XXX Support ack and admin_notify
+ except AlreadySubscribedError:
+ print >> tee, _('Already a member: $subscriber')
+ except errors.InvalidEmailAddress:
+ if not address:
+ print >> tee, _('Bad/Invalid email address: blank line')
+ else:
+ print >> tee, _('Bad/Invalid email address: $subscriber')
+ else:
+ print >> tee, _('Subscribing: $subscriber')
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ fqdn_listname = options.options.listname
+ mlist = config.db.list_manager.get(fqdn_listname)
+ if mlist is None:
+ parser.error(_('No such list: $fqdn_listname'))
+
+ # Set up defaults.
+ send_welcome_msg = (options.options.welcome_msg
+ if options.options.welcome_msg is not None
+ else mlist.send_welcome_msg)
+ admin_notify = (options.options.admin_notify
+ if options.options.admin_notify is not None
+ else mlist.admin_notify)
+
+ with i18n.using_language(mlist.preferred_language):
+ if options.options.digest:
+ dmembers = readfile(options.options.digest)
+ else:
+ dmembers = set()
+ if options.options.regular:
+ nmembers = readfile(options.options.regular)
+ else:
+ nmembers = set()
+
+ if not dmembers and not nmembers:
+ print _('Nothing to do.')
+ sys.exit(0)
+
+ outfp = StringIO()
+ if nmembers:
+ addall(mlist, nmembers, DeliveryMode.regular,
+ send_welcome_msg, admin_notify, outfp)
+
+ if dmembers:
+ addall(mlist, dmembers, DeliveryMode.mime_digests,
+ send_welcome_msg, admin_notify, outfp)
+
+ config.db.commit()
+
+ if admin_notify:
+ subject = _('$mlist.real_name subscription notification')
+ msg = Message.UserNotification(
+ mlist.owner, mlist.no_reply_address, subject,
+ outfp.getvalue(), mlist.preferred_language)
+ msg.send(mlist)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/arch.py b/src/mailman/bin/arch.py
new file mode 100644
index 000000000..a27fa8d7f
--- /dev/null
+++ b/src/mailman/bin/arch.py
@@ -0,0 +1,151 @@
+# Copyright (C) 1998-2009 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 errno
+import shutil
+import optparse
+
+from locknix.lockfile import Lock
+
+from mailman import i18n
+from mailman.Archiver.HyperArch import HyperArchive
+from mailman.Defaults import hours
+from mailman.configuration import config
+from mailman.initialize import initialize
+from mailman.version import MAILMAN_VERSION
+
+_ = i18n._
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%%prog [options] listname [mbox]
+
+Rebuild a list's archive.
+
+Use this command to rebuild the archives for a mailing list. You may want to
+do this if you edit some messages in an archive, or remove some messages from
+an archive.
+
+Where 'mbox' is the path to a list's complete mbox archive. Usually this will
+be some path in the archives/private directory. For example:
+
+% bin/arch mylist archives/private/mylist.mbox/mylist.mbox
+
+'mbox' is optional. If it is missing, it is calculated from the listname.
+"""))
+ parser.add_option('-q', '--quiet',
+ dest='verbose', default=True, action='store_false',
+ help=_('Make the archiver output less verbose'))
+ parser.add_option('--wipe',
+ default=False, action='store_true',
+ help=_("""\
+First wipe out the original archive before regenerating. You usually want to
+specify this argument unless you're generating the archive in chunks."""))
+ parser.add_option('-s', '--start',
+ default=None, type='int', metavar='N',
+ help=_("""\
+Start indexing at article N, where article 0 is the first in the mbox.
+Defaults to 0."""))
+ parser.add_option('-e', '--end',
+ default=None, type='int', metavar='M',
+ help=_("""\
+End indexing at article M. This script is not very efficient with respect to
+memory management, and for large archives, it may not be possible to index the
+mbox entirely. For that reason, you can specify the start and end article
+numbers."""))
+ parser.add_option('-C', '--config',
+ help=_('Alternative configuration file to use'))
+ opts, args = parser.parse_args()
+ if len(args) < 1:
+ parser.print_help()
+ print >> sys.stderr, _('listname is required')
+ sys.exit(1)
+ if len(args) > 2:
+ parser.print_help()
+ print >> sys.stderr, _('Unexpected arguments')
+ sys.exit(1)
+ return parser, opts, args
+
+
+
+def main():
+ parser, opts, args = parseargs()
+ initialize(opts.config)
+
+ i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
+
+ listname = args[0].lower().strip()
+ if len(args) < 2:
+ mbox = None
+ else:
+ mbox = args[1]
+
+ # Open the mailing list object
+ mlist = config.list_manager.get(listname)
+ if mlist is None:
+ parser.error(_('No such list: $listname'))
+ if mbox is None:
+ mbox = mlist.ArchiveFileName()
+
+ i18n.set_language(mlist.preferred_language)
+ # Lay claim to the archive's lock file. This is so no other post can
+ # mess up the archive while we're processing it. Try to pick a
+ # suitably long period of time for the lock lifetime even though we
+ # really don't know how long it will take.
+ #
+ # XXX processUnixMailbox() should refresh the lock.
+ lock_path = os.path.join(mlist.data_path, '.archiver.lck')
+ with Lock(lock_path, lifetime=int(hours(3))):
+ # Try to open mbox before wiping old archive.
+ try:
+ fp = open(mbox)
+ except IOError, e:
+ if e.errno == errno.ENOENT:
+ print >> sys.stderr, _('Cannot open mbox file: $mbox')
+ else:
+ print >> sys.stderr, e
+ sys.exit(1)
+ # Maybe wipe the old archives
+ if opts.wipe:
+ if mlist.scrub_nondigest:
+ # TK: save the attachments dir because they are not in mbox
+ saved = False
+ atchdir = os.path.join(mlist.archive_dir(), 'attachments')
+ savedir = os.path.join(mlist.archive_dir() + '.mbox',
+ 'attachments')
+ try:
+ os.rename(atchdir, savedir)
+ saved = True
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ shutil.rmtree(mlist.archive_dir())
+ if mlist.scrub_nondigest and saved:
+ os.renames(savedir, atchdir)
+
+ archiver = HyperArchive(mlist)
+ archiver.VERBOSE = opts.verbose
+ try:
+ archiver.processUnixMailbox(fp, opts.start, opts.end)
+ finally:
+ archiver.close()
+ fp.close()
diff --git a/src/mailman/bin/bumpdigests.py b/src/mailman/bin/bumpdigests.py
new file mode 100644
index 000000000..b1ed37a21
--- /dev/null
+++ b/src/mailman/bin/bumpdigests.py
@@ -0,0 +1,74 @@
+# Copyright (C) 1998-2009 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.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, e:
+ parser.print_help()
+ print >> sys.stderr, _('No such list: $listname')
+ sys.exit(1)
+ try:
+ mlist.bump_digest_volume()
+ finally:
+ mlist.Save()
+ mlist.Unlock()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/check_perms.py b/src/mailman/bin/check_perms.py
new file mode 100644
index 000000000..4b75aa9f6
--- /dev/null
+++ b/src/mailman/bin/check_perms.py
@@ -0,0 +1,408 @@
+# Copyright (C) 1998-2009 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 pwd
+import grp
+import errno
+import optparse
+
+from stat import *
+
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.version import MAILMAN_VERSION
+
+
+# XXX Need to check the archives/private/*/database/* files
+
+
+
+class State:
+ FIX = False
+ VERBOSE = False
+ ERRORS = 0
+
+STATE = State()
+
+DIRPERMS = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH
+QFILEPERMS = S_ISGID | S_IRWXU | S_IRWXG
+PYFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
+ARTICLEFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
+MBOXPERMS = S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR
+PRIVATEPERMS = QFILEPERMS
+
+
+
+def statmode(path):
+ return os.stat(path).st_mode
+
+
+def statgidmode(path):
+ stat = os.stat(path)
+ return stat.st_mode, stat.st_gid
+
+
+seen = {}
+
+# libc's getgrgid re-opens /etc/group each time :(
+_gidcache = {}
+
+def getgrgid(gid):
+ data = _gidcache.get(gid)
+ if data is None:
+ data = grp.getgrgid(gid)
+ _gidcache[gid] = data
+ return data
+
+
+
+def checkwalk(arg, dirname, names):
+ # Short-circuit duplicates
+ if seen.has_key(dirname):
+ return
+ seen[dirname] = True
+ for name in names:
+ path = os.path.join(dirname, name)
+ if arg.VERBOSE:
+ print _(' checking gid and mode for $path')
+ try:
+ mode, gid = statgidmode(path)
+ except OSError, e:
+ if e.errno <> errno.ENOENT: raise
+ continue
+ if gid <> MAILMAN_GID:
+ try:
+ groupname = getgrgid(gid)[0]
+ except KeyError:
+ groupname = '<anon gid %d>' % gid
+ arg.ERRORS += 1
+ print _(
+ '$path bad group (has: $groupname, expected $MAILMAN_GROUP)'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chown(path, -1, MAILMAN_GID)
+ else:
+ print
+ # Most directories must be at least rwxrwsr-x.
+ # The private archive directory and database directory must be at
+ # least rwxrws---. Their 'other' permissions are checked in
+ # checkarchives() and checkarchivedbs() below. Their 'user' and
+ # 'group' permissions are checked here.
+ # The directories under qfiles should be rwxrws---. Their 'user' and
+ # 'group' permissions are checked here. Their 'other' permissions
+ # aren't checked.
+ private = config.PRIVATE_ARCHIVE_FILE_DIR
+ if path == private or (
+ os.path.commonprefix((path, private)) == private
+ and os.path.split(path)[1] == 'database'):
+ # then...
+ targetperms = PRIVATEPERMS
+ elif (os.path.commonprefix((path, config.QUEUE_DIR))
+ == config.QUEUE_DIR):
+ targetperms = QFILEPERMS
+ else:
+ targetperms = DIRPERMS
+ octperms = oct(targetperms)
+ if S_ISDIR(mode) and (mode & targetperms) <> targetperms:
+ arg.ERRORS += 1
+ print _('directory permissions must be $octperms: $path'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | targetperms)
+ else:
+ print
+ elif os.path.splitext(path)[1] in ('.py', '.pyc', '.pyo'):
+ octperms = oct(PYFILEPERMS)
+ if mode & PYFILEPERMS <> PYFILEPERMS:
+ print _('source perms must be $octperms: $path'),
+ arg.ERRORS += 1
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | PYFILEPERMS)
+ else:
+ print
+ elif path.endswith('-article'):
+ # Article files must be group writeable
+ octperms = oct(ARTICLEFILEPERMS)
+ if mode & ARTICLEFILEPERMS <> ARTICLEFILEPERMS:
+ print _('article db files must be $octperms: $path'),
+ arg.ERRORS += 1
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | ARTICLEFILEPERMS)
+ else:
+ print
+
+
+
+def checkall():
+ # first check PREFIX
+ if STATE.VERBOSE:
+ prefix = config.PREFIX
+ print _('checking mode for $prefix')
+ dirs = {}
+ for d in (config.PREFIX, config.EXEC_PREFIX, config.VAR_PREFIX,
+ config.LOG_DIR):
+ dirs[d] = True
+ for d in dirs.keys():
+ try:
+ mode = statmode(d)
+ except OSError, e:
+ if e.errno <> errno.ENOENT: raise
+ print _('WARNING: directory does not exist: $d')
+ continue
+ if (mode & DIRPERMS) <> DIRPERMS:
+ STATE.ERRORS += 1
+ print _('directory must be at least 02775: $d'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(d, mode | DIRPERMS)
+ else:
+ print
+ # check all subdirs
+ os.path.walk(d, checkwalk, STATE)
+
+
+
+def checkarchives():
+ private = config.PRIVATE_ARCHIVE_FILE_DIR
+ if STATE.VERBOSE:
+ print _('checking perms on $private')
+ # private archives must not be other readable
+ mode = statmode(private)
+ if mode & S_IROTH:
+ STATE.ERRORS += 1
+ print _('$private must not be other-readable'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(private, mode & ~S_IROTH)
+ else:
+ print
+ # In addition, on a multiuser system you may want to hide the private
+ # archives so other users can't read them.
+ if mode & S_IXOTH:
+ print _("""\
+Warning: Private archive directory is other-executable (o+x).
+ This could allow other users on your system to read private archives.
+ If you're on a shared multiuser system, you should consult the
+ installation manual on how to fix this.""")
+
+
+
+def checkmboxfile(mboxdir):
+ absdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, mboxdir)
+ for f in os.listdir(absdir):
+ if not f.endswith('.mbox'):
+ continue
+ mboxfile = os.path.join(absdir, f)
+ mode = statmode(mboxfile)
+ if (mode & MBOXPERMS) <> MBOXPERMS:
+ STATE.ERRORS = STATE.ERRORS + 1
+ print _('mbox file must be at least 0660:'), mboxfile
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(mboxfile, mode | MBOXPERMS)
+ else:
+ print
+
+
+
+def checkarchivedbs():
+ # The archives/private/listname/database file must not be other readable
+ # or executable otherwise those files will be accessible when the archives
+ # are public. That may not be a horrible breach, but let's close this off
+ # anyway.
+ for dir in os.listdir(config.PRIVATE_ARCHIVE_FILE_DIR):
+ if dir.endswith('.mbox'):
+ checkmboxfile(dir)
+ dbdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, dir, 'database')
+ try:
+ mode = statmode(dbdir)
+ except OSError, e:
+ if e.errno not in (errno.ENOENT, errno.ENOTDIR): raise
+ continue
+ if mode & S_IRWXO:
+ STATE.ERRORS += 1
+ print _('$dbdir "other" perms must be 000'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(dbdir, mode & ~S_IRWXO)
+ else:
+ print
+
+
+
+def checkcgi():
+ cgidir = os.path.join(config.EXEC_PREFIX, 'cgi-bin')
+ if STATE.VERBOSE:
+ print _('checking cgi-bin permissions')
+ exes = os.listdir(cgidir)
+ for f in exes:
+ path = os.path.join(cgidir, f)
+ if STATE.VERBOSE:
+ print _(' checking set-gid for $path')
+ mode = statmode(path)
+ if mode & S_IXGRP and not mode & S_ISGID:
+ STATE.ERRORS += 1
+ print _('$path must be set-gid'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | S_ISGID)
+ else:
+ print
+
+
+
+def checkmail():
+ wrapper = os.path.join(config.WRAPPER_DIR, 'mailman')
+ if STATE.VERBOSE:
+ print _('checking set-gid for $wrapper')
+ mode = statmode(wrapper)
+ if not mode & S_ISGID:
+ STATE.ERRORS += 1
+ print _('$wrapper must be set-gid'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(wrapper, mode | S_ISGID)
+
+
+
+def checkadminpw():
+ for pwfile in (os.path.join(config.DATA_DIR, 'adm.pw'),
+ os.path.join(config.DATA_DIR, 'creator.pw')):
+ targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP
+ if STATE.VERBOSE:
+ print _('checking permissions on $pwfile')
+ try:
+ mode = statmode(pwfile)
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ return
+ if mode <> targetmode:
+ STATE.ERRORS += 1
+ octmode = oct(mode)
+ print _('$pwfile permissions must be exactly 0640 (got $octmode)'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(pwfile, targetmode)
+ else:
+ print
+
+
+def checkmta():
+ if config.MTA:
+ modname = 'mailman.MTA.' + config.MTA
+ __import__(modname)
+ try:
+ sys.modules[modname].checkperms(STATE)
+ except AttributeError:
+ pass
+
+
+
+def checkdata():
+ targetmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
+ checkfiles = ('config.pck', 'config.pck.last',
+ 'config.db', 'config.db.last',
+ 'next-digest', 'next-digest-topics',
+ 'digest.mbox', 'pending.pck',
+ 'request.db', 'request.db.tmp')
+ if STATE.VERBOSE:
+ print _('checking permissions on list data')
+ for dir in os.listdir(config.LIST_DATA_DIR):
+ for file in checkfiles:
+ path = os.path.join(config.LIST_DATA_DIR, dir, file)
+ if STATE.VERBOSE:
+ print _(' checking permissions on: $path')
+ try:
+ mode = statmode(path)
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ continue
+ if (mode & targetmode) <> targetmode:
+ STATE.ERRORS += 1
+ print _('file permissions must be at least 660: $path'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | targetmode)
+ else:
+ print
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%prog [options]
+
+Check the permissions of all Mailman files. With no options, just report the
+permission and ownership problems found."""))
+ parser.add_option('-f', '--fix',
+ default=False, action='store_true', help=_("""\
+Fix all permission and ownership problems found. With this option, you must
+run check_perms as root."""))
+ parser.add_option('-v', '--verbose',
+ default=False, action='store_true',
+ help=_('Produce more verbose output'))
+ 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 parser, opts, args
+
+
+
+def main():
+ global MAILMAN_USER, MAILMAN_GROUP, MAILMAN_UID, MAILMAN_GID
+
+ parser, opts, args = parseargs()
+ STATE.FIX = opts.fix
+ STATE.VERBOSE = opts.verbose
+
+ config.load(opts.config)
+
+ MAILMAN_USER = config.MAILMAN_USER
+ MAILMAN_GROUP = config.MAILMAN_GROUP
+ # Let KeyErrors percolate
+ MAILMAN_GID = grp.getgrnam(MAILMAN_GROUP).gr_gid
+ MAILMAN_UID = pwd.getpwnam(MAILMAN_USER).pw_uid
+
+ checkall()
+ checkarchives()
+ checkarchivedbs()
+ checkcgi()
+ checkmail()
+ checkdata()
+ checkadminpw()
+ checkmta()
+
+ if not STATE.ERRORS:
+ print _('No problems found')
+ else:
+ print _('Problems found:'), STATE.ERRORS
+ print _('Re-run as $MAILMAN_USER (or root) with -f flag to fix')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/checkdbs.py b/src/mailman/bin/checkdbs.py
new file mode 100644
index 000000000..2ce08aab7
--- /dev/null
+++ b/src/mailman/bin/checkdbs.py
@@ -0,0 +1,199 @@
+# Copyright (C) 1998-2009 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 Message
+from mailman import Utils
+from mailman import i18n
+from mailman.app.requests import handle_request
+from mailman.configuration import config
+from mailman.version import MAILMAN_VERSION
+
+_ = i18n._
+
+# 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 >> sys.stderr, _('Unexpected arguments')
+ sys.exit(1)
+ return opts, args, parser
+
+
+
+def pending_requests(mlist):
+ # Must return a byte string
+ lcset = Utils.GetCharSet(mlist.preferred_language)
+ pending = []
+ first = True
+ requestsdb = config.db.get_list_requests(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 = Utils.GetCharSet(mlist.preferred_language)
+ 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(Utils.GetCharSet(mlist.preferred_language))
+ 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 = config.db.get_list_requests(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
+
+
+
+def main():
+ opts, args, parser = parseargs()
+ config.load(opts.config)
+
+ i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
+
+ 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 = config.db.requests.get_list_requests(mlist).count
+ # While we're at it, let's evict yesterday's autoresponse data
+ midnight_today = Utils.midnight()
+ evictions = []
+ for sender in mlist.hold_and_cmd_autoresponses.keys():
+ date, respcount = mlist.hold_and_cmd_autoresponses[sender]
+ if Utils.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:
+ i18n.set_language(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,
+ 'host_name': mlist.host_name,
+ '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 = Message.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/src/mailman/bin/cleanarch.py b/src/mailman/bin/cleanarch.py
new file mode 100644
index 000000000..325fad91a
--- /dev/null
+++ b/src/mailman/bin/cleanarch.py
@@ -0,0 +1,133 @@
+# Copyright (C) 2001-2009 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/>.
+
+"""Clean up an .mbox archive file."""
+
+import re
+import sys
+import mailbox
+import optparse
+
+from mailman.i18n import _
+from mailman.version import MAILMAN_VERSION
+
+
+cre = re.compile(mailbox.UnixMailbox._fromlinepattern)
+# From RFC 2822, a header field name must contain only characters from 33-126
+# inclusive, excluding colon. I.e. from oct 41 to oct 176 less oct 072. Must
+# use re.match() so that it's anchored at the beginning of the line.
+fre = re.compile(r'[\041-\071\073-\176]+')
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%prog [options] < inputfile > outputfile
+
+The archiver looks for Unix-From lines separating messages in an mbox archive
+file. For compatibility, it specifically looks for lines that start with
+'From ' -- i.e. the letters capital-F, lowercase-r, o, m, space, ignoring
+everything else on the line.
+
+Normally, any lines that start 'From ' in the body of a message should be
+escaped such that a > character is actually the first on a line. It is
+possible though that body lines are not actually escaped. This script
+attempts to fix these by doing a stricter test of the Unix-From lines. Any
+lines that start From ' but do not pass this stricter test are escaped with a
+'>' character."""))
+ parser.add_option('-q', '--quiet',
+ default=False, action='store_true', help=_("""\
+Don't print changed line information to standard error."""))
+ parser.add_option('-s', '--status',
+ default=-1, type='int', help=_("""\
+Print a '#' character for every n lines processed. With a number less than or
+equal to zero, suppress the '#' characters."""))
+ parser.add_option('-n', '--dry-run',
+ default=False, action='store_true', help=_("""\
+Don't actually output anything."""))
+ opts, args = parser.parser_args()
+ if args:
+ parser.print_error(_('Unexpected arguments'))
+ return parser, opts, args
+
+
+
+def escape_line(line, lineno, quiet, output):
+ if output:
+ sys.stdout.write('>' + line)
+ if not quiet:
+ print >> sys.stderr, _('Unix-From line changed: $lineno')
+ print >> sys.stderr, line[:-1]
+
+
+
+def main():
+ parser, opts, args = parseargs()
+
+ lineno = 0
+ statuscnt = 0
+ messages = 0
+ prevline = None
+ while True:
+ lineno += 1
+ line = sys.stdin.readline()
+ if not line:
+ break
+ if line.startswith('From '):
+ if cre.match(line):
+ # This is a real Unix-From line. But it could be a message
+ # /about/ Unix-From lines, so as a second order test, make
+ # sure there's at least one RFC 2822 header following
+ nextline = sys.stdin.readline()
+ lineno += 1
+ if not nextline:
+ # It was the last line of the mbox, so it couldn't have
+ # been a Unix-From
+ escape_line(line, lineno, quiet, output)
+ break
+ fieldname = nextline.split(':', 1)
+ if len(fieldname) < 2 or not fre.match(nextline):
+ # The following line was not a header, so this wasn't a
+ # valid Unix-From
+ escape_line(line, lineno, quiet, output)
+ if output:
+ sys.stdout.write(nextline)
+ else:
+ # It's a valid Unix-From line
+ messages += 1
+ if output:
+ # Before we spit out the From_ line, make sure the
+ # previous line was blank.
+ if prevline is not None and prevline <> '\n':
+ sys.stdout.write('\n')
+ sys.stdout.write(line)
+ sys.stdout.write(nextline)
+ else:
+ # This is a bogus Unix-From line
+ escape_line(line, lineno, quiet, output)
+ elif output:
+ # Any old line
+ sys.stdout.write(line)
+ if status > 0 and (lineno % status) == 0:
+ sys.stderr.write('#')
+ statuscnt += 1
+ if statuscnt > 50:
+ print >> sys.stderr
+ statuscnt = 0
+ prevline = line
+ print >> sys.stderr, _('%(messages)d messages found')
diff --git a/src/mailman/bin/config_list.py b/src/mailman/bin/config_list.py
new file mode 100644
index 000000000..a5cec9480
--- /dev/null
+++ b/src/mailman/bin/config_list.py
@@ -0,0 +1,332 @@
+# Copyright (C) 1998-2009 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 errors
+from mailman import MailList
+from mailman import Utils
+from mailman import i18n
+from mailman.configuration import config
+from mailman.version import MAILMAN_VERSION
+
+_ = i18n._
+
+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.
+ language = mlist.preferred_language
+ charset = Utils.GetCharSet(language)
+ i18n.set_language(language)
+ 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 = Utils.GetCharSet(mlist.preferred_language)
+ 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 = Utils.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 &lt;/&gt; to <>
+ desc = re.sub('&lt;', '<', desc)
+ desc = re.sub('&gt;', '>', desc)
+ # Print out the variable description.
+ desc = Utils.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, e:
+ parser.error(_('No such list "$listname"\n$e'))
+ 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()
+ config.load(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/src/mailman/bin/create_list.py b/src/mailman/bin/create_list.py
new file mode 100644
index 000000000..8058a7d67
--- /dev/null
+++ b/src/mailman/bin/create_list.py
@@ -0,0 +1,129 @@
+# Copyright (C) 1998-2009 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
+
+from mailman import Message
+from mailman import Utils
+from mailman import i18n
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.core import errors
+from mailman.interfaces.listmanager import ListAlreadyExistsError
+from mailman.options import SingleMailingListOptions
+
+
+_ = i18n._
+
+
+
+class ScriptOptions(SingleMailingListOptions):
+ usage = _("""\
+%prog [options]
+
+Create a new mailing list.
+
+fqdn_listname is the 'fully qualified list name', basically the posting
+address of the list. It must be a valid email address and the domain must be
+registered with Mailman.
+
+Note that listnames are forced to lowercase.""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '--language',
+ type='unicode', action='store',
+ help=_("""\
+Make the list's preferred language LANGUAGE, which must be a two letter
+language code."""))
+ self.parser.add_option(
+ '-o', '--owner',
+ type='unicode', action='append', default=[],
+ dest='owners', help=_("""\
+Specific a listowner email address. If the address is not currently
+registered with Mailman, the address is registered and linked to a user.
+Mailman will send a confirmation message to the address, but it will also send
+a list creation notice to the address. More than one owner can be
+specified."""))
+ self.parser.add_option(
+ '-q', '--quiet',
+ default=False, action='store_true',
+ help=_("""\
+Normally the administrator is notified by email (after a prompt) that their
+list has been created. This option suppresses the prompt and
+notification."""))
+ self.parser.add_option(
+ '-a', '--automate',
+ default=False, action='store_true',
+ help=_("""\
+This option suppresses the prompt prior to administrator notification but
+still sends the notification. It can be used to make newlist totally
+non-interactive but still send the notification, assuming at least one list
+owner is specified with the -o option.."""))
+
+ def sanity_check(self):
+ """Set up some defaults we couldn't set up earlier."""
+ if self.options.language is None:
+ self.options.language = unicode(config.mailman.default_language)
+ # Is the language known?
+ if self.options.language not in config.languages.enabled_codes:
+ self.parser.error(_('Unknown language: $opts.language'))
+ # Handle variable number of positional arguments
+ if len(self.arguments) > 0:
+ parser.error(_('Unexpected arguments'))
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ # Create the mailing list, applying styles as appropriate.
+ fqdn_listname = options.options.listname
+ if fqdn_listname is None:
+ options.parser.error(_('--listname is required'))
+ try:
+ mlist = create_list(fqdn_listname, options.options.owners)
+ mlist.preferred_language = options.options.language
+ except errors.InvalidEmailAddress:
+ options.parser.error(_('Illegal list name: $fqdn_listname'))
+ except ListAlreadyExistsError:
+ options.parser.error(_('List already exists: $fqdn_listname'))
+ except errors.BadDomainSpecificationError, domain:
+ options.parser.error(_('Undefined domain: $domain'))
+
+ config.db.commit()
+
+ if not options.options.quiet:
+ d = dict(
+ listname = mlist.fqdn_listname,
+ admin_url = mlist.script_url('admin'),
+ listinfo_url = mlist.script_url('listinfo'),
+ requestaddr = mlist.request_address,
+ siteowner = mlist.no_reply_address,
+ )
+ text = Utils.maketext('newlist.txt', d, mlist=mlist)
+ # Set the I18N language to the list's preferred language so the header
+ # will match the template language. Stashing and restoring the old
+ # translation context is just (healthy? :) paranoia.
+ with i18n.using_language(mlist.preferred_language):
+ msg = Message.UserNotification(
+ owner_mail, mlist.no_reply_address,
+ _('Your new mailing list: $fqdn_listname'),
+ text, mlist.preferred_language)
+ msg.send(mlist)
diff --git a/src/mailman/bin/disabled.py b/src/mailman/bin/disabled.py
new file mode 100644
index 000000000..cc8eb2c69
--- /dev/null
+++ b/src/mailman/bin/disabled.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2001-2009 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 errors
+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.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)
+
+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)
+
+
+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 errors.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(
+ 'NotAMemberError when sending disabled notice: %s',
+ member)
+ mlist.ApprovedDeleteMember(member, 'cron/disabled')
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/docs/master.txt b/src/mailman/bin/docs/master.txt
new file mode 100644
index 000000000..0d3cade77
--- /dev/null
+++ b/src/mailman/bin/docs/master.txt
@@ -0,0 +1,49 @@
+Mailman queue runner control
+============================
+
+Mailman has a number of queue runners which process messages in its queue file
+directories. In normal operation, a command line script called 'mailmanctl'
+is used to start, stop and manage the queue runners. mailmanctl actually is
+just a wrapper around the real queue runner watcher script called master.py.
+
+ >>> from mailman.testing.helpers import TestableMaster
+
+Start the master in a subthread.
+
+ >>> master = TestableMaster()
+ >>> master.start()
+
+There should be a process id for every qrunner that claims to be startable.
+
+ >>> from lazr.config import as_boolean
+ >>> startable_qrunners = [qconf for qconf in config.qrunner_configs
+ ... if as_boolean(qconf.start)]
+ >>> len(list(master.qrunner_pids)) == len(startable_qrunners)
+ True
+
+Now verify that all the qrunners are running.
+
+ >>> import os
+
+ # This should produce no output.
+ >>> for pid in master.qrunner_pids:
+ ... os.kill(pid, 0)
+
+Stop the master process, which should also kill (and not restart) the child
+queue runner processes.
+
+ >>> master.stop()
+
+None of the children are running now.
+
+ >>> import errno
+ >>> for pid in master.qrunner_pids:
+ ... try:
+ ... os.kill(pid, 0)
+ ... print 'Process did not exit:', pid
+ ... except OSError, error:
+ ... if error.errno == errno.ESRCH:
+ ... # The child process exited.
+ ... pass
+ ... else:
+ ... raise
diff --git a/src/mailman/bin/dumpdb.py b/src/mailman/bin/dumpdb.py
new file mode 100644
index 000000000..6657602e4
--- /dev/null
+++ b/src/mailman/bin/dumpdb.py
@@ -0,0 +1,88 @@
+# Copyright (C) 1998-2009 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 pprint
+import cPickle
+
+from mailman.config import config
+from mailman.i18n import _
+from mailman.interact import interact
+from mailman.options import Options
+
+
+COMMASPACE = ', '
+m = []
+
+
+
+class ScriptOptions(Options):
+ usage=_("""\
+%prog [options] filename
+
+Dump the contents of any Mailman queue file. The queue file is a data file
+with multiple Python pickles in it.""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-n', '--noprint',
+ dest='doprint', default=True, action='store_false',
+ help=_("""\
+Don't attempt to pretty print the object. This is useful if there is some
+problem with the object and you just want to get an unpickled representation.
+Useful with 'bin/dumpdb -i <file>'. In that case, the list of
+unpickled objects will be left in a variable called 'm'."""))
+ self.parser.add_option(
+ '-i', '--interact',
+ default=False, action='store_true',
+ help=_("""\
+Start an interactive Python session, with a variable called 'm' containing the
+list of unpickled objects."""))
+
+ def sanity_check(self):
+ if len(self.arguments) < 1:
+ self.parser.error(_('No filename given.'))
+ elif len(self.arguments) > 1:
+ self.parser.error(_('Unexpected arguments'))
+ else:
+ self.filename = self.arguments[0]
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ pp = pprint.PrettyPrinter(indent=4)
+ with open(options.filename) as fp:
+ while True:
+ try:
+ m.append(cPickle.load(fp))
+ except EOFError:
+ break
+ if options.options.doprint:
+ print _('[----- start pickle -----]')
+ for i, obj in enumerate(m):
+ count = i + 1
+ print _('<----- start object $count ----->')
+ if isinstance(obj, basestring):
+ print obj
+ else:
+ pp.pprint(obj)
+ print _('[----- end pickle -----]')
+ if options.options.interact:
+ interact()
diff --git a/src/mailman/bin/export.py b/src/mailman/bin/export.py
new file mode 100644
index 000000000..d1992b4b4
--- /dev/null
+++ b/src/mailman/bin/export.py
@@ -0,0 +1,310 @@
+# Copyright (C) 2006-2009 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.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(unicode(_value))
+ 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/src/mailman/bin/find_member.py b/src/mailman/bin/find_member.py
new file mode 100644
index 000000000..0982724a0
--- /dev/null
+++ b/src/mailman/bin/find_member.py
@@ -0,0 +1,135 @@
+# Copyright (C) 1998-2009 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.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/src/mailman/bin/gate_news.py b/src/mailman/bin/gate_news.py
new file mode 100644
index 000000000..eac30422d
--- /dev/null
+++ b/src/mailman/bin/gate_news.py
@@ -0,0 +1,243 @@
+# Copyright (C) 1998-2009 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 locknix import lockfile
+
+from mailman import MailList
+from mailman import Message
+from mailman import Utils
+from mailman import loginit
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.queue 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 = Utils.nntpsplit(mlist.nntp_host)
+ # 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), 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
+ 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, 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,
+ listname=mlist.internal_name(),
+ fromusenet=True)
+ log.info('posted to list %s: %7d', listname, num)
+ except nntplib.NNTPError, 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 lockfile.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 lockfile.Lock(GATENEWS_LOCK_FILE,
+ # It's okay to hijack this
+ lifetime=LOCK_LIFETIME) as lock:
+ process_lists(lock)
+ clearcache()
+ except lockfile.TimeOutError:
+ log.error('Could not acquire gate_news lock')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/genaliases.py b/src/mailman/bin/genaliases.py
new file mode 100644
index 000000000..e8916d030
--- /dev/null
+++ b/src/mailman/bin/genaliases.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2001-2009 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/>.
+
+__metaclass__ = type
+__all__ = [
+ 'main',
+ ]
+
+
+import sys
+
+from mailman.config import config
+from mailman.i18n import _
+from mailman.options import Options
+
+
+
+class ScriptOptions(Options):
+ """Options for the genaliases script."""
+
+ usage = _("""\
+%prog [options]
+
+Regenerate the Mailman specific MTA aliases from scratch. The actual output
+depends on the value of the 'MTA' variable in your etc/mailman.cfg file.""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-q', '--quiet',
+ default=False, action='store_true', help=_("""\
+Some MTA output can include more verbose help text. Use this to tone down the
+verbosity."""))
+
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ # Get the MTA-specific module.
+ module_path, class_path = config.mta.incoming.rsplit('.', 1)
+ __import__(module_path)
+ getattr(sys.modules[module_path], class_path)().regenerate()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/import.py b/src/mailman/bin/import.py
new file mode 100644
index 000000000..d2361e808
--- /dev/null
+++ b/src/mailman/bin/import.py
@@ -0,0 +1,315 @@
+# Copyright (C) 2006-2009 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 the XML representation of a mailing list."""
+
+import sys
+import codecs
+import optparse
+import traceback
+
+from xml.dom import minidom
+from xml.parsers.expat import ExpatError
+
+from mailman import Defaults
+from mailman import errors
+from mailman import MemberAdaptor
+from mailman import Utils
+from mailman import passwords
+from mailman.MailList import MailList
+from mailman.i18n import _
+from mailman.initialize import initialize
+from mailman.version import MAILMAN_VERSION
+
+
+OPTS = None
+
+
+
+def nodetext(node):
+ # Expect only one TEXT_NODE in the list of children
+ for child in node.childNodes:
+ if child.nodeType == node.TEXT_NODE:
+ return child.data
+ return u''
+
+
+def nodegen(node, *elements):
+ for child in node.childNodes:
+ if child.nodeType <> minidom.Node.ELEMENT_NODE:
+ continue
+ if elements and child.tagName not in elements:
+ print _('Ignoring unexpected element: $node.tagName')
+ else:
+ yield child
+
+
+
+def parse_config(node):
+ config = dict()
+ for child in nodegen(node, 'option'):
+ name = child.getAttribute('name')
+ if not name:
+ print _('Skipping unnamed option')
+ continue
+ vtype = child.getAttribute('type') or 'string'
+ if vtype in ('email_list', 'email_list_ex', 'checkbox'):
+ value = []
+ for subnode in nodegen(child):
+ value.append(nodetext(subnode))
+ elif vtype == 'bool':
+ value = nodetext(child)
+ try:
+ value = bool(int(value))
+ except ValueError:
+ value = {'true' : True,
+ 'false': False,
+ }.get(value.lower())
+ if value is None:
+ print _('Skipping bad boolean value: $value')
+ continue
+ elif vtype == 'radio':
+ value = nodetext(child).lower()
+ boolval = {'true' : True,
+ 'false': False,
+ }.get(value)
+ if boolval is None:
+ value = int(value)
+ else:
+ value = boolval
+ elif vtype == 'number':
+ value = nodetext(child)
+ # First try int then float
+ try:
+ value = int(value)
+ except ValueError:
+ value = float(value)
+ elif vtype in ('header_filter', 'topics'):
+ value = []
+ fakebltins = dict(__builtins__ = dict(True=True, False=False))
+ for subnode in nodegen(child):
+ reprstr = nodetext(subnode)
+ # Turn the reprs back into tuples, in a safe way
+ tupleval = eval(reprstr, fakebltins)
+ value.append(tupleval)
+ else:
+ value = nodetext(child)
+ # And now some special casing :(
+ if name == 'new_member_options':
+ value = int(nodetext(child))
+ config[name] = value
+ return config
+
+
+
+
+def parse_roster(node):
+ members = []
+ for child in nodegen(node, 'member'):
+ member = dict()
+ member['id'] = mid = child.getAttribute('id')
+ if not mid:
+ print _('Skipping member with no id')
+ continue
+ if OPTS.verbose:
+ print _('* Processing member: $mid')
+ for subnode in nodegen(child):
+ attr = subnode.tagName
+ if attr == 'delivery':
+ value = (subnode.getAttribute('status'),
+ subnode.getAttribute('delivery'))
+ elif attr in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'):
+ value = {'true' : True,
+ 'false': False,
+ }.get(nodetext(subnode).lower(), False)
+ elif attr == 'topics':
+ value = []
+ for subsubnode in nodegen(subnode):
+ value.append(nodetext(subsubnode))
+ elif attr == 'password':
+ value = nodetext(subnode)
+ if OPTS.reset_passwords or value == '{NONE}' or not value:
+ value = passwords.make_secret(Utils.MakeRandomPassword())
+ else:
+ value = nodetext(subnode)
+ member[attr] = value
+ members.append(member)
+ return members
+
+
+
+def load(fp):
+ try:
+ doc = minidom.parse(fp)
+ except ExpatError:
+ print _('Expat error in file: $fp.name')
+ traceback.print_exc()
+ sys.exit(1)
+ doc.normalize()
+ # Make sure there's only one top-level <mailman> node
+ gen = nodegen(doc, 'mailman')
+ top = gen.next()
+ try:
+ gen.next()
+ except StopIteration:
+ pass
+ else:
+ print _('Malformed XML; duplicate <mailman> nodes')
+ sys.exit(1)
+ all_listdata = []
+ for listnode in nodegen(top, 'list'):
+ listdata = dict()
+ name = listnode.getAttribute('name')
+ if OPTS.verbose:
+ print _('Processing list: $name')
+ if not name:
+ print _('Ignoring malformed <list> node')
+ continue
+ for child in nodegen(listnode, 'configuration', 'roster'):
+ if child.tagName == 'configuration':
+ list_config = parse_config(child)
+ else:
+ assert(child.tagName == 'roster')
+ list_roster = parse_roster(child)
+ all_listdata.append((name, list_config, list_roster))
+ return all_listdata
+
+
+
+def create(all_listdata):
+ for name, list_config, list_roster in all_listdata:
+ fqdn_listname = '%s@%s' % (name, list_config['host_name'])
+ if Utils.list_exists(fqdn_listname):
+ print _('Skipping already existing list: $fqdn_listname')
+ continue
+ mlist = MailList()
+ try:
+ if OPTS.verbose:
+ print _('Creating mailing list: $fqdn_listname')
+ mlist.Create(fqdn_listname, list_config['owner'][0],
+ list_config['password'])
+ except errors.BadDomainSpecificationError:
+ print _('List is not in a supported domain: $fqdn_listname')
+ continue
+ # Save the list creation, then unlock and relock the list. This is so
+ # that we use normal SQLAlchemy transactions to manage all the
+ # attribute and membership updates. Without this, no transaction will
+ # get committed in the second Save() below and we'll lose all our
+ # updates.
+ mlist.Save()
+ mlist.Unlock()
+ mlist.Lock()
+ try:
+ for option, value in list_config.items():
+ # XXX Here's what sucks. Some properties need to have
+ # _setValue() called on the gui component, because those
+ # methods do some pre-processing on the values before they're
+ # applied to the MailList instance. But we don't have a good
+ # way to find a category and sub-category that a particular
+ # property belongs to. Plus this will probably change. So
+ # for now, we'll just hard code the extra post-processing
+ # here. The good news is that not all _setValue() munging
+ # needs to be done -- for example, we've already converted
+ # everything to dollar strings.
+ if option in ('filter_mime_types', 'pass_mime_types',
+ 'filter_filename_extensions',
+ 'pass_filename_extensions'):
+ value = value.splitlines()
+ if option == 'available_languages':
+ mlist.set_languages(*value)
+ else:
+ setattr(mlist, option, value)
+ for member in list_roster:
+ mid = member['id']
+ if OPTS.verbose:
+ print _('* Adding member: $mid')
+ status, delivery = member['delivery']
+ kws = {'password' : member['password'],
+ 'language' : member['language'],
+ 'realname' : member['realname'],
+ 'digest' : delivery <> 'regular',
+ }
+ mlist.addNewMember(mid, **kws)
+ status = {'enabled' : MemberAdaptor.ENABLED,
+ 'byuser' : MemberAdaptor.BYUSER,
+ 'byadmin' : MemberAdaptor.BYADMIN,
+ 'bybounce' : MemberAdaptor.BYBOUNCE,
+ }.get(status, MemberAdaptor.UNKNOWN)
+ mlist.setDeliveryStatus(mid, status)
+ for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'nomail'):
+ mlist.setMemberOption(mid,
+ Defaults.OPTINFO[opt],
+ member[opt])
+ topics = member.get('topics')
+ if topics:
+ mlist.setMemberTopics(mid, topics)
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%prog [options]
+
+Import the configuration and/or members of a mailing list in XML format. The
+imported mailing list must not already exist. All mailing lists named in the
+XML file are imported, but those that already exist are skipped unless --error
+is given."""))
+ parser.add_option('-i', '--inputfile',
+ metavar='FILENAME', default=None, type='string',
+ help=_("""\
+Input XML from FILENAME. If not given, or if FILENAME is '-', standard input
+is used."""))
+ parser.add_option('-p', '--reset-passwords',
+ default=False, action='store_true', help=_("""\
+With this option, user passwords in the XML are ignored and are reset to a
+random password. If the generated passwords were not included in the input
+XML, they will always be randomly generated."""))
+ parser.add_option('-v', '--verbose',
+ default=False, action='store_true',
+ help=_('Produce more verbose output'))
+ 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():
+ global OPTS
+
+ parser, opts, args = parseargs()
+ initialize(opts.config)
+ OPTS = opts
+
+ if opts.inputfile in (None, '-'):
+ fp = sys.stdin
+ else:
+ fp = open(opts.inputfile, 'r')
+
+ try:
+ listbags = load(fp)
+ create(listbags)
+ finally:
+ if fp is not sys.stdin:
+ fp.close()
diff --git a/src/mailman/bin/inject.py b/src/mailman/bin/inject.py
new file mode 100644
index 000000000..2bc8a49e3
--- /dev/null
+++ b/src/mailman/bin/inject.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2002-2009 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
+
+from email import message_from_string
+
+from mailman import Utils
+from mailman.Message import Message
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.inject import inject_text
+from mailman.options import SingleMailingListOptions
+
+
+
+class ScriptOptions(SingleMailingListOptions):
+ usage=_("""\
+%prog [options] [filename]
+
+Inject a message from a file into Mailman's incoming queue. 'filename' is the
+name of the plaintext message file to inject. If omitted, or the string '-',
+standard input is used.
+""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-q', '--queue',
+ type='string', help=_("""\
+The name of the queue to inject the message to. The queuename must be one of
+the directories inside the qfiles directory. If omitted, the incoming queue
+is used."""))
+
+ def sanity_check(self):
+ if not self.options.listname:
+ self.parser.error(_('Missing listname'))
+ if len(self.arguments) == 0:
+ self.filename = '-'
+ elif len(self.arguments) > 1:
+ self.parser.print_error(_('Unexpected arguments'))
+ else:
+ self.filename = self.arguments[0]
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ if options.options.queue is None:
+ qdir = config.INQUEUE_DIR
+ else:
+ qdir = os.path.join(config.QUEUE_DIR, options.options.queue)
+ if not os.path.isdir(qdir):
+ options.parser.error(_('Bad queue directory: $qdir'))
+
+ fqdn_listname = options.options.listname
+ mlist = config.db.list_manager.get(fqdn_listname)
+ if mlist is None:
+ options.parser.error(_('No such list: $fqdn_listname'))
+
+ if options.filename == '-':
+ message_text = sys.stdin.read()
+ else:
+ with open(options.filename) as fp:
+ message_text = fp.read()
+
+ inject_text(mlist, message_text, qdir=qdir)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/list_lists.py b/src/mailman/bin/list_lists.py
new file mode 100644
index 000000000..ea1640910
--- /dev/null
+++ b/src/mailman/bin/list_lists.py
@@ -0,0 +1,104 @@
+# Copyright (C) 1998-2009 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/>.
+
+from mailman.config import config
+from mailman.i18n import _
+from mailman.options import Options
+
+
+
+class ScriptOptions(Options):
+ usage = _("""\
+%prog [options]
+
+List all mailing lists.""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-a', '--advertised',
+ default=False, action='store_true',
+ help=_("""\
+List only those mailing lists that are publicly advertised"""))
+ self.parser.add_option(
+ '-b', '--bare',
+ default=False, action='store_true',
+ help=_("""\
+Displays only the list name, with no description."""))
+ self.parser.add_option(
+ '-d', '--domain',
+ default=[], type='string', action='append',
+ dest='domains', help=_("""\
+List only those mailing lists that match the given virtual domain, which may
+be either the email host or the url host name. Multiple -d options may be
+given."""))
+ self.parser.add_option(
+ '-f', '--full',
+ default=False, action='store_true',
+ help=_("""\
+Print the full list name, including the posting address."""))
+
+ def sanity_check(self):
+ if len(self.arguments) > 0:
+ self.parser.error(_('Unexpected arguments'))
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ mlists = []
+ longest = 0
+
+ listmgr = config.db.list_manager
+ for fqdn_name in sorted(listmgr.names):
+ mlist = listmgr.get(fqdn_name)
+ if options.options.advertised and not mlist.advertised:
+ continue
+ if options.options.domains:
+ for domain in options.options.domains:
+ if domain in mlist.web_page_url or domain == mlist.host_name:
+ mlists.append(mlist)
+ break
+ else:
+ mlists.append(mlist)
+ if options.options.full:
+ name = mlist.fqdn_listname
+ else:
+ name = mlist.real_name
+ longest = max(len(name), longest)
+
+ if not mlists and not options.options.bare:
+ print _('No matching mailing lists found')
+ return
+
+ if not options.options.bare:
+ num_mlists = len(mlists)
+ print _('$num_mlists matching mailing lists found:')
+
+ format = '%%%ds - %%.%ds' % (longest, 77 - longest)
+ for mlist in mlists:
+ if options.options.full:
+ name = mlist.fqdn_listname
+ else:
+ name = mlist.real_name
+ if options.options.bare:
+ print name
+ else:
+ description = mlist.description or _('[no description available]')
+ print ' ', format % (name, description)
diff --git a/src/mailman/bin/list_members.py b/src/mailman/bin/list_members.py
new file mode 100644
index 000000000..443f764d6
--- /dev/null
+++ b/src/mailman/bin/list_members.py
@@ -0,0 +1,201 @@
+# Copyright (C) 1998-2009 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
+
+from email.Utils import formataddr
+
+from mailman import Utils
+from mailman.config import config
+from mailman.core import errors
+from mailman.i18n import _
+from mailman.interfaces import DeliveryStatus
+from mailman.options import SingleMailingListOptions
+
+
+COMMASPACE = ', '
+
+WHYCHOICES = {
+ 'enabled' : DeliveryStatus.enabled,
+ 'byuser' : DeliveryStatus.by_user,
+ 'byadmin' : DeliveryStatus.by_moderator,
+ 'bybounce': DeliveryStatus.by_bounces,
+ }
+
+KINDCHOICES = set(('mime', 'plain', 'any'))
+
+
+
+class ScriptOptions(SingleMailingListOptions):
+ usage = _("""\
+%prog [options]
+
+List all the members of a mailing list. Note that with the options below, if
+neither -r or -d is supplied, regular members are printed first, followed by
+digest members, but no indication is given as to address status.
+
+listname is the name of the mailing list to use.""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-o', '--output',
+ type='string', help=_("""\
+Write output to specified file instead of standard out."""))
+ self.parser.add_option(
+ '-r', '--regular',
+ default=None, action='store_true',
+ help=_('Print just the regular (non-digest) members.'))
+ self.parser.add_option(
+ '-d', '--digest',
+ default=None, type='string', metavar='KIND',
+ help=_("""\
+Print just the digest members. KIND can be 'mime', 'plain', or
+'any'. 'mime' prints just the members receiving MIME digests, while 'plain'
+prints just the members receiving plain text digests. 'any' prints all
+members receiving any kind of digest."""))
+ self.parser.add_option(
+ '-n', '--nomail',
+ type='string', metavar='WHY', help=_("""\
+Print the members that have delivery disabled. WHY selects just the subset of
+members with delivery disabled for a particular reason, where 'any' prints all
+disabled members. 'byadmin', 'byuser', 'bybounce', and 'unknown' prints just
+the users who are disabled for that particular reason. WHY can also be
+'enabled' which prints just those members for whom delivery is enabled."""))
+ self.parser.add_option(
+ '-f', '--fullnames',
+ default=False, action='store_true',
+ help=_('Include the full names in the output'))
+ self.parser.add_option(
+ '-i', '--invalid',
+ default=False, action='store_true', help=_("""\
+Print only the addresses in the membership list that are invalid. Ignores -r,
+-d, -n."""))
+
+ def sanity_check(self):
+ if not self.options.listname:
+ self.parser.error(_('Missing listname'))
+ if len(self.arguments) > 0:
+ self.parser.print_error(_('Unexpected arguments'))
+ if self.options.digest is not None:
+ self.options.kind = self.options.digest.lower()
+ if self.options.kind not in KINDCHOICES:
+ self.parser.error(
+ _('Invalid value for -d: $self.options.digest'))
+ if self.options.nomail is not None:
+ why = self.options.nomail.lower()
+ if why == 'any':
+ self.options.why = 'any'
+ elif why not in WHYCHOICES:
+ self.parser.error(
+ _('Invalid value for -n: $self.options.nomail'))
+ self.options.why = why
+ if self.options.regular is None and self.options.digest is None:
+ self.options.regular = self.options.digest = True
+ self.options.kind = 'any'
+
+
+
+def safe(string):
+ if not string:
+ return ''
+ return string.encode(sys.getdefaultencoding(), 'replace')
+
+
+def isinvalid(addr):
+ try:
+ Utils.ValidateEmail(addr)
+ return False
+ except errors.EmailAddressError:
+ return True
+
+
+
+def whymatches(mlist, addr, why):
+ # Return true if the `why' matches the reason the address is enabled, or
+ # in the case of why is None, that they are disabled for any reason
+ # (i.e. not enabled).
+ status = mlist.getDeliveryStatus(addr)
+ if why in (None, 'any'):
+ return status <> DeliveryStatus.enabled
+ return status == WHYCHOICES[why]
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ fqdn_listname = options.options.listname
+ if options.options.output:
+ try:
+ fp = open(options.output, 'w')
+ except IOError:
+ options.parser.error(
+ _('Could not open file for writing: $options.options.output'))
+ else:
+ fp = sys.stdout
+
+ mlist = config.db.list_manager.get(fqdn_listname)
+ if mlist is None:
+ options.parser.error(_('No such list: $fqdn_listname'))
+
+ # The regular delivery and digest members.
+ rmembers = set(mlist.regular_members.members)
+ dmembers = set(mlist.digest_members.members)
+
+ fullnames = options.options.fullnames
+ if options.options.invalid:
+ all = sorted(member.address.address for member in rmembers + dmembers)
+ for address in all:
+ user = config.db.user_manager.get_user(address)
+ name = (user.real_name if fullnames and user else u'')
+ if options.options.invalid and isinvalid(address):
+ print >> fp, formataddr((safe(name), address))
+ return
+ if options.options.regular:
+ for address in sorted(member.address.address for member in rmembers):
+ user = config.db.user_manager.get_user(address)
+ name = (user.real_name if fullnames and user else u'')
+ # Filter out nomails
+ if (options.options.nomail and
+ not whymatches(mlist, address, options.options.why)):
+ continue
+ print >> fp, formataddr((safe(name), address))
+ if options.options.digest:
+ for address in sorted(member.address.address for member in dmembers):
+ user = config.db.user_manager.get_user(address)
+ name = (user.real_name if fullnames and user else u'')
+ # Filter out nomails
+ if (options.options.nomail and
+ not whymatches(mlist, address, options.options.why)):
+ continue
+ # Filter out digest kinds
+## if mlist.getMemberOption(addr, config.DisableMime):
+## # They're getting plain text digests
+## if opts.kind == 'mime':
+## continue
+## else:
+## # They're getting MIME digests
+## if opts.kind == 'plain':
+## continue
+ print >> fp, formataddr((safe(name), address))
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/list_owners.py b/src/mailman/bin/list_owners.py
new file mode 100644
index 000000000..953fb8941
--- /dev/null
+++ b/src/mailman/bin/list_owners.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2002-2009 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.MailList import MailList
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.initialize import initialize
+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)
+
+ listmgr = config.db.list_manager
+ listnames = set(args or listmgr.names)
+ bylist = {}
+
+ for listname in listnames:
+ mlist = listmgr.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/src/mailman/bin/mailmanctl.py b/src/mailman/bin/mailmanctl.py
new file mode 100644
index 000000000..667a46a70
--- /dev/null
+++ b/src/mailman/bin/mailmanctl.py
@@ -0,0 +1,232 @@
+# Copyright (C) 2001-2009 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/>.
+
+"""Mailman start/stop script."""
+
+import os
+import grp
+import pwd
+import sys
+import errno
+import signal
+import logging
+
+from optparse import OptionParser
+
+from mailman.config import config
+from mailman.core.initialize import initialize
+from mailman.i18n import _
+from mailman.version import MAILMAN_VERSION
+
+
+COMMASPACE = ', '
+
+log = None
+parser = None
+
+
+
+def parseargs():
+ parser = OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+Primary start-up and shutdown script for Mailman's qrunner daemon.
+
+This script starts, stops, and restarts the main Mailman queue runners, making
+sure that the various long-running qrunners are still alive and kicking. It
+does this by forking and exec'ing the qrunners and waiting on their pids.
+When it detects a subprocess has exited, it may restart it.
+
+The qrunners respond to SIGINT, SIGTERM, SIGUSR1 and SIGHUP. SIGINT, SIGTERM
+and SIGUSR1 all cause the qrunners to exit cleanly, but the master will only
+restart qrunners that have exited due to a SIGUSR1. SIGHUP causes the master
+and the qrunners to close their log files, and reopen then upon the next
+printed message.
+
+The master also responds to SIGINT, SIGTERM, SIGUSR1 and SIGHUP, which it
+simply passes on to the qrunners (note that the master will close and reopen
+its own log files on receipt of a SIGHUP). The master also leaves its own
+process id in the file data/master-qrunner.pid but you normally don't need to
+use this pid directly. The `start', `stop', `restart', and `reopen' commands
+handle everything for you.
+
+Commands:
+
+ start - Start the master daemon and all qrunners. Prints a message and
+ exits if the master daemon is already running.
+
+ stop - Stops the master daemon and all qrunners. After stopping, no
+ more messages will be processed.
+
+ restart - Restarts the qrunners, but not the master process. Use this
+ whenever you upgrade or update Mailman so that the qrunners will
+ use the newly installed code.
+
+ reopen - This will close all log files, causing them to be re-opened the
+ next time a message is written to them
+
+Usage: %prog [options] [ start | stop | restart | reopen ]"""))
+ parser.add_option('-u', '--run-as-user',
+ default=True, action='store_false',
+ help=_("""\
+Normally, this script will refuse to run if the user id and group id are not
+set to the `mailman' user and group (as defined when you configured Mailman).
+If run as root, this script will change to this user and group before the
+check is made.
+
+This can be inconvenient for testing and debugging purposes, so the -u flag
+means that the step that sets and checks the uid/gid is skipped, and the
+program is run as the current user and group. This flag is not recommended
+for normal production environments.
+
+Note though, that if you run with -u and are not in the mailman group, you may
+have permission problems, such as begin unable to delete a list's archives
+through the web. Tough luck!"""))
+ parser.add_option('-f', '--force',
+ default=False, action='store_true',
+ help=_("""\
+If the master watcher finds an existing master lock, it will normally exit
+with an error message. With this option,the master will perform an extra
+level of checking. If a process matching the host/pid described in the lock
+file is running, the master will still exit, requiring you to manually clean
+up the lock. But if no matching process is found, the master will remove the
+apparently stale lock and make another attempt to claim the master lock."""))
+ parser.add_option('-q', '--quiet',
+ default=False, action='store_true',
+ help=_("""\
+Don't print status messages. Error messages are still printed to standard
+error."""))
+ parser.add_option('-C', '--config',
+ help=_('Alternative configuration file to use'))
+ options, arguments = parser.parse_args()
+ if not arguments:
+ parser.error(_('No command given.'))
+ if len(arguments) > 1:
+ commands = COMMASPACE.join(arguments)
+ parser.error(_('Bad command: $commands'))
+ parser.options = options
+ parser.arguments = arguments
+ return parser
+
+
+
+def kill_watcher(sig):
+ try:
+ with open(config.PIDFILE) as f:
+ pid = int(f.read().strip())
+ except (IOError, ValueError), e:
+ # For i18n convenience
+ print >> sys.stderr, _('PID unreadable in: $config.PIDFILE')
+ print >> sys.stderr, e
+ print >> sys.stderr, _('Is qrunner even running?')
+ return
+ try:
+ os.kill(pid, sig)
+ except OSError, error:
+ if e.errno <> errno.ESRCH:
+ raise
+ print >> sys.stderr, _('No child with pid: $pid')
+ print >> sys.stderr, e
+ print >> sys.stderr, _('Stale pid file removed.')
+ os.unlink(config.PIDFILE)
+
+
+
+def check_privileges():
+ # If we're running as root (uid == 0), coerce the uid and gid to that
+ # which Mailman was configured for, and refuse to run if we didn't coerce
+ # the uid/gid.
+ gid = grp.getgrnam(config.MAILMAN_GROUP).gr_gid
+ uid = pwd.getpwnam(config.MAILMAN_USER).pw_uid
+ myuid = os.getuid()
+ if myuid == 0:
+ # Set the process's supplimental groups.
+ groups = [group.gr_gid for group in grp.getgrall()
+ if config.MAILMAN_USER in group.gr_mem]
+ groups.append(gid)
+ os.setgroups(groups)
+ os.setgid(gid)
+ os.setuid(uid)
+ elif myuid <> uid:
+ name = config.MAILMAN_USER
+ parser.error(
+ _('Run this program as root or as the $name user, or use -u.'))
+
+
+
+def main():
+ global log, parser
+
+ parser = parseargs()
+ initialize(parser.options.config)
+
+ log = logging.getLogger('mailman.qrunner')
+
+ if not parser.options.run_as_user:
+ check_privileges()
+ else:
+ if not parser.options.quiet:
+ print _('Warning! You may encounter permission problems.')
+
+ # Handle the commands
+ command = parser.arguments[0].lower()
+ if command == 'stop':
+ if not parser.options.quiet:
+ print _("Shutting down Mailman's master qrunner")
+ kill_watcher(signal.SIGTERM)
+ elif command == 'restart':
+ if not parser.options.quiet:
+ print _("Restarting Mailman's master qrunner")
+ kill_watcher(signal.SIGUSR1)
+ elif command == 'reopen':
+ if not parser.options.quiet:
+ print _('Re-opening all log files')
+ kill_watcher(signal.SIGHUP)
+ elif command == 'start':
+ # Start the master qrunner watcher process.
+ #
+ # Daemon process startup according to Stevens, Advanced Programming in
+ # the UNIX Environment, Chapter 13.
+ pid = os.fork()
+ if pid:
+ # parent
+ if not parser.options.quiet:
+ print _("Starting Mailman's master qrunner.")
+ return
+ # child
+ #
+ # Create a new session and become the session leader, but since we
+ # won't be opening any terminal devices, don't do the ultra-paranoid
+ # suggestion of doing a second fork after the setsid() call.
+ os.setsid()
+ # Instead of cd'ing to root, cd to the Mailman runtime directory.
+ os.chdir(config.VAR_DIR)
+ # Exec the master watcher.
+ args = [sys.executable, sys.executable,
+ os.path.join(config.BIN_DIR, 'master')]
+ if parser.options.force:
+ args.append('--force')
+ if parser.options.config:
+ args.extend(['-C', parser.options.config])
+ log.debug('starting: %s', args)
+ os.execl(*args)
+ # We should never get here.
+ raise RuntimeError('os.execl() failed')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/master.py b/src/mailman/bin/master.py
new file mode 100644
index 000000000..d954bc865
--- /dev/null
+++ b/src/mailman/bin/master.py
@@ -0,0 +1,452 @@
+# Copyright (C) 2001-2009 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/>.
+
+"""Master sub-process watcher."""
+
+__metaclass__ = type
+__all__ = [
+ 'Loop',
+ 'get_lock_data',
+ ]
+
+
+import os
+import sys
+import errno
+import signal
+import socket
+import logging
+
+from datetime import timedelta
+from lazr.config import as_boolean
+from locknix import lockfile
+from munepy import Enum
+
+from mailman.config import config
+from mailman.core.logging import reopen
+from mailman.i18n import _
+from mailman.options import Options
+
+
+DOT = '.'
+LOCK_LIFETIME = timedelta(days=1, hours=6)
+SECONDS_IN_A_DAY = 86400
+
+
+
+class ScriptOptions(Options):
+ """Options for the master watcher."""
+
+ usage = _("""\
+Master sub-process watcher.
+
+Start and watch the configured queue runners and ensure that they stay alive
+and kicking. Each are fork and exec'd in turn, with the master waiting on
+their process ids. When it detects a child queue runner has exited, it may
+restart it.
+
+The queue runners respond to SIGINT, SIGTERM, SIGUSR1 and SIGHUP. SIGINT,
+SIGTERM and SIGUSR1 all cause the qrunners to exit cleanly. The master will
+restart qrunners that have exited due to a SIGUSR1 or some kind of other exit
+condition (say because of an exception). SIGHUP causes the master and the
+qrunners to close their log files, and reopen then upon the next printed
+message.
+
+The master also responds to SIGINT, SIGTERM, SIGUSR1 and SIGHUP, which it
+simply passes on to the qrunners. Note that the master will close and reopen
+its own log files on receipt of a SIGHUP. The master also leaves its own
+process id in the file `data/master-qrunner.pid` but you normally don't need
+to use this pid directly.
+
+Usage: %prog [options]""")
+
+ def add_options(self):
+ self.parser.add_option(
+ '-n', '--no-restart',
+ dest='restartable', default=True, action='store_false',
+ help=_("""\
+Don't restart the qrunners when they exit because of an error or a SIGUSR1.
+Use this only for debugging."""))
+ self.parser.add_option(
+ '-f', '--force',
+ default=False, action='store_true',
+ help=_("""\
+If the master watcher finds an existing master lock, it will normally exit
+with an error message. With this option,the master will perform an extra
+level of checking. If a process matching the host/pid described in the lock
+file is running, the master will still exit, requiring you to manually clean
+up the lock. But if no matching process is found, the master will remove the
+apparently stale lock and make another attempt to claim the master lock."""))
+ self.parser.add_option(
+ '-r', '--runner',
+ dest='runners', action='append', default=[],
+ help=_("""\
+Override the default set of queue runners that the master watch will invoke
+instead of the default set. Multiple -r options may be given. The values for
+-r are passed straight through to bin/qrunner."""))
+
+ def sanity_check(self):
+ if len(self.arguments) > 0:
+ self.parser.error(_('Too many arguments'))
+
+
+
+def get_lock_data():
+ """Get information from the master lock file.
+
+ :return: A 3-tuple of the hostname, integer process id, and file name of
+ the lock file.
+ """
+ with open(config.LOCK_FILE) as fp:
+ filename = os.path.split(fp.read().strip())[1]
+ parts = filename.split('.')
+ hostname = DOT.join(parts[1:-2])
+ pid = int(parts[-2])
+ return hostname, int(pid), filename
+
+
+class WatcherState(Enum):
+ # Another master watcher is running.
+ conflict = 1
+ # No conflicting process exists.
+ stale_lock = 2
+ # Hostname from lock file doesn't match.
+ host_mismatch = 3
+
+
+def master_state():
+ """Get the state of the master watcher.
+
+ :return: WatcherState describing the state of the lock file.
+ """
+
+ # 1 if proc exists on host (but is it qrunner? ;)
+ # 0 if host matches but no proc
+ # hostname if hostname doesn't match
+ hostname, pid, tempfile = get_lock_data()
+ if hostname <> socket.gethostname():
+ return WatcherState.host_mismatch
+ # Find out if the process exists by calling kill with a signal 0.
+ try:
+ os.kill(pid, 0)
+ return WatcherState.conflict
+ except OSError, e:
+ if e.errno == errno.ESRCH:
+ # No matching process id.
+ return WatcherState.stale_lock
+ # Some other error occurred.
+ raise
+
+
+def acquire_lock_1(force):
+ """Try to acquire the master queue runner lock.
+
+ :param force: Flag that controls whether to force acquisition of the lock.
+ :return: The master queue runner lock.
+ :raises: `TimeOutError` if the lock could not be acquired.
+ """
+ lock = lockfile.Lock(config.LOCK_FILE, LOCK_LIFETIME)
+ try:
+ lock.lock(timedelta(seconds=0.1))
+ return lock
+ except lockfile.TimeOutError:
+ if not force:
+ raise
+ # Force removal of lock first.
+ lock.disown()
+ hostname, pid, tempfile = get_lock_data()
+ os.unlink(config.LOCK_FILE)
+ os.unlink(os.path.join(config.LOCK_DIR, tempfile))
+ return acquire_lock_1(force=False)
+
+
+def acquire_lock(force):
+ """Acquire the master queue runner lock.
+
+ :return: The master queue runner lock or None if the lock couldn't be
+ acquired. In that case, an error messages is also printed to standard
+ error.
+ """
+ try:
+ lock = acquire_lock_1(force)
+ return lock
+ except lockfile.TimeOutError:
+ status = master_state()
+ if status == WatcherState.conflict:
+ # Hostname matches and process exists.
+ message = _("""\
+The master qrunner lock could not be acquired because it appears
+as though another master qrunner is already running.
+""")
+ elif status == WatcherState.stale_lock:
+ # Hostname matches but the process does not exist.
+ message = _("""\
+The master qrunner lock could not be acquired. It appears as though there is
+a stale master qrunner lock. Try re-running mailmanctl with the -s flag.
+""")
+ else:
+ assert status == WatcherState.host_mismatch, (
+ 'Invalid enum value: %s' % status)
+ # Hostname doesn't even match.
+ hostname, pid, tempfile = get_lock_data()
+ message = _("""\
+The master qrunner lock could not be acquired, because it appears as if some
+process on some other host may have acquired it. We can't test for stale
+locks across host boundaries, so you'll have to clean this up manually.
+
+Lock file: $config.LOCK_FILE
+Lock host: $hostname
+
+Exiting.""")
+ config.options.parser.error(message)
+
+
+
+class Loop:
+ """Main control loop class."""
+
+ def __init__(self, lock=None, restartable=None, config_file=None):
+ self._lock = lock
+ self._restartable = restartable
+ self._config_file = config_file
+ self._kids = {}
+
+ def install_signal_handlers(self):
+ """Install various signals handlers for control from mailmanctl."""
+ log = logging.getLogger('mailman.qrunner')
+ # Set up our signal handlers. Also set up a SIGALRM handler to
+ # refresh the lock once per day. The lock lifetime is 1 day + 6 hours
+ # so this should be plenty.
+ def sigalrm_handler(signum, frame):
+ self._lock.refresh()
+ signal.alarm(SECONDS_IN_A_DAY)
+ signal.signal(signal.SIGALRM, sigalrm_handler)
+ signal.alarm(SECONDS_IN_A_DAY)
+ # SIGHUP tells the qrunners to close and reopen their log files.
+ def sighup_handler(signum, frame):
+ reopen()
+ for pid in self._kids:
+ os.kill(pid, signal.SIGHUP)
+ log.info('Master watcher caught SIGHUP. Re-opening log files.')
+ signal.signal(signal.SIGHUP, sighup_handler)
+ # SIGUSR1 is used by 'mailman restart'.
+ def sigusr1_handler(signum, frame):
+ for pid in self._kids:
+ os.kill(pid, signal.SIGUSR1)
+ log.info('Master watcher caught SIGUSR1. Exiting.')
+ signal.signal(signal.SIGUSR1, sigusr1_handler)
+ # SIGTERM is what init will kill this process with when changing run
+ # levels. It's also the signal 'mailmanctl stop' uses.
+ def sigterm_handler(signum, frame):
+ for pid in self._kids:
+ os.kill(pid, signal.SIGTERM)
+ log.info('Master watcher caught SIGTERM. Exiting.')
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ # SIGINT is what control-C gives.
+ def sigint_handler(signum, frame):
+ for pid in self._kids:
+ os.kill(pid, signal.SIGINT)
+ log.info('Master watcher caught SIGINT. Restarting.')
+ signal.signal(signal.SIGINT, sigint_handler)
+
+ def _start_runner(self, spec):
+ """Start a queue runner.
+
+ All arguments are passed to the qrunner process.
+
+ :param spec: A queue runner spec, in a format acceptable to
+ bin/qrunner's --runner argument, e.g. name:slice:count
+ :type spec: string
+ :return: The process id of the child queue runner.
+ :rtype: int
+ """
+ pid = os.fork()
+ if pid:
+ # Parent.
+ return pid
+ # Child.
+ #
+ # Craft the command line arguments for the exec() call.
+ rswitch = '--runner=' + spec
+ # Wherever mailmanctl lives, so too must live the qrunner script.
+ exe = os.path.join(config.BIN_DIR, 'qrunner')
+ # config.PYTHON, which is the absolute path to the Python interpreter,
+ # must be given as argv[0] due to Python's library search algorithm.
+ args = [sys.executable, sys.executable, exe, rswitch, '-s']
+ if self._config_file is not None:
+ args.extend(['-C', self._config_file])
+ log = logging.getLogger('mailman.qrunner')
+ log.debug('starting: %s', args)
+ os.execl(*args)
+ # We should never get here.
+ raise RuntimeError('os.execl() failed')
+
+ def start_qrunners(self, qrunner_names=None):
+ """Start all the configured qrunners.
+
+ :param qrunners: If given, a sequence of queue runner names to start.
+ If not given, this sequence is taken from the configuration file.
+ :type qrunners: a sequence of strings
+ """
+ if not qrunner_names:
+ qrunner_names = []
+ for qrunner_config in config.qrunner_configs:
+ # Strip off the 'qrunner.' prefix.
+ assert qrunner_config.name.startswith('qrunner.'), (
+ 'Unexpected qrunner configuration section name: %s',
+ qrunner_config.name)
+ qrunner_names.append(qrunner_config.name[8:])
+ # For each qrunner we want to start, find their config section, which
+ # will tell us the name of the class to instantiate, along with the
+ # number of hash space slices to manage.
+ for name in qrunner_names:
+ section_name = 'qrunner.' + name
+ # Let AttributeError propagate.
+ qrunner_config = getattr(config, section_name)
+ if not as_boolean(qrunner_config.start):
+ continue
+ package, class_name = qrunner_config['class'].rsplit(DOT, 1)
+ __import__(package)
+ # Let AttributeError propagate.
+ class_ = getattr(sys.modules[package], class_name)
+ # Find out how many qrunners to instantiate. This must be a power
+ # of 2.
+ count = int(qrunner_config.instances)
+ assert (count & (count - 1)) == 0, (
+ 'Queue runner "%s", not a power of 2: %s', name, count)
+ for slice_number in range(count):
+ # qrunner name, slice #, # of slices, restart count
+ info = (name, slice_number, count, 0)
+ spec = '%s:%d:%d' % (name, slice_number, count)
+ pid = self._start_runner(spec)
+ log = logging.getLogger('mailman.qrunner')
+ log.debug('[%d] %s', pid, spec)
+ self._kids[pid] = info
+
+ def loop(self):
+ """Main loop.
+
+ Wait until all the qrunners have exited, restarting them if necessary
+ and configured to do so.
+ """
+ log = logging.getLogger('mailman.qrunner')
+ while True:
+ try:
+ pid, status = os.wait()
+ except OSError, error:
+ # No children? We're done.
+ if error.errno == errno.ECHILD:
+ break
+ # If the system call got interrupted, just restart it.
+ elif error.errno == errno.EINTR:
+ continue
+ else:
+ raise
+ # Find out why the subprocess exited by getting the signal
+ # received or exit status.
+ if os.WIFSIGNALED(status):
+ why = os.WTERMSIG(status)
+ elif os.WIFEXITED(status):
+ why = os.WEXITSTATUS(status)
+ else:
+ why = None
+ # We'll restart the subprocess if it exited with a SIGUSR1 or
+ # because of a failure (i.e. no exit signal), and the no-restart
+ # command line switch was not given. This lets us better handle
+ # runaway restarts (e.g. if the subprocess had a syntax error!)
+ qrname, slice_number, count, restarts = self._kids.pop(pid)
+ config_name = 'qrunner.' + qrname
+ restart = False
+ if why == signal.SIGUSR1 and self._restartable:
+ restart = True
+ # Have we hit the maximum number of restarts?
+ restarts += 1
+ max_restarts = int(getattr(config, config_name).max_restarts)
+ if restarts > max_restarts:
+ restart = False
+ # Are we permanently non-restartable?
+ log.debug("""\
+Master detected subprocess exit
+(pid: %d, why: %s, class: %s, slice: %d/%d) %s""",
+ pid, why, qrname, slice_number + 1, count,
+ ('[restarting]' if restart else ''))
+ # See if we've reached the maximum number of allowable restarts
+ if restarts > max_restarts:
+ log.info("""\
+qrunner %s reached maximum restart limit of %d, not restarting.""",
+ qrname, max_restarts)
+ # Now perhaps restart the process unless it exited with a
+ # SIGTERM or we aren't restarting.
+ if restart:
+ spec = '%s:%d:%d' % (qrname, slice_number, count)
+ newpid = self._start_runner(spec)
+ self._kids[newpid] = (qrname, slice_number, count, restarts)
+
+ def cleanup(self):
+ """Ensure that all children have exited."""
+ log = logging.getLogger('mailman.qrunner')
+ # Send SIGTERMs to all the child processes and wait for them all to
+ # exit.
+ for pid in self._kids:
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except OSError, error:
+ if error.errno == errno.ESRCH:
+ # The child has already exited.
+ log.info('ESRCH on pid: %d', pid)
+ # Wait for all the children to go away.
+ while self._kids:
+ try:
+ pid, status = os.wait()
+ del self._kids[pid]
+ except OSError, e:
+ if e.errno == errno.ECHILD:
+ break
+ elif e.errno == errno.EINTR:
+ continue
+ raise
+
+
+
+def main():
+ """Main process."""
+
+ options = ScriptOptions()
+ options.initialize()
+
+ # Acquire the master lock, exiting if we can't acquire it. We'll let the
+ # caller handle any clean up or lock breaking. No with statement here
+ # because Lock's constructor doesn't support a timeout.
+ lock = acquire_lock(options.options.force)
+ try:
+ with open(config.PIDFILE, 'w') as fp:
+ print >> fp, os.getpid()
+ loop = Loop(lock, options.options.restartable, options.options.config)
+ loop.install_signal_handlers()
+ try:
+ loop.start_qrunners(options.options.runners)
+ loop.loop()
+ finally:
+ loop.cleanup()
+ os.remove(config.PIDFILE)
+ finally:
+ lock.unlock()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/mmsitepass.py b/src/mailman/bin/mmsitepass.py
new file mode 100644
index 000000000..132803fc9
--- /dev/null
+++ b/src/mailman/bin/mmsitepass.py
@@ -0,0 +1,113 @@
+# Copyright (C) 1998-2009 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 getpass
+import optparse
+
+from mailman import Utils
+from mailman import passwords
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.initialize import initialize
+from mailman.version import MAILMAN_VERSION
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%prog [options] [password]
+
+Set the site or list creator password.
+
+The site password can be used in most if not all places that the list
+administrator's password can be used, which in turn can be used in most places
+that a list user's password can be used. The list creator password is a
+separate password that can be given to non-site administrators to delegate the
+ability to create new mailing lists.
+
+If password is not given on the command line, it will be prompted for.
+"""))
+ parser.add_option('-c', '--listcreator',
+ default=False, action='store_true',
+ help=_("""\
+Set the list creator password instead of the site password. The list
+creator is authorized to create and remove lists, but does not have
+the total power of the site administrator."""))
+ parser.add_option('-p', '--password-scheme',
+ default='', type='string',
+ help=_("""\
+Specify the RFC 2307 style hashing scheme for passwords included in the
+output. Use -P to get a list of supported schemes, which are
+case-insensitive."""))
+ parser.add_option('-P', '--list-hash-schemes',
+ default=False, action='store_true', help=_("""\
+List the supported password hashing schemes and exit. The scheme labels are
+case-insensitive."""))
+ parser.add_option('-C', '--config',
+ help=_('Alternative configuration file to use'))
+ opts, args = parser.parse_args()
+ if len(args) > 1:
+ parser.error(_('Unexpected arguments'))
+ if opts.list_hash_schemes:
+ for label in passwords.Schemes:
+ print str(label).upper()
+ sys.exit(0)
+ return parser, opts, args
+
+
+def check_password_scheme(parser, password_scheme):
+ # shoule be checked after config is loaded.
+ if password_scheme == '':
+ password_scheme = config.PASSWORD_SCHEME
+ scheme = passwords.lookup_scheme(password_scheme.lower())
+ if not scheme:
+ parser.error(_('Invalid password scheme'))
+ return scheme
+
+
+
+def main():
+ parser, opts, args = parseargs()
+ initialize(opts.config)
+ opts.password_scheme = check_password_scheme(parser, opts.password_scheme)
+ if args:
+ password = args[0]
+ else:
+ # Prompt for the password
+ if opts.listcreator:
+ prompt_1 = _('New list creator password: ')
+ else:
+ prompt_1 = _('New site administrator password: ')
+ pw1 = getpass.getpass(prompt_1)
+ pw2 = getpass.getpass(_('Enter password again to confirm: '))
+ if pw1 <> pw2:
+ print _('Passwords do not match; no changes made.')
+ sys.exit(1)
+ password = pw1
+ Utils.set_global_password(password,
+ not opts.listcreator, opts.password_scheme)
+ if Utils.check_global_password(password, not opts.listcreator):
+ print _('Password changed.')
+ else:
+ print _('Password change failed.')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/nightly_gzip.py b/src/mailman/bin/nightly_gzip.py
new file mode 100644
index 000000000..f886e5801
--- /dev/null
+++ b/src/mailman/bin/nightly_gzip.py
@@ -0,0 +1,117 @@
+# Copyright (C) 1998-2009 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
+
+try:
+ import gzip
+except ImportError:
+ sys.exit(0)
+
+from mailman import MailList
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.initialize import initialize
+from mailman.version import MAILMAN_VERSION
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%prog [options] [listname ...]
+
+Re-generate the Pipermail gzip'd archive flat files."""))
+ parser.add_option('-v', '--verbose',
+ default=False, action='store_true',
+ help=_("Print each file as it's being gzip'd"))
+ parser.add_option('-z', '--level',
+ default=6, type='int',
+ help=_('Specifies the compression level'))
+ parser.add_option('-C', '--config',
+ help=_('Alternative configuration file to use'))
+ opts, args = parser.parse_args()
+ if opts.level < 1 or opts.level > 9:
+ parser.print_help()
+ print >> sys.stderr, _('Illegal compression level: $opts.level')
+ sys.exit(1)
+ return opts, args, parser
+
+
+
+def compress(txtfile, opts):
+ if opts.verbose:
+ print _("gzip'ing: $txtfile")
+ infp = outfp = None
+ try:
+ infp = open(txtfile)
+ outfp = gzip.open(txtfile + '.gz', 'wb', opts.level)
+ outfp.write(infp.read())
+ finally:
+ if outfp:
+ outfp.close()
+ if infp:
+ infp.close()
+
+
+
+def main():
+ opts, args, parser = parseargs()
+ initialize(opts.config)
+
+ if config.ARCHIVE_TO_MBOX not in (1, 2) or config.GZIP_ARCHIVE_TXT_FILES:
+ # We're only going to run the nightly archiver if messages are
+ # archived to the mbox, and the gzip file is not created on demand
+ # (i.e. for every individual post). This is the normal mode of
+ # operation.
+ return
+
+ # Process all the specified lists
+ for listname in set(args or config.list_manager.names):
+ mlist = MailList.MailList(listname, lock=False)
+ if not mlist.archive:
+ continue
+ dir = mlist.archive_dir()
+ try:
+ allfiles = os.listdir(dir)
+ except OSError:
+ # Has the list received any messages? If not, last_post_time will
+ # be zero, so it's not really a bogus archive dir.
+ if mlist.last_post_time > 0:
+ print _('List $listname has a bogus archive_directory: $dir')
+ continue
+ if opts.verbose:
+ print _('Processing list: $listname')
+ files = []
+ for f in allfiles:
+ if os.path.splitext(f)[1] <> '.txt':
+ continue
+ # stat both the .txt and .txt.gz files and append them only if
+ # the former is newer than the latter.
+ txtfile = os.path.join(dir, f)
+ gzpfile = txtfile + '.gz'
+ txt_mtime = os.path.getmtime(txtfile)
+ try:
+ gzp_mtime = os.path.getmtime(gzpfile)
+ except OSError:
+ gzp_mtime = -1
+ if txt_mtime > gzp_mtime:
+ files.append(txtfile)
+ for f in files:
+ compress(f, opts)
diff --git a/src/mailman/bin/qrunner.py b/src/mailman/bin/qrunner.py
new file mode 100644
index 000000000..62e943aad
--- /dev/null
+++ b/src/mailman/bin/qrunner.py
@@ -0,0 +1,269 @@
+# Copyright (C) 2001-2009 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 signal
+import logging
+
+from mailman.config import config
+from mailman.core.logging import reopen
+from mailman.i18n import _
+from mailman.options import Options
+
+
+COMMASPACE = ', '
+log = None
+
+
+
+def r_callback(option, opt, value, parser):
+ dest = getattr(parser.values, option.dest)
+ parts = value.split(':')
+ if len(parts) == 1:
+ runner = parts[0]
+ rslice = rrange = 1
+ elif len(parts) == 3:
+ runner = parts[0]
+ try:
+ rslice = int(parts[1])
+ rrange = int(parts[2])
+ except ValueError:
+ parser.print_help()
+ print >> sys.stderr, _('Bad runner specification: $value')
+ sys.exit(1)
+ else:
+ parser.print_help()
+ print >> sys.stderr, _('Bad runner specification: $value')
+ sys.exit(1)
+ dest.append((runner, rslice, rrange))
+
+
+
+class ScriptOptions(Options):
+
+ usage = _("""\
+Run one or more qrunners, once or repeatedly.
+
+Each named runner class is run in round-robin fashion. In other words, the
+first named runner is run to consume all the files currently in its
+directory. When that qrunner is done, the next one is run to consume all the
+files in /its/ directory, and so on. The number of total iterations can be
+given on the command line.
+
+Usage: %prog [options]
+
+-r is required unless -l or -h is given, and its argument must be one of the
+names displayed by the -l switch.
+
+Normally, this script should be started from mailmanctl. Running it
+separately or with -o is generally useful only for debugging.
+""")
+
+ def add_options(self):
+ self.parser.add_option(
+ '-r', '--runner',
+ metavar='runner[:slice:range]', dest='runners',
+ type='string', default=[],
+ action='callback', callback=r_callback,
+ help=_("""\
+Run the named qrunner, which must be one of the strings returned by the -l
+option. Optional slice:range if given, is used to assign multiple qrunner
+processes to a queue. range is the total number of qrunners for this queue
+while slice is the number of this qrunner from [0..range).
+
+When using the slice:range form, you must ensure that each qrunner for the
+queue is given the same range value. If slice:runner is not given, then 1:1
+is used.
+
+Multiple -r options may be given, in which case each qrunner will run once in
+round-robin fashion. The special runner `All' is shorthand for a qrunner for
+each listed by the -l option."""))
+ self.parser.add_option(
+ '-o', '--once',
+ default=False, action='store_true', help=_("""\
+Run each named qrunner exactly once through its main loop. Otherwise, each
+qrunner runs indefinitely, until the process receives signal."""))
+ self.parser.add_option(
+ '-l', '--list',
+ default=False, action='store_true',
+ help=_('List the available qrunner names and exit.'))
+ self.parser.add_option(
+ '-v', '--verbose',
+ default=0, action='count', help=_("""\
+Display more debugging information to the logs/qrunner log file."""))
+ self.parser.add_option(
+ '-s', '--subproc',
+ default=False, action='store_true', help=_("""\
+This should only be used when running qrunner as a subprocess of the
+mailmanctl startup script. It changes some of the exit-on-error behavior to
+work better with that framework."""))
+
+ def sanity_check(self):
+ if self.arguments:
+ self.parser.error(_('Unexpected arguments'))
+ if not self.options.runners and not self.options.list:
+ self.parser.error(_('No runner name given.'))
+
+
+
+def make_qrunner(name, slice, range, once=False):
+ # Several conventions for specifying the runner name are supported. It
+ # could be one of the shortcut names. If the name is a full module path,
+ # use it explicitly. If the name starts with a dot, it's a class name
+ # relative to the Mailman.queue package.
+ qrunner_config = getattr(config, 'qrunner.' + name, None)
+ if qrunner_config is not None:
+ # It was a shortcut name.
+ class_path = qrunner_config['class']
+ elif name.startswith('.'):
+ class_path = 'mailman.queue' + name
+ else:
+ class_path = name
+ module_name, class_name = class_path.rsplit('.', 1)
+ try:
+ __import__(module_name)
+ except ImportError, e:
+ if config.options.options.subproc:
+ # Exit with SIGTERM exit code so the master watcher won't try to
+ # restart us.
+ print >> sys.stderr, _('Cannot import runner module: $module_name')
+ print >> sys.stderr, e
+ sys.exit(signal.SIGTERM)
+ else:
+ raise
+ qrclass = getattr(sys.modules[module_name], class_name)
+ if once:
+ # Subclass to hack in the setting of the stop flag in _do_periodic()
+ class Once(qrclass):
+ def _do_periodic(self):
+ self.stop()
+ qrunner = Once(name, slice)
+ else:
+ qrunner = qrclass(name, slice)
+ return qrunner
+
+
+
+def set_signals(loop):
+ """Set up the signal handlers.
+
+ Signals caught are: SIGTERM, SIGINT, SIGUSR1 and SIGHUP. The latter is
+ used to re-open the log files. SIGTERM and SIGINT are treated exactly the
+ same -- they cause qrunner to exit with no restart from the master.
+ SIGUSR1 also causes qrunner to exit, but the master watcher will restart
+ it in that case.
+
+ :param loop: A loop queue runner instance.
+ """
+ def sigterm_handler(signum, frame):
+ # Exit the qrunner cleanly
+ loop.stop()
+ loop.status = signal.SIGTERM
+ log.info('%s qrunner caught SIGTERM. Stopping.', loop.name())
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ def sigint_handler(signum, frame):
+ # Exit the qrunner cleanly
+ loop.stop()
+ loop.status = signal.SIGINT
+ log.info('%s qrunner caught SIGINT. Stopping.', loop.name())
+ signal.signal(signal.SIGINT, sigint_handler)
+ def sigusr1_handler(signum, frame):
+ # Exit the qrunner cleanly
+ loop.stop()
+ loop.status = signal.SIGUSR1
+ log.info('%s qrunner caught SIGUSR1. Stopping.', loop.name())
+ signal.signal(signal.SIGUSR1, sigusr1_handler)
+ # SIGHUP just tells us to rotate our log files.
+ def sighup_handler(signum, frame):
+ reopen()
+ log.info('%s qrunner caught SIGHUP. Reopening logs.', loop.name())
+ signal.signal(signal.SIGHUP, sighup_handler)
+
+
+
+def main():
+ global log
+
+ options = ScriptOptions()
+ options.initialize()
+
+ if options.options.list:
+ prefixlen = max(len(shortname)
+ for shortname in config.qrunner_shortcuts)
+ for shortname in sorted(config.qrunner_shortcuts):
+ runnername = config.qrunner_shortcuts[shortname]
+ shortname = (' ' * (prefixlen - len(shortname))) + shortname
+ print _('$shortname runs $runnername')
+ sys.exit(0)
+
+ # Fast track for one infinite runner
+ if len(options.options.runners) == 1 and not options.options.once:
+ qrunner = make_qrunner(*options.options.runners[0])
+ class Loop:
+ status = 0
+ def __init__(self, qrunner):
+ self._qrunner = qrunner
+ def name(self):
+ return self._qrunner.__class__.__name__
+ def stop(self):
+ self._qrunner.stop()
+ loop = Loop(qrunner)
+ set_signals(loop)
+ # Now start up the main loop
+ log = logging.getLogger('mailman.qrunner')
+ log.info('%s qrunner started.', loop.name())
+ qrunner.run()
+ log.info('%s qrunner exiting.', loop.name())
+ else:
+ # Anything else we have to handle a bit more specially
+ qrunners = []
+ for runner, rslice, rrange in options.options.runners:
+ qrunner = make_qrunner(runner, rslice, rrange, once=True)
+ qrunners.append(qrunner)
+ # This class is used to manage the main loop
+ class Loop:
+ status = 0
+ def __init__(self):
+ self._isdone = False
+ def name(self):
+ return 'Main loop'
+ def stop(self):
+ self._isdone = True
+ def isdone(self):
+ return self._isdone
+ loop = Loop()
+ set_signals(loop)
+ log.info('Main qrunner loop started.')
+ while not loop.isdone():
+ for qrunner in qrunners:
+ # In case the SIGTERM came in the middle of this iteration
+ if loop.isdone():
+ break
+ if options.options.verbose:
+ log.info('Now doing a %s qrunner iteration',
+ qrunner.__class__.__bases__[0].__name__)
+ qrunner.run()
+ if options.options.once:
+ break
+ log.info('Main qrunner loop exiting.')
+ # All done
+ sys.exit(loop.status)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/remove_list.py b/src/mailman/bin/remove_list.py
new file mode 100644
index 000000000..05211b200
--- /dev/null
+++ b/src/mailman/bin/remove_list.py
@@ -0,0 +1,83 @@
+# Copyright (C) 1998-2009 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
+
+from mailman.app.lifecycle import remove_list
+from mailman.config import config
+from mailman.i18n import _
+from mailman.options import MultipleMailingListOptions
+
+
+
+class ScriptOptions(MultipleMailingListOptions):
+ usage = _("""\
+%prog [options]
+
+Remove the components of a mailing list with impunity - beware!
+
+This removes (almost) all traces of a mailing list. By default, the lists
+archives are not removed, which is very handy for retiring old lists.
+""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-a', '--archives',
+ default=False, action='store_true',
+ help=_("""\
+Remove the list's archives too, or if the list has already been deleted,
+remove any residual archives."""))
+ self.parser.add_option(
+ '-q', '--quiet',
+ default=False, action='store_true',
+ help=_('Suppress status messages'))
+
+ def sanity_check(self):
+ if len(self.options.listnames) == 0:
+ self.parser.error(_('Nothing to do'))
+ if len(self.arguments) > 0:
+ self.parser.error(_('Unexpected arguments'))
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ for fqdn_listname in options.options.listnames:
+ if not options.options.quiet:
+ print _('Removing list: $fqdn_listname')
+ mlist = config.db.list_manager.get(fqdn_listname)
+ if mlist is None:
+ if options.options.archives:
+ print _("""\
+No such list: ${fqdn_listname}. Removing its residual archives.""")
+ else:
+ print >> sys.stderr, _(
+ 'No such list (or list already deleted): $fqdn_listname')
+
+ if not options.options.archives:
+ print _('Not removing archives. Reinvoke with -a to remove them.')
+
+ remove_list(fqdn_listname, mlist, options.options.archives)
+ config.db.commit()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/senddigests.py b/src/mailman/bin/senddigests.py
new file mode 100644
index 000000000..fb057d6b9
--- /dev/null
+++ b/src/mailman/bin/senddigests.py
@@ -0,0 +1,83 @@
+# Copyright (C) 1998-2009 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.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, 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/src/mailman/bin/set_members.py b/src/mailman/bin/set_members.py
new file mode 100644
index 000000000..cdd11c56f
--- /dev/null
+++ b/src/mailman/bin/set_members.py
@@ -0,0 +1,189 @@
+# Copyright (C) 2007-2009 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 csv
+import optparse
+
+from mailman import Message
+from mailman import Utils
+from mailman import i18n
+from mailman import passwords
+from mailman.app.membership import add_member
+from mailman.app.notifications import (
+ send_admin_subscription_notice, send_welcome_message)
+from mailman.configuration import config
+from mailman.initialize import initialize
+from mailman.interfaces import DeliveryMode
+from mailman.version import MAILMAN_VERSION
+
+
+_ = i18n._
+
+DELIVERY_MODES = {
+ 'regular': DeliveryMode.regular,
+ 'plain': DeliveryMode.plaintext_digests,
+ 'mime': DeliveryMode.mime_digests,
+ }
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%prog [options] csv-file
+
+Set the membership of a mailing list to that described in a CSV file. Each
+row of the CSV file has the following format. Only the address column is
+required.
+
+ - email address
+ - full name (default: the empty string)
+ - delivery mode (default: regular delivery) [1]
+
+[1] The delivery mode is a case insensitive string of the following values:
+
+ regular - regular, i.e. immediate delivery
+ mime - MIME digest delivery
+ plain - plain text (RFC 1153) digest delivery
+
+Any address not included in the CSV file is removed from the list membership.
+"""))
+ parser.add_option('-l', '--listname',
+ type='string', help=_("""\
+Mailng list to set the membership for."""))
+ parser.add_option('-w', '--welcome-msg',
+ type='string', metavar='<y|n>', help=_("""\
+Set whether or not to send the list members a welcome message, overriding
+whatever the list's 'send_welcome_msg' setting is."""))
+ parser.add_option('-a', '--admin-notify',
+ type='string', metavar='<y|n>', help=_("""\
+Set whether or not to send the list administrators a notification on the
+success/failure of these subscriptions, overriding whatever the list's
+'admin_notify_mchanges' setting is."""))
+ parser.add_option('-v', '--verbose', action='store_true',
+ help=_('Increase verbosity'))
+ parser.add_option('-C', '--config',
+ help=_('Alternative configuration file to use'))
+ opts, args = parser.parse_args()
+ if opts.welcome_msg is not None:
+ ch = opts.welcome_msg[0].lower()
+ if ch == 'y':
+ opts.welcome_msg = True
+ elif ch == 'n':
+ opts.welcome_msg = False
+ else:
+ parser.error(_('Illegal value for -w: $opts.welcome_msg'))
+ if opts.admin_notify is not None:
+ ch = opts.admin_notify[0].lower()
+ if ch == 'y':
+ opts.admin_notify = True
+ elif ch == 'n':
+ opts.admin_notify = False
+ else:
+ parser.error(_('Illegal value for -a: $opts.admin_notify'))
+ return parser, opts, args
+
+
+
+def parse_file(filename):
+ members = {}
+ with open(filename) as fp:
+ for row in csv.reader(fp):
+ if len(row) == 0:
+ continue
+ elif len(row) == 1:
+ address = row[0]
+ real_name = None
+ delivery_mode = DeliveryMode.regular
+ elif len(row) == 2:
+ address, real_name = row
+ delivery_mode = DeliveryMode.regular
+ else:
+ # Ignore extra columns
+ address, real_name = row[0:2]
+ delivery_mode = DELIVERY_MODES.get(row[2].lower())
+ if delivery_mode is None:
+ delivery_mode = DeliveryMode.regular
+ members[address] = real_name, delivery_mode
+ return members
+
+
+
+def main():
+ parser, opts, args = parseargs()
+ initialize(opts.config)
+
+ mlist = config.db.list_manager.get(opts.listname)
+ if mlist is None:
+ parser.error(_('No such list: $opts.listname'))
+
+ # Set up defaults.
+ if opts.welcome_msg is None:
+ send_welcome_msg = mlist.send_welcome_msg
+ else:
+ send_welcome_msg = opts.welcome_msg
+ if opts.admin_notify is None:
+ admin_notify = mlist.admin_notify_mchanges
+ else:
+ admin_notify = opts.admin_notify
+
+ # Parse the csv files.
+ member_data = {}
+ for filename in args:
+ member_data.update(parse_file(filename))
+
+ future_members = set(member_data)
+ current_members = set(obj.address for obj in mlist.members.addresses)
+ add_members = future_members - current_members
+ delete_members = current_members - future_members
+ change_members = current_members & future_members
+
+ with i18n.using_language(mlist.preferred_language):
+ # Start by removing all the delete members.
+ for address in delete_members:
+ print _('deleting address: $address')
+ member = mlist.members.get_member(address)
+ member.unsubscribe()
+ # For all members that are in both lists, update their full name and
+ # delivery mode.
+ for address in change_members:
+ print _('updating address: $address')
+ real_name, delivery_mode = member_data[address]
+ member = mlist.members.get_member(address)
+ member.preferences.delivery_mode = delivery_mode
+ user = config.db.user_manager.get_user(address)
+ user.real_name = real_name
+ for address in add_members:
+ print _('adding address: $address')
+ real_name, delivery_mode = member_data[address]
+ password = passwords.make_secret(
+ Utils.MakeRandomPassword(),
+ passwords.lookup_scheme(config.PASSWORD_SCHEME))
+ add_member(mlist, address, real_name, password, delivery_mode,
+ mlist.preferred_language, send_welcome_msg,
+ admin_notify)
+ if send_welcome_msg:
+ send_welcome_message(mlist, address, language, delivery_mode)
+ if admin_notify:
+ send_admin_subscription_notice(mlist, address, real_name)
+
+ config.db.flush()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/show_config.py b/src/mailman/bin/show_config.py
new file mode 100644
index 000000000..8d26c5c97
--- /dev/null
+++ b/src/mailman/bin/show_config.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2006-2009 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.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()
diff --git a/src/mailman/bin/show_qfiles.py b/src/mailman/bin/show_qfiles.py
new file mode 100644
index 000000000..e4b64e0cd
--- /dev/null
+++ b/src/mailman/bin/show_qfiles.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2006-2009 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
+
+from cPickle import load
+
+from mailman.config import config
+from mailman.i18n import _
+from mailman.options import Options
+
+
+
+class ScriptOptions(Options):
+ usage = _("""
+%%prog [options] qfiles ...
+
+Show the contents of one or more Mailman queue files.""")
+
+ def add_options(self):
+ super(ScriptOptions, self).add_options()
+ self.parser.add_option(
+ '-q', '--quiet',
+ default=False, action='store_true',
+ help=_("Don't print 'helpful' message delimiters."))
+ self.parser.add_option(
+ '-s', '--summary',
+ default=False, action='store_true',
+ help=_('Show a summary of queue files.'))
+
+
+
+def main():
+ options = ScriptOptions()
+ options.initialize()
+
+ if options.options.summary:
+ queue_totals = {}
+ files_by_queue = {}
+ for switchboard in config.switchboards.values():
+ total = 0
+ file_mappings = {}
+ for filename in os.listdir(switchboard.queue_directory):
+ base, ext = os.path.splitext(filename)
+ file_mappings[ext] = file_mappings.get(ext, 0) + 1
+ total += 1
+ files_by_queue[switchboard.queue_directory] = file_mappings
+ queue_totals[switchboard.queue_directory] = total
+ # Sort by queue name.
+ for queue_directory in sorted(files_by_queue):
+ total = queue_totals[queue_directory]
+ print queue_directory
+ print _('\tfile count: $total')
+ file_mappings = files_by_queue[queue_directory]
+ for ext in sorted(file_mappings):
+ print '\t{0}: {1}'.format(ext, file_mappings[ext])
+ return
+ # No summary.
+ for filename in options.arguments:
+ if not options.options.quiet:
+ print '====================>', filename
+ with open(filename) as fp:
+ if filename.endswith('.pck'):
+ msg = load(fp)
+ data = load(fp)
+ if data.get('_parsemsg'):
+ sys.stdout.write(msg)
+ else:
+ sys.stdout.write(msg.as_string())
+ else:
+ sys.stdout.write(fp.read())
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/unshunt.py b/src/mailman/bin/unshunt.py
new file mode 100644
index 000000000..fc889377c
--- /dev/null
+++ b/src/mailman/bin/unshunt.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2002-2009 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/>.
+
+__metaclass__ = type
+__all__ = [
+ 'main',
+ ]
+
+
+import sys
+
+from mailman.config import config
+from mailman.i18n import _
+from mailman.options import Options
+
+
+
+def main():
+ options = Options()
+ options.initialize()
+
+ switchboard = config.switchboards['shunt']
+ switchboard.recover_backup_files()
+
+ for filebase in switchboard.files:
+ try:
+ msg, msgdata = switchboard.dequeue(filebase)
+ whichq = msgdata.get('whichq', 'in')
+ config.switchboards[whichq].enqueue(msg, msgdata)
+ except Exception, e:
+ # If there are any unshunting errors, log them and continue trying
+ # other shunted messages.
+ print >> sys.stderr, _(
+ 'Cannot unshunt message $filebase, skipping:\n$e')
+ else:
+ # Unlink the .bak file left by dequeue()
+ switchboard.finish(filebase)
diff --git a/src/mailman/bin/update.py b/src/mailman/bin/update.py
new file mode 100644
index 000000000..34ea6cda3
--- /dev/null
+++ b/src/mailman/bin/update.py
@@ -0,0 +1,660 @@
+# Copyright (C) 1998-2009 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 md5
+import sys
+import time
+import email
+import errno
+import shutil
+import cPickle
+import marshal
+import optparse
+
+from locknix.lockfile import TimeOutError
+
+from mailman import MailList
+from mailman import Message
+from mailman import Pending
+from mailman import Utils
+from mailman import version
+from mailman.MemberAdaptor import BYBOUNCE, ENABLED
+from mailman.OldStyleMemberships import OldStyleMemberships
+from mailman.Queue.Switchboard import Switchboard
+from mailman.configuration import config
+from mailman.i18n import _
+from mailman.initialize import initialize
+from mailman.utilities.filesystem import makedirs
+
+
+FRESH = 0
+NOTFRESH = -1
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=version.MAILMAN_VERSION,
+ usage=_("""\
+Perform all necessary upgrades.
+
+%prog [options]"""))
+ parser.add_option('-f', '--force',
+ default=False, action='store_true', help=_("""\
+Force running the upgrade procedures. Normally, if the version number of the
+installed Mailman matches the current version number (or a 'downgrade' is
+detected), nothing will be done."""))
+ 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 parser, opts, args
+
+
+
+def calcversions():
+ # Returns a tuple of (lastversion, thisversion). If the last version
+ # could not be determined, lastversion will be FRESH or NOTFRESH,
+ # depending on whether this installation appears to be fresh or not. The
+ # determining factor is whether there are files in the $var_prefix/logs
+ # subdir or not. The version numbers are HEX_VERSIONs.
+ #
+ # See if we stored the last updated version
+ lastversion = None
+ thisversion = version.HEX_VERSION
+ try:
+ fp = open(os.path.join(config.DATA_DIR, 'last_mailman_version'))
+ data = fp.read()
+ fp.close()
+ lastversion = int(data, 16)
+ except (IOError, ValueError):
+ pass
+ #
+ # try to figure out if this is a fresh install
+ if lastversion is None:
+ lastversion = FRESH
+ try:
+ if os.listdir(config.LOG_DIR):
+ lastversion = NOTFRESH
+ except OSError:
+ pass
+ return (lastversion, thisversion)
+
+
+
+def makeabs(relpath):
+ return os.path.join(config.PREFIX, relpath)
+
+
+def make_varabs(relpath):
+ return os.path.join(config.VAR_PREFIX, relpath)
+
+
+
+def move_language_templates(mlist):
+ listname = mlist.internal_name()
+ print _('Fixing language templates: $listname')
+ # Mailman 2.1 has a new cascading search for its templates, defined and
+ # described in Utils.py:maketext(). Putting templates in the top level
+ # templates/ subdir or the lists/<listname> subdir is deprecated and no
+ # longer searched..
+ #
+ # What this means is that most templates can live in the global templates/
+ # subdirectory, and only needs to be copied into the list-, vhost-, or
+ # site-specific language directories when needed.
+ #
+ # Also, by default all standard (i.e. English) templates must now live in
+ # the templates/en directory. This update cleans up all the templates,
+ # deleting more-specific duplicates (as calculated by md5 checksums) in
+ # favor of more-global locations.
+ #
+ # First, get rid of any lists/<list> template or lists/<list>/en template
+ # that is identical to the global templates/* default.
+ for gtemplate in os.listdir(os.path.join(config.TEMPLATE_DIR, 'en')):
+ # BAW: get rid of old templates, e.g. admlogin.txt and
+ # handle_opts.html
+ try:
+ fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate))
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ # No global template
+ continue
+ gcksum = md5.new(fp.read()).digest()
+ fp.close()
+ # Match against the lists/<list>/* template
+ try:
+ fp = open(os.path.join(mlist.fullpath(), gtemplate))
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ else:
+ tcksum = md5.new(fp.read()).digest()
+ fp.close()
+ if gcksum == tcksum:
+ os.unlink(os.path.join(mlist.fullpath(), gtemplate))
+ # Match against the lists/<list>/*.prev template
+ try:
+ fp = open(os.path.join(mlist.fullpath(), gtemplate + '.prev'))
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ else:
+ tcksum = md5.new(fp.read()).digest()
+ fp.close()
+ if gcksum == tcksum:
+ os.unlink(os.path.join(mlist.fullpath(), gtemplate + '.prev'))
+ # Match against the lists/<list>/en/* templates
+ try:
+ fp = open(os.path.join(mlist.fullpath(), 'en', gtemplate))
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ else:
+ tcksum = md5.new(fp.read()).digest()
+ fp.close()
+ if gcksum == tcksum:
+ os.unlink(os.path.join(mlist.fullpath(), 'en', gtemplate))
+ # Match against the templates/* template
+ try:
+ fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate))
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ else:
+ tcksum = md5.new(fp.read()).digest()
+ fp.close()
+ if gcksum == tcksum:
+ os.unlink(os.path.join(config.TEMPLATE_DIR, gtemplate))
+ # Match against the templates/*.prev template
+ try:
+ fp = open(os.path.join(config.TEMPLATE_DIR, gtemplate + '.prev'))
+ except IOError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ else:
+ tcksum = md5.new(fp.read()).digest()
+ fp.close()
+ if gcksum == tcksum:
+ os.unlink(os.path.join(config.TEMPLATE_DIR,
+ gtemplate + '.prev'))
+
+
+
+def situate_list(listname):
+ # This turns the directory called 'listname' into a directory called
+ # 'listname@domain'. Start by finding out what the domain should be.
+ # A list's domain is its email host.
+ mlist = MailList.MailList(listname, lock=False, check_version=False)
+ fullname = mlist.fqdn_listname
+ oldpath = os.path.join(config.VAR_PREFIX, 'lists', listname)
+ newpath = os.path.join(config.VAR_PREFIX, 'lists', fullname)
+ if os.path.exists(newpath):
+ print >> sys.stderr, _('WARNING: could not situate list: $listname')
+ else:
+ os.rename(oldpath, newpath)
+ print _('situated list $listname to $fullname')
+ return fullname
+
+
+
+def dolist(listname):
+ mlist = MailList.MailList(listname, lock=False)
+ try:
+ mlist.Lock(0.5)
+ except TimeOutError:
+ print >> sys.stderr, _(
+ 'WARNING: could not acquire lock for list: $listname')
+ return 1
+ # Sanity check the invariant that every BYBOUNCE disabled member must have
+ # bounce information. Some earlier betas broke this. BAW: we're
+ # submerging below the MemberAdaptor interface, so skip this if we're not
+ # using OldStyleMemberships.
+ if isinstance(mlist._memberadaptor, OldStyleMemberships):
+ noinfo = {}
+ for addr, (reason, when) in mlist.delivery_status.items():
+ if reason == BYBOUNCE and not mlist.bounce_info.has_key(addr):
+ noinfo[addr] = reason, when
+ # What to do about these folks with a BYBOUNCE delivery status and no
+ # bounce info? This number should be very small, and I think it's
+ # fine to simple re-enable them and let the bounce machinery
+ # re-disable them if necessary.
+ n = len(noinfo)
+ if n > 0:
+ print _(
+ 'Resetting $n BYBOUNCEs disabled addrs with no bounce info')
+ for addr in noinfo.keys():
+ mlist.setDeliveryStatus(addr, ENABLED)
+
+ mbox_dir = make_varabs('archives/private/%s.mbox' % (listname))
+ mbox_file = make_varabs('archives/private/%s.mbox/%s' % (listname,
+ listname))
+ o_pub_mbox_file = make_varabs('archives/public/%s' % (listname))
+ o_pri_mbox_file = make_varabs('archives/private/%s' % (listname))
+ html_dir = o_pri_mbox_file
+ o_html_dir = makeabs('public_html/archives/%s' % (listname))
+ # Make the mbox directory if it's not there.
+ if not os.path.exists(mbox_dir):
+ makedirs(mbox_dir)
+ else:
+ # This shouldn't happen, but hey, just in case
+ if not os.path.isdir(mbox_dir):
+ print _("""\
+For some reason, $mbox_dir exists as a file. This won't work with b6, so I'm
+renaming it to ${mbox_dir}.tmp and proceeding.""")
+ os.rename(mbox_dir, "%s.tmp" % (mbox_dir))
+ makedirs(mbox_dir)
+ # Move any existing mboxes around, but watch out for both a public and a
+ # private one existing
+ if os.path.isfile(o_pri_mbox_file) and os.path.isfile(o_pub_mbox_file):
+ if mlist.archive_private:
+ print _("""\
+
+$listname has both public and private mbox archives. Since this list
+currently uses private archiving, I'm installing the private mbox archive --
+$o_pri_mbox_file -- as the active archive, and renaming
+ $o_pub_mbox_file
+to
+ ${o_pub_mbox_file}.preb6
+
+You can integrate that into the archives if you want by using the 'arch'
+script.
+""") % (mlist._internal_name, o_pri_mbox_file, o_pub_mbox_file,
+ o_pub_mbox_file)
+ os.rename(o_pub_mbox_file, "%s.preb6" % (o_pub_mbox_file))
+ else:
+ print _("""\
+$mlist._internal_name has both public and private mbox archives. Since this
+list currently uses public archiving, I'm installing the public mbox file
+archive file ($o_pub_mbox_file) as the active one, and renaming
+$o_pri_mbox_file to ${o_pri_mbox_file}.preb6
+
+You can integrate that into the archives if you want by using the 'arch'
+script.
+""")
+ os.rename(o_pri_mbox_file, "%s.preb6" % (o_pri_mbox_file))
+ # Move private archive mbox there if it's around
+ # and take into account all sorts of absurdities
+ print _('- updating old private mbox file')
+ if os.path.exists(o_pri_mbox_file):
+ if os.path.isfile(o_pri_mbox_file):
+ os.rename(o_pri_mbox_file, mbox_file)
+ elif not os.path.isdir(o_pri_mbox_file):
+ newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \
+ % o_pri_mbox_file
+ os.rename(o_pri_mbox_file, newname)
+ print _("""\
+ unknown file in the way, moving
+ $o_pri_mbox_file
+ to
+ $newname""")
+ else:
+ # directory
+ print _("""\
+ looks like you have a really recent development installation...
+ you're either one brave soul, or you already ran me""")
+ # Move public archive mbox there if it's around
+ # and take into account all sorts of absurdities.
+ print _('- updating old public mbox file')
+ if os.path.exists(o_pub_mbox_file):
+ if os.path.isfile(o_pub_mbox_file):
+ os.rename(o_pub_mbox_file, mbox_file)
+ elif not os.path.isdir(o_pub_mbox_file):
+ newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \
+ % o_pub_mbox_file
+ os.rename(o_pub_mbox_file, newname)
+ print _("""\
+ unknown file in the way, moving
+ $o_pub_mbox_file
+ to
+ $newname""")
+ else: # directory
+ print _("""\
+ looks like you have a really recent development installation...
+ you're either one brave soul, or you already ran me""")
+ # Move the html archives there
+ if os.path.isdir(o_html_dir):
+ os.rename(o_html_dir, html_dir)
+ # chmod the html archives
+ os.chmod(html_dir, 02775)
+ # BAW: Is this still necessary?!
+ mlist.Save()
+ # Check to see if pre-b4 list-specific templates are around
+ # and move them to the new place if there's not already
+ # a new one there
+ tmpl_dir = os.path.join(config.PREFIX, "templates")
+ list_dir = os.path.join(config.PREFIX, "lists")
+ b4_tmpl_dir = os.path.join(tmpl_dir, mlist._internal_name)
+ new_tmpl_dir = os.path.join(list_dir, mlist._internal_name)
+ if os.path.exists(b4_tmpl_dir):
+ print _("""\
+- This list looks like it might have <= b4 list templates around""")
+ for f in os.listdir(b4_tmpl_dir):
+ o_tmpl = os.path.join(b4_tmpl_dir, f)
+ n_tmpl = os.path.join(new_tmpl_dir, f)
+ if os.path.exists(o_tmpl):
+ if not os.path.exists(n_tmpl):
+ os.rename(o_tmpl, n_tmpl)
+ print _('- moved $o_tmpl to $n_tmpl')
+ else:
+ print _("""\
+- both $o_tmpl and $n_tmpl exist, leaving untouched""")
+ else:
+ print _("""\
+- $o_tmpl doesn't exist, leaving untouched""")
+ # Move all the templates to the en language subdirectory as required for
+ # Mailman 2.1
+ move_language_templates(mlist)
+ # Avoid eating filehandles with the list lockfiles
+ mlist.Unlock()
+ return 0
+
+
+
+def archive_path_fixer(unused_arg, dir, files):
+ # Passed to os.path.walk to fix the perms on old html archives.
+ for f in files:
+ abs = os.path.join(dir, f)
+ if os.path.isdir(abs):
+ if f == "database":
+ os.chmod(abs, 02770)
+ else:
+ os.chmod(abs, 02775)
+ elif os.path.isfile(abs):
+ os.chmod(abs, 0664)
+
+
+def remove_old_sources(module):
+ # Also removes old directories.
+ src = '%s/%s' % (config.PREFIX, module)
+ pyc = src + "c"
+ if os.path.isdir(src):
+ print _('removing directory $src and everything underneath')
+ shutil.rmtree(src)
+ elif os.path.exists(src):
+ print _('removing $src')
+ try:
+ os.unlink(src)
+ except os.error, rest:
+ print _("Warning: couldn't remove $src -- $rest")
+ if module.endswith('.py') and os.path.exists(pyc):
+ try:
+ os.unlink(pyc)
+ except OSError, rest:
+ print _("couldn't remove old file $pyc -- $rest")
+
+
+
+def update_qfiles():
+ print _('updating old qfiles')
+ prefix = `time.time()` + '+'
+ # Be sure the qfiles/in directory exists (we don't really need the
+ # switchboard object, but it's convenient for creating the directory).
+ sb = Switchboard(config.INQUEUE_DIR)
+ for filename in os.listdir(config.QUEUE_DIR):
+ # Updating means just moving the .db and .msg files to qfiles/in where
+ # it should be dequeued, converted, and processed normally.
+ if os.path.splitext(filename) == '.msg':
+ oldmsgfile = os.path.join(config.QUEUE_DIR, filename)
+ newmsgfile = os.path.join(config.INQUEUE_DIR, prefix + filename)
+ os.rename(oldmsgfile, newmsgfile)
+ elif os.path.splitext(filename) == '.db':
+ olddbfile = os.path.join(config.QUEUE_DIR, filename)
+ newdbfile = os.path.join(config.INQUEUE_DIR, prefix + filename)
+ os.rename(olddbfile, newdbfile)
+ # Now update for the Mailman 2.1.5 qfile format. For every filebase in
+ # the qfiles/* directories that has both a .pck and a .db file, pull the
+ # data out and re-queue them.
+ for dirname in os.listdir(config.QUEUE_DIR):
+ dirpath = os.path.join(config.QUEUE_DIR, dirname)
+ if dirpath == config.BADQUEUE_DIR:
+ # The files in qfiles/bad can't possibly be pickles
+ continue
+ sb = Switchboard(dirpath)
+ try:
+ for filename in os.listdir(dirpath):
+ filepath = os.path.join(dirpath, filename)
+ filebase, ext = os.path.splitext(filepath)
+ # Handle the .db metadata files as part of the handling of the
+ # .pck or .msg message files.
+ if ext not in ('.pck', '.msg'):
+ continue
+ msg, data = dequeue(filebase)
+ if msg is not None and data is not None:
+ sb.enqueue(msg, data)
+ except EnvironmentError, e:
+ if e.errno <> errno.ENOTDIR:
+ raise
+ print _('Warning! Not a directory: $dirpath')
+
+
+
+# Implementations taken from the pre-2.1.5 Switchboard
+def ext_read(filename):
+ fp = open(filename)
+ d = marshal.load(fp)
+ # Update from version 2 files
+ if d.get('version', 0) == 2:
+ del d['filebase']
+ # Do the reverse conversion (repr -> float)
+ for attr in ['received_time']:
+ try:
+ sval = d[attr]
+ except KeyError:
+ pass
+ else:
+ # Do a safe eval by setting up a restricted execution
+ # environment. This may not be strictly necessary since we
+ # know they are floats, but it can't hurt.
+ d[attr] = eval(sval, {'__builtins__': {}})
+ fp.close()
+ return d
+
+
+def dequeue(filebase):
+ # Calculate the .db and .msg filenames from the given filebase.
+ msgfile = os.path.join(filebase + '.msg')
+ pckfile = os.path.join(filebase + '.pck')
+ dbfile = os.path.join(filebase + '.db')
+ # Now we are going to read the message and metadata for the given
+ # filebase. We want to read things in this order: first, the metadata
+ # file to find out whether the message is stored as a pickle or as
+ # plain text. Second, the actual message file. However, we want to
+ # first unlink the message file and then the .db file, because the
+ # qrunner only cues off of the .db file
+ msg = None
+ try:
+ data = ext_read(dbfile)
+ os.unlink(dbfile)
+ except EnvironmentError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ data = {}
+ # Between 2.1b4 and 2.1b5, the `rejection-notice' key in the metadata
+ # was renamed to `rejection_notice', since dashes in the keys are not
+ # supported in METAFMT_ASCII.
+ if data.has_key('rejection-notice'):
+ data['rejection_notice'] = data['rejection-notice']
+ del data['rejection-notice']
+ msgfp = None
+ try:
+ try:
+ msgfp = open(pckfile)
+ msg = cPickle.load(msgfp)
+ os.unlink(pckfile)
+ except EnvironmentError, e:
+ if e.errno <> errno.ENOENT: raise
+ msgfp = None
+ try:
+ msgfp = open(msgfile)
+ msg = email.message_from_file(msgfp, Message.Message)
+ os.unlink(msgfile)
+ except EnvironmentError, e:
+ if e.errno <> errno.ENOENT: raise
+ except (email.Errors.MessageParseError, ValueError), e:
+ # This message was unparsable, most likely because its
+ # MIME encapsulation was broken. For now, there's not
+ # much we can do about it.
+ print _('message is unparsable: $filebase')
+ msgfp.close()
+ msgfp = None
+ if config.QRUNNER_SAVE_BAD_MESSAGES:
+ # Cheapo way to ensure the directory exists w/ the
+ # proper permissions.
+ sb = Switchboard(config.BADQUEUE_DIR)
+ os.rename(msgfile, os.path.join(
+ config.BADQUEUE_DIR, filebase + '.txt'))
+ else:
+ os.unlink(msgfile)
+ msg = data = None
+ except EOFError:
+ # For some reason the pckfile was empty. Just delete it.
+ print _('Warning! Deleting empty .pck file: $pckfile')
+ os.unlink(pckfile)
+ finally:
+ if msgfp:
+ msgfp.close()
+ return msg, data
+
+
+
+def main():
+ parser, opts, args = parseargs()
+ initialize(opts.config)
+
+ # calculate the versions
+ lastversion, thisversion = calcversions()
+ hexlversion = hex(lastversion)
+ hextversion = hex(thisversion)
+ if lastversion == thisversion and not opts.force:
+ # nothing to do
+ print _('No updates are necessary.')
+ sys.exit(0)
+ if lastversion > thisversion and not opts.force:
+ print _("""\
+Downgrade detected, from version $hexlversion to version $hextversion
+This is probably not safe.
+Exiting.""")
+ sys.exit(1)
+ print _('Upgrading from version $hexlversion to $hextversion')
+ errors = 0
+ # get rid of old stuff
+ print _('getting rid of old source files')
+ for mod in ('mailman/Archiver.py', 'mailman/HyperArch.py',
+ 'mailman/HyperDatabase.py', 'mailman/pipermail.py',
+ 'mailman/smtplib.py', 'mailman/Cookie.py',
+ 'bin/update_to_10b6', 'scripts/mailcmd',
+ 'scripts/mailowner', 'mail/wrapper', 'mailman/pythonlib',
+ 'cgi-bin/archives', 'mailman/MailCommandHandler'):
+ remove_old_sources(mod)
+ if not config.list_manager.names:
+ print _('no lists == nothing to do, exiting')
+ return
+ # For people with web archiving, make sure the directories
+ # in the archiving are set with proper perms for b6.
+ if os.path.isdir("%s/public_html/archives" % config.PREFIX):
+ print _("""\
+fixing all the perms on your old html archives to work with b6
+If your archives are big, this could take a minute or two...""")
+ os.path.walk("%s/public_html/archives" % config.PREFIX,
+ archive_path_fixer, "")
+ print _('done')
+ for listname in config.list_manager.names:
+ # With 2.2.0a0, all list names grew an @domain suffix. If you find a
+ # list without that, move it now.
+ if not '@' in listname:
+ listname = situate_list(listname)
+ print _('Updating mailing list: $listname')
+ errors += dolist(listname)
+ print
+ print _('Updating Usenet watermarks')
+ wmfile = os.path.join(config.DATA_DIR, 'gate_watermarks')
+ try:
+ fp = open(wmfile)
+ except IOError:
+ print _('- nothing to update here')
+ else:
+ d = marshal.load(fp)
+ fp.close()
+ for listname in d.keys():
+ if listname not in listnames:
+ # this list no longer exists
+ continue
+ mlist = MailList.MailList(listname, lock=0)
+ try:
+ mlist.Lock(0.5)
+ except TimeOutError:
+ print >> sys.stderr, _(
+ 'WARNING: could not acquire lock for list: $listname')
+ errors = errors + 1
+ else:
+ # Pre 1.0b7 stored 0 in the gate_watermarks file to indicate
+ # that no gating had been done yet. Without coercing this to
+ # None, the list could now suddenly get flooded.
+ mlist.usenet_watermark = d[listname] or None
+ mlist.Save()
+ mlist.Unlock()
+ os.unlink(wmfile)
+ print _('- usenet watermarks updated and gate_watermarks removed')
+ # In Mailman 2.1, the qfiles directory has a different structure and a
+ # different content. Also, in Mailman 2.1.5 we collapsed the message
+ # files from separate .msg (pickled Message objects) and .db (marshalled
+ # dictionaries) to a shared .pck file containing two pickles.
+ update_qfiles()
+ # This warning was necessary for the upgrade from 1.0b9 to 1.0b10.
+ # There's no good way of figuring this out for releases prior to 2.0beta2
+ # :(
+ if lastversion == NOTFRESH:
+ print _("""
+
+NOTE NOTE NOTE NOTE NOTE
+
+ You are upgrading an existing Mailman installation, but I can't tell what
+ version you were previously running.
+
+ If you are upgrading from Mailman 1.0b9 or earlier you will need to
+ manually update your mailing lists. For each mailing list you need to
+ copy the file templates/options.html lists/<listname>/options.html.
+
+ However, if you have edited this file via the Web interface, you will have
+ to merge your changes into this file, otherwise you will lose your
+ changes.
+
+NOTE NOTE NOTE NOTE NOTE
+
+""")
+ if not errors:
+ # Record the version we just upgraded to
+ fp = open(os.path.join(config.DATA_DIR, 'last_mailman_version'), 'w')
+ fp.write(hex(config.HEX_VERSION) + '\n')
+ fp.close()
+ else:
+ lockdir = config.LOCK_DIR
+ print _('''\
+
+ERROR:
+
+The locks for some lists could not be acquired. This means that either
+Mailman was still active when you upgraded, or there were stale locks in the
+$lockdir directory.
+
+You must put Mailman into a quiescent state and remove all stale locks, then
+re-run "make update" manually. See the INSTALL and UPGRADE files for details.
+''')
diff --git a/src/mailman/bin/version.py b/src/mailman/bin/version.py
new file mode 100644
index 000000000..0fb2c5a5b
--- /dev/null
+++ b/src/mailman/bin/version.py
@@ -0,0 +1,46 @@
+# Copyright (C) 1998-2009 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 optparse
+
+from mailman import version
+from mailman.i18n import _
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=version.MAILMAN_VERSION,
+ usage=_("""\
+%prog
+
+Print the Mailman version and exit."""))
+ opts, args = parser.parse_args()
+ if args:
+ parser.error(_('Unexpected arguments'))
+ return parser, opts, args
+
+
+
+def main():
+ parser, opts, args = parseargs()
+ # Yes, this is kind of silly
+ print _('Using $version.MAILMAN_VERSION ($version.CODENAME)')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/bin/withlist.py b/src/mailman/bin/withlist.py
new file mode 100644
index 000000000..8f2d8a2b5
--- /dev/null
+++ b/src/mailman/bin/withlist.py
@@ -0,0 +1,220 @@
+# Copyright (C) 1998-2009 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 interact
+from mailman.config import config
+from mailman.core.initialize import initialize
+from mailman.i18n import _
+from mailman.version import MAILMAN_VERSION
+
+
+LAST_MLIST = None
+VERBOSE = True
+
+
+
+def do_list(listname, args, func):
+ global LAST_MLIST
+
+ if '@' not in listname:
+ listname += '@' + config.DEFAULT_EMAIL_HOST
+
+ # XXX FIXME Remove this when this script is converted to
+ # MultipleMailingListOptions.
+ listname = listname.decode(sys.getdefaultencoding())
+ mlist = config.db.list_manager.get(listname)
+ if mlist is None:
+ print >> sys.stderr, _('Unknown list: $listname')
+ else:
+ if VERBOSE:
+ print >> sys.stderr, _('Loaded list: $listname')
+ LAST_MLIST = mlist
+ # Try to import the module and run the callable.
+ if func:
+ return func(mlist, *args)
+ return None
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=MAILMAN_VERSION,
+ usage=_("""\
+%prog [options] listname [args ...]
+
+General framework for interacting with a mailing list object.
+
+There are two ways to use this script: interactively or programmatically.
+Using it interactively allows you to play with, examine and modify a
+IMailinglist object from Python's interactive interpreter. When running
+interactively, a IMailingList object called 'm' will be available in the
+global namespace.
+
+Programmatically, you can write a function to operate on a IMailingList
+object, and this script will take care of the housekeeping (see below for
+examples). In that case, the general usage syntax is:
+
+ % bin/withlist [options] listname [args ...]
+
+Here's an example of how to use the -r option. Say you have a file in the
+Mailman installation directory called 'listaddr.py', with the following
+two functions:
+
+ def listaddr(mlist):
+ print mlist.posting_address
+
+ def requestaddr(mlist):
+ print mlist.request_address
+
+Now, from the command line you can print the list's posting address by running
+the following from the command line:
+
+ % bin/withlist -r listaddr mylist
+ Loading list: mylist
+ Importing listaddr ...
+ Running listaddr.listaddr() ...
+ mylist@myhost.com
+
+And you can print the list's request address by running:
+
+ % bin/withlist -r listaddr.requestaddr mylist
+ Loading list: mylist
+ Importing listaddr ...
+ Running listaddr.requestaddr() ...
+ mylist-request@myhost.com
+
+As another example, say you wanted to change the password for a particular
+user on a particular list. You could put the following function in a file
+called 'changepw.py':
+
+ from mailman.errors import NotAMemberError
+
+ def changepw(mlist, addr, newpasswd):
+ try:
+ mlist.setMemberPassword(addr, newpasswd)
+ mlist.Save()
+ except NotAMemberError:
+ print 'No address matched:', addr
+
+and run this from the command line:
+
+ % bin/withlist -l -r changepw mylist somebody@somewhere.org foobar"""))
+ parser.add_option('-i', '--interactive',
+ default=None, action='store_true', help=_("""\
+Leaves you at an interactive prompt after all other processing is complete.
+This is the default unless the -r option is given."""))
+ parser.add_option('-r', '--run',
+ type='string', help=_("""\
+This can be used to run a script with the opened IMailingList object. This
+works by attempting to import'module' (which must be in the directory
+containing withlist, or already be accessible on your sys.path), and then
+calling 'callable' from the module. callable can be a class or function; it
+is called with the IMailingList object as the first argument. If additional
+args are given on the command line, they are passed as subsequent positional
+args to the callable.
+
+Note that 'module.' is optional; if it is omitted then a module with the name
+'callable' will be imported.
+
+The global variable 'r' will be set to the results of this call."""))
+ parser.add_option('-a', '--all',
+ default=False, action='store_true', help=_("""\
+This option only works with the -r option. Use this if you want to execute
+the script on all mailing lists. When you use -a you should not include a
+listname argument on the command line. The variable 'r' will be a list of all
+the results."""))
+ parser.add_option('-q', '--quiet',
+ default=False, action='store_true',
+ help=_('Suppress all status messages.'))
+ parser.add_option('-C', '--config',
+ help=_('Alternative configuration file to use'))
+ opts, args = parser.parse_args()
+ return parser, opts, args
+
+
+
+def main():
+ global VERBOSE
+
+ parser, opts, args = parseargs()
+ config_file = (os.getenv('MAILMAN_CONFIG_FILE')
+ if opts.config is None
+ else opts.config)
+ initialize(config_file, not opts.quiet)
+
+ VERBOSE = not opts.quiet
+ # The default for interact is true unless -r was given
+ if opts.interactive is None:
+ if not opts.run:
+ opts.interactive = True
+ else:
+ opts.interactive = False
+
+ dolist = True
+ if len(args) < 1 and not opts.all:
+ warning = _('No list name supplied.')
+ if opts.interactive:
+ # Let them keep going
+ print >> sys.stderr, warning
+ dolist = False
+ else:
+ parser.error(warning)
+
+ if opts.all and not opts.run:
+ parser.error(_('--all requires --run'))
+
+ # Try to import the module for the callable
+ func = None
+ if opts.run:
+ i = opts.run.rfind('.')
+ if i < 0:
+ module = opts.run
+ callable = opts.run
+ else:
+ module = opts.run[:i]
+ callable = opts.run[i+1:]
+ if VERBOSE:
+ print >> sys.stderr, _('Importing $module ...')
+ __import__(module)
+ mod = sys.modules[module]
+ if VERBOSE:
+ print >> sys.stderr, _('Running ${module}.${callable}() ...')
+ func = getattr(mod, callable)
+
+ r = None
+ if opts.all:
+ r = [do_list(listname, args, func)
+ for listname in config.list_manager.names]
+ elif dolist:
+ listname = args.pop(0).lower().strip()
+ r = do_list(listname, args, func)
+
+ # Now go to interactive mode, perhaps
+ if opts.interactive:
+ if dolist:
+ banner = _(
+ "The variable 'm' is the $listname mailing list")
+ else:
+ banner = interact.DEFAULT_BANNER
+ overrides = dict(m=LAST_MLIST, r=r,
+ commit=config.db.commit,
+ abort=config.db.abort,
+ config=config)
+ interact.interact(upframe=False, banner=banner, overrides=overrides)