aboutsummaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
author <>2003-01-02 05:25:50 +0000
committer <>2003-01-02 05:25:50 +0000
commitb132a73f15e432eaf43310fce9196ca0c0651465 (patch)
treec15f816ba7c4de99fef510e3bd75af0890d47441 /bin
downloadmailman2-b132a73f15e432eaf43310fce9196ca0c0651465.tar.gz
mailman2-b132a73f15e432eaf43310fce9196ca0c0651465.tar.xz
mailman2-b132a73f15e432eaf43310fce9196ca0c0651465.zip
This commit was manufactured by cvs2svn to create branch
'Release_2_1-maint'.
Diffstat (limited to 'bin')
-rw-r--r--bin/.cvsignore2
-rw-r--r--bin/Makefile.in78
-rwxr-xr-xbin/add_members297
-rw-r--r--bin/arch187
-rw-r--r--bin/b4b5-archfix96
-rw-r--r--bin/change_pw209
-rwxr-xr-xbin/check_db153
-rwxr-xr-xbin/check_perms362
-rw-r--r--bin/cleanarch165
-rwxr-xr-xbin/clone_member219
-rw-r--r--bin/config_list339
-rw-r--r--bin/convert.py44
-rw-r--r--bin/dumpdb134
-rwxr-xr-xbin/find_member184
-rw-r--r--bin/fix_url.py92
-rw-r--r--bin/genaliases102
-rw-r--r--bin/inject107
-rw-r--r--bin/list_admins101
-rw-r--r--bin/list_lists122
-rwxr-xr-xbin/list_members232
-rw-r--r--bin/list_owners120
-rw-r--r--bin/mailmanctl524
-rwxr-xr-xbin/mmsitepass105
-rwxr-xr-xbin/newlist219
-rwxr-xr-xbin/pygettext.py545
-rw-r--r--bin/qrunner270
-rwxr-xr-xbin/remove_members179
-rwxr-xr-xbin/rmlist138
-rwxr-xr-xbin/sync_members286
-rwxr-xr-xbin/transcheck405
-rw-r--r--bin/unshunt87
-rwxr-xr-xbin/update588
-rw-r--r--bin/version26
-rw-r--r--bin/withlist275
34 files changed, 6992 insertions, 0 deletions
diff --git a/bin/.cvsignore b/bin/.cvsignore
new file mode 100644
index 00000000..7bda5c8c
--- /dev/null
+++ b/bin/.cvsignore
@@ -0,0 +1,2 @@
+.cvsignore
+Makefile
diff --git a/bin/Makefile.in b/bin/Makefile.in
new file mode 100644
index 00000000..a406ca46
--- /dev/null
+++ b/bin/Makefile.in
@@ -0,0 +1,78 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+MAILDIR= $(exec_prefix)/mail
+SCRIPTSDIR= $(prefix)/bin
+
+SHELL= /bin/sh
+
+SCRIPTS= mmsitepass newlist rmlist add_members \
+ list_members remove_members clone_member update arch \
+ sync_members check_db withlist check_perms find_member \
+ version config_list list_lists dumpdb cleanarch \
+ list_admins genaliases change_pw mailmanctl qrunner inject \
+ unshunt fix_url.py convert.py transcheck b4b5-archfix \
+ list_owners
+
+BUILDDIR= ../build/bin
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+
+# Rules
+
+all:
+
+install:
+ for f in $(SCRIPTS); \
+ do \
+ $(INSTALL) -m $(EXEMODE) $(BUILDDIR)/$$f $(SCRIPTSDIR); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm Makefile
diff --git a/bin/add_members b/bin/add_members
new file mode 100755
index 00000000..ad4f43b8
--- /dev/null
+++ b/bin/add_members
@@ -0,0 +1,297 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# argv[1] should be the name of the list.
+# argv[2] should be the list of non-digested users.
+# argv[3] should be the list of digested users.
+
+# Make sure that the list of email addresses doesn't contain any comments,
+# like majordomo may throw in. For now, you just have to remove them manually.
+
+"""Add members to a list from the command line.
+
+Usage:
+ add_members [options] listname
+
+Options:
+
+ --regular-members-file=file
+ -r file
+ 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. Note that
+ -n/--non-digest-members-file are deprecated synonyms for this option.
+
+ --digest-members-file=file
+ -d=file
+ Similar to above, but these people become digest members.
+
+ --changes-msg=<y|n>
+ -c <y|n>
+ Set whether or not to send the list members the `there's going to be
+ big changes to your list' message. defaults to no.
+
+ --welcome-msg=<y|n>
+ -w <y|n>
+ Set whether or not to send the list members a welcome message,
+ overriding whatever the list's `send_welcome_msg' setting is.
+
+ --admin-notify=<y|n>
+ -a <y|n>
+ 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.
+
+ --help
+ -h
+ Print this help message and exit.
+
+ listname
+ The name of the Mailman list you are adding members to. It must
+ already exist.
+
+You must supply at least one of -r and -d options. At most one of the
+files can be `-'.
+"""
+
+import sys
+import os
+import getopt
+from cStringIO import StringIO
+
+import paths
+# Import this /after/ paths so that the sys.path is properly hacked
+from email.Utils import parseaddr
+
+from Mailman import MailList
+from Mailman import Utils
+from Mailman import Message
+from Mailman import Errors
+from Mailman import mm_cfg
+from Mailman import i18n
+
+_ = i18n._
+
+
+
+def usage(status, msg=''):
+ if status:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(status)
+
+
+
+def readfile(filename):
+ if filename == '-':
+ fp = sys.stdin
+ closep = 0
+ else:
+ fp = open(filename)
+ closep = 1
+ # strip all the lines of whitespace and discard blank lines
+ lines = filter(None, [line.strip() for line in fp.readlines()])
+ if closep:
+ fp.close()
+ return lines
+
+
+
+def SendExplanation(mlist, users):
+ listname = mlist.real_name
+ listhost = mlist.host_name
+ d = {'listname' : listname,
+ 'listhost' : listhost,
+ 'listaddr' : mlist.GetListEmail(),
+ 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1),
+ 'requestaddr' : mlist.GetRequestEmail(),
+ 'adminaddr' : mlist.GetOwnerEmail(),
+ 'version' : mm_cfg.VERSION,
+ }
+ otrans = i18n.get_translation()
+ i18n.set_language(mlist.preferred_language)
+ try:
+ text = Utils.maketext('convert.txt', d)
+ subject = _('Big change in %(listname)s@%(listhost)s mailing list')
+ msg = Message.UserNotification(users, mlist.GetBouncesEmail(),
+ subject, text,
+ mlist.preferred_language)
+ finally:
+ i18n.set_translation(otrans)
+ msg.send(mlist)
+
+
+
+class Tee:
+ def __init__(self, outfp):
+ self.__outfp = outfp
+
+ def write(self, msg):
+ sys.stdout.write(msg)
+ self.__outfp.write(msg)
+
+
+class UserDesc: pass
+
+
+
+def addall(mlist, members, digest, ack, outfp):
+ tee = Tee(outfp)
+ for member in members:
+ userdesc = UserDesc()
+ userdesc.fullname, userdesc.address = parseaddr(member)
+ userdesc.digest = digest
+
+ try:
+ mlist.ApprovedAddMember(userdesc, ack, 0)
+ except Errors.MMAlreadyAMember:
+ print >> tee, _('Already a member: %(member)s')
+ except Errors.MMBadEmailError:
+ if userdesc.address == '':
+ print >> tee, _('Bad/Invalid email address: blank line')
+ else:
+ print >> tee, _('Bad/Invalid email address: %(member)s')
+ except Errors.MMHostileAddress:
+ print >> tee, _('Hostile address (illegal characters): %(member)s')
+ else:
+ print >> tee, _('Subscribed: %(member)s')
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:],
+ 'a:n:r:d:c:w:h',
+ ['admin-notify=',
+ 'regular-members-file=',
+ 'non-digest-members-file=',
+ 'digest-members-file=',
+ 'changes-msg=',
+ 'welcome-msg=',
+ 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ if len(args) <> 1:
+ usage(1)
+
+ listname = args[0].lower().strip()
+ nfile = None
+ dfile = None
+ send_changes_msg = 0
+ send_welcome_msg = None
+ admin_notif = None
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-d', '--digest-members-file'):
+ dfile = arg
+ # Deprecate -/--non-digest-members-file or consistency with
+ # list_members
+ elif opt in ('-r', '--regular-members-file'):
+ nfile = arg
+ elif opt in ('-n', '--non-digest-members-file'):
+ nfile = arg
+ # I don't think we need to use the warnings module here.
+ print >> sys.stderr, 'option', opt, \
+ 'is deprecated, use -r/--regular-members-file'
+ elif opt in ('-c', '--changes-msg'):
+ if arg.lower()[0] == 'y':
+ send_changes_msg = 1
+ elif arg.lower()[0] == 'n':
+ send_changes_msg = 0
+ else:
+ usage(1, _('Bad argument to -c/--changes-msg: %(arg)s'))
+ elif opt in ('-w', '--welcome-msg'):
+ if arg.lower()[0] == 'y':
+ send_welcome_msg = 1
+ elif arg.lower()[0] == 'n':
+ send_welcome_msg = 0
+ else:
+ usage(1, _('Bad argument to -w/--welcome-msg: %(arg)s'))
+ elif opt in ('-a', '--admin-notify'):
+ if arg.lower()[0] == 'y':
+ admin_notif = 1
+ elif arg.lower()[0] == 'n':
+ admin_notif = 0
+ else:
+ usage(1, _('Bad argument to -a/--admin-notify: %(arg)s'))
+
+ if dfile is None and nfile is None:
+ usage(1)
+
+ if dfile == "-" and nfile == "-":
+ usage(1, _('Cannot read both digest and normal members '
+ 'from standard input.'))
+
+ try:
+ mlist = MailList.MailList(listname)
+ except Errors.MMUnknownListError:
+ usage(1, _('No such list: %(listname)s'))
+
+ # Set up defaults
+ if send_welcome_msg is None:
+ send_welcome_msg = mlist.send_welcome_msg
+ if admin_notif is None:
+ admin_notif = mlist.admin_notify_mchanges
+
+ otrans = i18n.get_translation()
+ # Read the regular and digest member files
+ try:
+ dmembers = []
+ if dfile:
+ dmembers = readfile(dfile)
+
+ nmembers = []
+ if nfile:
+ nmembers = readfile(nfile)
+
+ if not dmembers and not nmembers:
+ usage(0, _('Nothing to do.'))
+
+ s = StringIO()
+ i18n.set_language(mlist.preferred_language)
+ if nmembers:
+ addall(mlist, nmembers, 0, send_welcome_msg, s)
+
+ if dmembers:
+ addall(mlist, dmembers, 1, send_welcome_msg, s)
+
+ if admin_notif:
+ realname = mlist.real_name
+ subject = _('%(realname)s subscription notification')
+ msg = Message.UserNotification(
+ mlist.owner, Utils.get_site_email(), subject, s.getvalue(),
+ mlist.preferred_language)
+ msg.send(mlist)
+
+ if send_changes_msg:
+ SendExplanation(mlist, nmembers + dmembers)
+
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+ i18n.set_translation(otrans)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/arch b/bin/arch
new file mode 100644
index 00000000..be430f68
--- /dev/null
+++ b/bin/arch
@@ -0,0 +1,187 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""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.
+
+Usage: %(PROGRAM)s [options] <listname> [<mbox>]
+
+Where options are:
+ -h / --help
+ Print this help message and exit.
+
+ -q / --quiet
+ Make the archiver output less verbose.
+
+ --wipe
+ First wipe out the original archive before regenerating. You usually
+ want to specify this argument unless you're generating the archive in
+ chunks.
+
+ -s N
+ --start=N
+ Start indexing at article N, where article 0 is the first in the mbox.
+ Defaults to 0.
+
+ -e M
+ --end=M
+ 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.
+
+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.
+"""
+
+import os
+import sys
+import getopt
+import shutil
+
+import paths
+from Mailman import mm_cfg
+from Mailman import Errors
+
+from Mailman.MailList import MailList
+from Mailman.Archiver.HyperArch import HyperArchive
+from Mailman.LockFile import LockFile
+from Mailman import i18n
+
+_ = i18n._
+
+PROGRAM = sys.argv[0]
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ # get command line arguments
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'hs:e:q',
+ ['help', 'start', 'end', 'quiet', 'wipe'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ start = None
+ end = None
+ verbose = 1
+ wipe = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-s', '--start'):
+ try:
+ start = int(arg)
+ except ValueError:
+ usage(1)
+ elif opt in ('-e', '--end'):
+ try:
+ end = int(arg)
+ except ValueError:
+ usage(1)
+ elif opt in ('-q', '--quiet'):
+ verbose = 0
+ elif opt == '--wipe':
+ wipe = 1
+
+ # grok arguments
+ if len(args) < 1:
+ usage(1, _('listname is required'))
+ listname = args[0].lower().strip()
+
+ if len(args) < 2:
+ mbox = None
+ else:
+ mbox = args[1]
+
+ if len(args) > 2:
+ usage(1)
+
+ # open the mailing list object
+ mlist = None
+ lock = None
+ try:
+ try:
+ mlist = MailList(listname)
+ except Errors.MMListError, e:
+ usage(2, _('No such list "%(listname)s"\n%(e)s'))
+ 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 glomming it. we pick a suitably
+ # long period of time for the lock lifetime, however we really don't
+ # know how long it will take.
+ #
+ # XXX: processUnixMailbox() should refresh the lock.
+ #
+ # XXX: this may not be necessary because I think we lay claim to the
+ # list lock up above, although that may be too short to be of use (and
+ # maybe we don't really want to lock the list anyway).
+ #
+ lockfile = os.path.join(mm_cfg.LOCK_DIR, mlist._internal_name) + \
+ '.archiver.lock'
+ # set the lock lifetime to 3 hours. XXX is this reasonable???
+ lock = LockFile(lockfile, lifetime=3*60*60)
+ lock.lock()
+ # Maybe wipe the old archives
+ if wipe:
+ shutil.rmtree(mlist.archive_dir())
+ try:
+ fp = open(mbox)
+ except IOError, msg:
+ usage(3, _('Cannot open mbox file %(mbox)s: %(msg)s'))
+
+ archiver = HyperArchive(mlist)
+ archiver.VERBOSE = verbose
+ try:
+ archiver.processUnixMailbox(fp, start, end)
+ finally:
+ archiver.close()
+ fp.close()
+ finally:
+ if lock:
+ lock.unlock()
+ if mlist:
+ mlist.Unlock()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/b4b5-archfix b/bin/b4b5-archfix
new file mode 100644
index 00000000..0ae66c9d
--- /dev/null
+++ b/bin/b4b5-archfix
@@ -0,0 +1,96 @@
+#! @PYTHON@
+#
+# Copyright (C) 2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Fix the MM2.1b4 archives.
+
+Usage: %(PROGRAM)s [options] file ...
+
+Where options are:
+ -h / --help
+ Print this help message and exit.
+
+Only use this to `fix' some archive database files that may have gotten
+written in Mailman 2.1b4 with some bogus data. Use like this from your
+$PREFIX directory
+
+%% %(PROGRAM)s `grep -l _mlist archives/private/*/database/*-article`
+
+(note the backquotes are required)
+
+You will need to run `bin/check_perms -f' after running this script.
+"""
+# This script is provided for convenience purposes only. It isn't supported.
+
+import os
+import sys
+import getopt
+import marshal
+import cPickle as pickle
+
+# Required to get the right classes for unpickling
+import paths
+from Mailman.i18n import _
+
+PROGRAM = sys.argv[0]
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ # get command line arguments
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'h', ['help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+
+ for filename in args:
+ print 'processing:', filename
+ fp = open(filename, 'rb')
+ d = marshal.load(fp)
+ fp.close()
+ newd = {}
+ for key, pckstr in d.items():
+ article = pickle.loads(pckstr)
+ newd[key] = pickle.dumps(article)
+ fp = open(filename + '.tmp', 'wb')
+ marshal.dump(newd, fp)
+ fp.close()
+ os.rename(filename, filename + '.bak')
+ os.rename(filename + '.tmp', filename)
+
+ print 'You should now run "bin/check_perms -f"'
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/change_pw b/bin/change_pw
new file mode 100644
index 00000000..0e365e08
--- /dev/null
+++ b/bin/change_pw
@@ -0,0 +1,209 @@
+#! @PYTHON@
+#
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Change a list's password.
+
+Prior to Mailman 2.1, list passwords were kept in crypt'd format -- usually.
+Some Python installations didn't have the crypt module available, so they'd
+fall back to md5. Then suddenly the Python installation might grow a crypt
+module and all list passwords would be broken.
+
+In Mailman 2.1, all list and site passwords are stored in SHA1 hexdigest
+form. This breaks list passwords for all existing pre-Mailman 2.1 lists, and
+since those passwords aren't stored anywhere in plain text, they cannot be
+retrieved and updated.
+
+Thus, this script generates new passwords for a list, and optionally sends it
+to all the owners of the list.
+
+Usage: change_pw [options]
+
+Options:
+
+ --all / -a
+ Change the password for all lists.
+
+ --domain=domain
+ -d domain
+ Change the password for all lists in the virtual domain `domain'. It
+ is okay to give multiple -d options.
+
+ --listname=listname
+ -l listname
+ Change the password only for the named list. It is okay to give
+ multiple -l options.
+
+ --password=newpassword
+ -p newpassword
+ Use the supplied plain text password `newpassword' as the new password
+ for any lists that are being changed (as specified by the -a, -d, and
+ -l options). If not given, lists will be assigned a randomly
+ generated new password.
+
+ --quiet / -q
+ Don't notify list owners of the new password. You'll have to have
+ some other way of letting the list owners know the new password
+ (presumably out-of-band).
+
+ --help / -h
+ Print this help message and exit.
+"""
+
+import sys
+import sha
+import getopt
+
+import paths
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import Message
+from Mailman import i18n
+
+_ = i18n._
+
+SPACE = ' '
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+_listcache = {}
+
+def openlist(listname):
+ missing = []
+ mlist = _listcache.get(listname, missing)
+ if mlist is missing:
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ usage(1, _('No such list "%(listname)s"\n%(e)s'))
+ _listcache[listname] = mlist
+ return mlist
+
+
+
+def main():
+ # Parse options
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'ad:l:p:qh',
+ ['all', 'domain=', 'listname=', 'password=', 'quiet', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ # defaults
+ listnames = {}
+ domains = {}
+ password = None
+ quiet = 0
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-a', '--all'):
+ for name in Utils.list_names():
+ listnames[name] = 1
+ elif opt in ('-d', '--domain'):
+ domains[arg] = 1
+ elif opt in ('-l', '--listname'):
+ listnames[arg] = 1
+ elif opt in ('-p', '--password'):
+ password = arg
+ elif opt in ('-q', '--quiet'):
+ quiet = 1
+
+ if args:
+ strargs = SPACE.join(args)
+ usage(1, _('Bad arguments: %(strargs)s'))
+
+ if password is not None:
+ if not password:
+ usage(1, _('Empty list passwords are not allowed'))
+ shapassword = sha.new(password).hexdigest()
+
+ if domains:
+ for name in Utils.list_names():
+ mlist = openlist(name)
+ if domains.has_key(mlist.host_name):
+ listnames[name] = 1
+
+ if not listnames:
+ print >> sys.stderr, _('Nothing to do.')
+ sys.exit(0)
+
+ # Set the password on the lists
+ for listname in listnames.keys():
+ mlist = openlist(listname)
+ mlist.Lock()
+ try:
+ if password is None:
+ randompw = Utils.MakeRandomPassword(8)
+ shapassword = sha.new(randompw).hexdigest()
+ notifypassword = randompw
+ else:
+ notifypassword = password
+
+ mlist.password = shapassword
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+ # Notification
+ print _('New %(listname)s password: %(notifypassword)s')
+ if not quiet:
+ otrans = i18n.get_translation()
+ i18n.set_language(mlist.preferred_language)
+ try:
+ hostname = mlist.host_name
+ adminurl = mlist.GetScriptURL('admin', absolute=1)
+ msg = Message.UserNotification(
+ mlist.owner[:], Utils.get_site_email(),
+ _('Your new %(listname)s list password'),
+ _('''\
+The site administrator at %(hostname)s has changed the password for your
+mailing list %(listname)s. It is now
+
+ %(notifypassword)s
+
+Please be sure to use this for all future list administration. You may want
+to log in now to your list and change the password to something more to your
+liking. Visit your list admin page at
+
+ %(adminurl)s
+'''),
+ mlist.preferred_language)
+ finally:
+ i18n.set_translation(otrans)
+ msg.send(mlist)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/check_db b/bin/check_db
new file mode 100755
index 00000000..c869b194
--- /dev/null
+++ b/bin/check_db
@@ -0,0 +1,153 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Check a list's config database file for integrity.
+
+All of the following files are checked:
+
+ config.pck
+ config.pck.last
+ config.db
+ config.db.last
+ config.safety
+
+It's okay if any of these are missing. config.pck and config.pck.last are
+pickled versions of the config database file for 2.1a3 and beyond. config.db
+and config.db.last are used in all earlier versions, and these are Python
+marshals. config.safety is a pickle written by 2.1a3 and beyond when the
+primary config.pck file could not be read.
+
+Usage: %(PROGRAM)s [options] [listname [listname ...]]
+
+Options:
+
+ --all / -a
+ Check the databases for all lists. Otherwise only the lists named on
+ the command line are checked.
+
+ --verbose / -v
+ Verbose output. The state of every tested file is printed.
+ Otherwise only corrupt files are displayed.
+
+ --help / -h
+ Print this text and exit.
+"""
+
+import sys
+import os
+import errno
+import getopt
+import marshal
+import cPickle
+
+import paths
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman.MailList import MailList
+from Mailman.i18n import _
+
+PROGRAM = sys.argv[0]
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def testfile(dbfile):
+ if dbfile.endswith('.db') or dbfile.endswith('.db.last'):
+ loadfunc = marshal.load
+ elif dbfile.endswith('.pck') or dbfile.endswith('.pck.last'):
+ loadfunc = cPickle.load
+ else:
+ assert 0
+ fp = open(dbfile)
+ try:
+ loadfunc(fp)
+ finally:
+ fp.close()
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'ahv',
+ ['all', 'verbose', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ verbose = 0
+ listnames = args
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-v', '--verbose'):
+ verbose = 1
+ elif opt in ('-a', '--all'):
+ listnames = Utils.list_names()
+
+ listnames = [n.lower().strip() for n in listnames]
+ if not listnames:
+ print _('Nothing to do.')
+ sys.exit(0)
+
+ for listname in listnames:
+ if not Utils.list_exists(listname):
+ print _('No list named:'), listname
+ continue
+ mlist = MailList(listname, lock=0)
+ pfile = os.path.join(mlist.fullpath(), 'config.pck')
+ plast = pfile + '.last'
+ dfile = os.path.join(mlist.fullpath(), 'config.db')
+ dlast = dfile + '.last'
+
+ if verbose:
+ print _('List:'), listname
+
+ for file in (pfile, plast, dfile, dlast):
+ status = 0
+ try:
+ testfile(file)
+ except IOError, e:
+ # Don't report ENOENT unless we're in verbose mode
+ if verbose or e.errno <> errno.ENOENT:
+ status = e
+ except Exception, e:
+ status = e
+ # Report errors
+ if status:
+ if isinstance(status, EnvironmentError):
+ # This already includes the file name
+ print ' ', status
+ else:
+ print ' %s: %s' % (file, status)
+ elif verbose:
+ print _(' %(file)s: okay')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/check_perms b/bin/check_perms
new file mode 100755
index 00000000..44fbe547
--- /dev/null
+++ b/bin/check_perms
@@ -0,0 +1,362 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Check the permissions for the Mailman installation.
+
+Usage: %(PROGRAM)s [-f] [-v] [-h]
+
+With no arguments, just check and report all the files that have bogus
+permissions or group ownership. With -f (and run as root), fix all the
+permission problems found. With -v be verbose.
+
+"""
+
+import sys
+import os
+import errno
+import getopt
+import pwd
+import grp
+from stat import *
+
+try:
+ import paths
+except ImportError:
+ print '''Could not import paths!
+
+This probably means that you are trying to run check_perms from the source
+directory. You must run this from the installation directory instead.
+'''
+ raise
+from Mailman import mm_cfg
+from Mailman.mm_cfg import MAILMAN_USER, MAILMAN_GROUP
+from Mailman.i18n import _
+
+# Let KeyErrors percolate
+MAILMAN_GID = grp.getgrnam(MAILMAN_GROUP)[2]
+MAILMAN_UID = pwd.getpwnam(MAILMAN_USER)[2]
+
+PROGRAM = sys.argv[0]
+
+# Gotta check the archives/private/*/database/* files
+
+
+
+class State:
+ FIX = 0
+ VERBOSE = 0
+ 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
+
+
+
+def statmode(path):
+ return os.stat(path)[ST_MODE]
+
+def statgidmode(path):
+ stat = os.stat(path)
+ return stat[ST_MODE], stat[ST_GID]
+
+def checkwalk(arg, dirname, names):
+ for name in names:
+ path = os.path.join(dirname, name)
+ if arg.VERBOSE:
+ print _(' checking gid and mode for %(path)s')
+ try:
+ mode, gid = statgidmode(path)
+ except OSError, e:
+ if e.errno <> errno.ENOENT: raise
+ continue
+ if gid <> MAILMAN_GID:
+ try:
+ groupname = grp.getgrgid(gid)[0]
+ except KeyError:
+ groupname = '<anon gid %d>' % gid
+ arg.ERRORS += 1
+ print _('%(path)s bad group (has: %(groupname)s, '
+ 'expected %(MAILMAN_GROUP)s)'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chown(path, -1, MAILMAN_GID)
+ else:
+ print
+ # all directories must be at least rwxrwsr-x. Don't check the private
+ # archive directory or database directory themselves since these are
+ # checked in checkarchives() and checkarchivedbs() below.
+ private = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR
+ if path == private or (os.path.commonprefix((path, private)) == private
+ and os.path.split(path)[1] == 'database'):
+ continue
+ # The directories under qfiles should have a more limited permission
+ if os.path.commonprefix((path, mm_cfg.QUEUE_DIR)) == mm_cfg.QUEUE_DIR:
+ targetperms = QFILEPERMS
+ octperms = oct(targetperms)
+ else:
+ targetperms = DIRPERMS
+ octperms = oct(targetperms)
+ if S_ISDIR(mode) and (mode & targetperms) <> targetperms:
+ arg.ERRORS += 1
+ print _('directory permissions must be %(octperms)s: %(path)s'),
+ 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)s: %(path)s'),
+ 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)s: %(path)s'),
+ arg.ERRORS += 1
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | ARTICLEFILEPERMS)
+ else:
+ print
+
+def checkall():
+ # first check PREFIX
+ if STATE.VERBOSE:
+ prefix = mm_cfg.PREFIX
+ print _('checking mode for %(prefix)s')
+ dirs = {}
+ for d in (mm_cfg.PREFIX, mm_cfg.EXEC_PREFIX, mm_cfg.VAR_PREFIX):
+ dirs[d] = 1
+ for d in dirs.keys():
+ mode = statmode(d)
+ if (mode & DIRPERMS) <> DIRPERMS:
+ STATE.ERRORS += 1
+ print _('directory must be at least 02775: %(d)s'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(d, mode | DIRPERMS)
+ else:
+ print
+ # check all subdirs
+ os.path.walk(d, checkwalk, STATE)
+
+
+def checkarchives():
+ private = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR
+ if STATE.VERBOSE:
+ print _('checking perms on %(private)s')
+ # private archives must not be other readable
+ mode = statmode(private)
+ if mode & S_IROTH:
+ STATE.ERRORS += 1
+ print _('%(private)s must not be other-readable'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(private, mode & ~S_IROTH)
+ else:
+ print
+
+
+MBOXPERMS = S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR
+
+
+def checkmboxfile(mboxdir):
+ absdir = os.path.join(mm_cfg.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(mm_cfg.PRIVATE_ARCHIVE_FILE_DIR):
+ if dir.endswith('.mbox'):
+ checkmboxfile(dir)
+ dbdir = os.path.join(mm_cfg.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)s "other" perms must be 000'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(dbdir, mode & ~S_IRWXO)
+ else:
+ print
+
+
+def checkcgi():
+ cgidir = os.path.join(mm_cfg.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)s')
+ mode = statmode(path)
+ if mode & S_IXGRP and not mode & S_ISGID:
+ STATE.ERRORS += 1
+ print _('%(path)s must be set-gid'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | S_ISGID)
+ else:
+ print
+
+def checkmail():
+ wrapper = os.path.join(mm_cfg.WRAPPER_DIR, 'mailman')
+ if STATE.VERBOSE:
+ print _('checking set-gid for %(wrapper)s')
+ mode = statmode(wrapper)
+ if not mode & S_ISGID:
+ STATE.ERRORS += 1
+ print _('%(wrapper)s must be set-gid'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(wrapper, mode | S_ISGID)
+
+def checkadminpw():
+ for pwfile in (os.path.join(mm_cfg.DATA_DIR, 'adm.pw'),
+ os.path.join(mm_cfg.DATA_DIR, 'creator.pw')):
+ targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP
+ if STATE.VERBOSE:
+ print _('checking permissions on %(pwfile)s')
+ 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)s permissions must be exactly 0640 '
+ '(got %(octmode)s)'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(pwfile, targetmode)
+ else:
+ print
+
+def checkmta():
+ if mm_cfg.MTA:
+ modname = 'Mailman.MTA.' + mm_cfg.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',
+ 'request.db', 'request.db.tmp')
+ if STATE.VERBOSE:
+ print _('checking permissions on list data')
+ # BAW: This needs to be converted to the Site module abstraction
+ for dir in os.listdir(mm_cfg.LIST_DATA_DIR):
+ for file in checkfiles:
+ path = os.path.join(mm_cfg.LIST_DATA_DIR, dir, file)
+ if STATE.VERBOSE:
+ print _(' checking permissions on: %(path)s')
+ 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)s'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | targetmode)
+ else:
+ print
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+if __name__ == '__main__':
+ try:
+ opts, args = getopt.getopt(sys.argv[1:],
+ 'fvh',
+ ['fix', 'verbose', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-f', '--fix'):
+ STATE.FIX = 1
+ elif opt in ('-v', '--verbose'):
+ STATE.VERBOSE = 1
+
+ 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)s (or root) with -f flag to fix')
diff --git a/bin/cleanarch b/bin/cleanarch
new file mode 100644
index 00000000..85a8df6a
--- /dev/null
+++ b/bin/cleanarch
@@ -0,0 +1,165 @@
+#! @PYTHON@
+
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Clean up an .mbox archive file.
+
+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.
+
+Usage: cleanarch [options] < inputfile > outputfile
+Options:
+ -s n
+ --status=n
+ Print a # character every n lines processed
+
+ -q / --quiet
+ Don't print changed line information to standard error.
+
+ -n / --dry-run
+ Don't actually output anything.
+
+ -h / --help
+ Print this message and exit
+"""
+
+import sys
+import re
+import getopt
+import mailbox
+
+import paths
+from Mailman.i18n import _
+
+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-\0176]+')
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def escape_line(line, lineno, quiet, output):
+ if output:
+ sys.stdout.write('>' + line)
+ if not quiet:
+ print >> sys.stderr, _('Unix-From line changed: %(lineno)d')
+ print >> sys.stderr, line[:-1]
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'hqns:',
+ ['help', 'quiet', 'dry-run', 'status='])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ quiet = 0
+ output = 1
+ status = -1
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-q', '--quiet'):
+ quiet = 1
+ elif opt in ('-n', '--dry-run'):
+ output = 0
+ elif opt in ('-s', '--status'):
+ try:
+ status = int(arg)
+ except ValueError:
+ usage(1, _('Bad status number: %(arg)s'))
+
+ if args:
+ usage(1)
+
+ lineno = 0
+ statuscnt = 0
+ messages = 0
+ while 1:
+ 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:
+ 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
+ print >> sys.stderr, _('%(messages)d messages found')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/clone_member b/bin/clone_member
new file mode 100755
index 00000000..5861aff5
--- /dev/null
+++ b/bin/clone_member
@@ -0,0 +1,219 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Clone a member address.
+
+Cloning a member address means that a new member will be added who has all the
+same options and passwords as the original member address. Note that this
+operation is fairly trusting of the user who runs it -- it does no
+verification to the new address, it does not send out a welcome message, etc.
+
+The existing member's subscription is usually not modified in any way. If you
+want to remove the old address, use the -r flag. If you also want to change
+any list admin addresses, use the -a flag.
+
+Usage:
+ clone_member [options] fromoldaddr tonewaddr
+
+Where:
+
+ --listname=listname
+ -l listname
+ Check and modify only the named mailing lists. If -l is not given,
+ then all mailing lists are scanned from the address. Multiple -l
+ options can be supplied.
+
+ --remove
+ -r
+ Remove the old address from the mailing list after it's been cloned.
+
+ --admin
+ -a
+ Scan the list admin addresses for the old address, and clone or change
+ them too.
+
+ --quiet
+ -q
+ Do the modifications quietly.
+
+ --nomodify
+ -n
+ Print what would be done, but don't actually do it. Inhibits the
+ --quiet flag.
+
+ --help
+ -h
+ Print this help message and exit.
+
+ fromoldaddr (`from old address') is the old address of the user. tonewaddr
+ (`to new address') is the new address of the user.
+
+"""
+
+import sys
+import getopt
+
+import paths
+from Mailman import MailList
+from Mailman import Utils
+from Mailman import Errors
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def dolist(mlist, options):
+ SPACE = ' '
+ if not options.quiet:
+ print _('processing mailing list:'), mlist.internal_name()
+
+ # scan the list owners. TBD: mlist.owner keys should be lowercase?
+ oldowners = mlist.owner[:]
+ oldowners.sort()
+ if options.admintoo:
+ if not options.quiet:
+ print _(' scanning list owners:'), SPACE.join(oldowners)
+ newowners = {}
+ foundp = 0
+ for owner in mlist.owner:
+ if options.lfromaddr == owner.lower():
+ foundp = 1
+ if options.remove:
+ continue
+ newowners[owner] = 1
+ if foundp:
+ newowners[options.toaddr] = 1
+ newowners = newowners.keys()
+ newowners.sort()
+ if options.modify:
+ mlist.owner = newowners
+ if not options.quiet:
+ if newowners <> oldowners:
+ print
+ print _(' new list owners:'), SPACE.join(newowners)
+ else:
+ print _('(no change)')
+
+ # see if the fromaddr is a digest member or regular member
+ if options.lfromaddr in mlist.getDigestMemberKeys():
+ digest = 1
+ elif options.lfromaddr in mlist.getRegularMemberKeys():
+ digest = 0
+ else:
+ if not options.quiet:
+ print _(' address not found:'), options.fromaddr
+ return
+
+ # Now change the membership address
+ try:
+ if options.modify:
+ mlist.changeMemberAddress(options.fromaddr, options.toaddr,
+ not options.remove)
+ if not options.quiet:
+ print _(' clone address added:'), options.toaddr
+ except Errors.MMAlreadyAMember:
+ if not options.quiet:
+ print _(' clone address is already a member:'), options.toaddr
+
+ if options.remove:
+ print _(' original address removed:'), options.fromaddr
+
+
+
+def main():
+ # default options
+ class Options:
+ listnames = None
+ remove = 0
+ admintoo = 0
+ quiet = 0
+ modify = 1
+
+ # scan sysargs
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'arl:qnh',
+ ['admin', 'remove', 'listname=', 'quiet', 'nomodify', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ options = Options()
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-q', '--quiet'):
+ options.quiet = 1
+ elif opt in ('-n', '--nomodify'):
+ options.modify = 0
+ elif opt in ('-a', '--admin'):
+ options.admintoo = 1
+ elif opt in ('-r', '--remove'):
+ options.remove = 1
+ elif opt in ('-l', '--listname'):
+ if options.listnames is None:
+ options.listnames = []
+ options.listnames.append(arg.lower())
+
+ # further options and argument processing
+ if not options.modify:
+ options.quiet = 0
+
+ if len(args) <> 2:
+ usage(1)
+ fromaddr = args[0]
+ toaddr = args[1]
+
+ # validate and normalize the target address
+ try:
+ Utils.ValidateEmail(toaddr)
+ except Errors.EmailAddressError:
+ usage(1, _('Not a valid email address: %(toaddr)s'))
+ lfromaddr = fromaddr.lower()
+ options.toaddr = toaddr
+ options.fromaddr = fromaddr
+ options.lfromaddr = lfromaddr
+
+ if options.listnames is None:
+ options.listnames = Utils.list_names()
+
+ for listname in options.listnames:
+ try:
+ mlist = MailList.MailList(listname)
+ except Errors.MMListError, e:
+ print _('Error opening list "%(listname)s", skipping.\n%(e)s')
+ continue
+ try:
+ dolist(mlist, options)
+ finally:
+ mlist.Save()
+ mlist.Unlock()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/config_list b/bin/config_list
new file mode 100644
index 00000000..9675e142
--- /dev/null
+++ b/bin/config_list
@@ -0,0 +1,339 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Configure a list from a text file description.
+
+Usage: config_list [options] listname
+
+Options:
+ --inputfile filename
+ -i filename
+ Configure the list by assigning each module-global variable in the
+ file to an attribute on the list object, then saving 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!
+
+ --outputfile filename
+ -o filename
+ Instead of configuring the list, print out a 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.
+
+ --checkonly
+ -c
+ With this option, the modified list is not actually changed. Only
+ useful with -i.
+
+ --verbose
+ -v
+ Print the name of each attribute as it is being changed. Only useful
+ with -i.
+
+ --help
+ -h
+ Print this help message and exit.
+
+The options -o and -i are mutually exclusive.
+
+"""
+
+import sys
+import re
+import time
+import getopt
+from types import TupleType
+
+import paths
+from Mailman import mm_cfg
+from Mailman import MailList
+from Mailman import Utils
+from Mailman import Errors
+from Mailman.i18n import _
+
+NL = '\n'
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def do_output(listname, outfile):
+ closep = 0
+ try:
+ if outfile == '-':
+ outfp = sys.stdout
+ else:
+ outfp = open(outfile, 'w')
+ closep = 1
+ # Open the specified list unlocked, since we're only reading it.
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError:
+ usage(1, _('No such list: %(listname)s'))
+ # get all the list config info. all this stuff is accessible via the
+ # web interface
+ when = time.ctime(time.time())
+ print >> outfp, _('''\
+## "%(listname)s" mailing list configuration settings -*- python -*-
+## captured on %(when)s
+''')
+
+ for k in mm_cfg.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
+ 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].split('\n')])
+ # Print out the category description
+ desc = Utils.wrap(desc)
+ for line in desc.split('\n'):
+ print >> outfp, '#', line
+ print >> outfp
+ for data in info[1:]:
+ if not isinstance(data, TupleType):
+ 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].split('\n')])
+ # 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 (mm_cfg.Text, mm_cfg.FileUpload):
+ print >> outfp, varname, '=',
+ lines = value.splitlines()
+ if not lines:
+ print >> outfp, "''"
+ elif len(lines) == 1:
+ print >> outfp, repr(lines[0])
+ else:
+ first = 1
+ outfp.write(' """')
+ for line in lines:
+ if first:
+ first = 0
+ else:
+ print >> outfp
+ outfp.write(line.replace('"', '\\"'))
+ print >> outfp, '"""'
+ elif vtype in (mm_cfg.Radio, mm_cfg.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 mm_cfg.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, TupleType):
+ 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):
+ fakedoc = FakeDoc()
+ # open the specified list locked, unless checkonly is set
+ try:
+ mlist = MailList.MailList(listname, lock=not checkonly)
+ except Errors.MMListError, e:
+ usage(1, _('No such list "%(listname)s"\n%(e)s'))
+ savelist = 0
+ 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 = 1
+ for k, v in globals.items():
+ if k in ('mlist', '__builtins__'):
+ continue
+ if not hasattr(mlist, k):
+ print >> sys.stderr, _('attribute "%(k)s" ignored')
+ continue
+ if verbose:
+ print >> sys.stderr, _('attribute "%(k)s" 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)s')
+ 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)s')
+ except Errors.EmailAddressError:
+ print >> sys.stderr, _(
+ 'Bad email address for option %(k)s: %(v)s')
+ 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 mm_cfg.ALLOW_OPEN_SUBSCRIBE:
+ validval -= 1
+ gui._setValue(mlist, k, validval, fakedoc)
+ # BAW: when to do gui._postValidate()???
+ finally:
+ if savelist and not checkonly:
+ mlist.Save()
+ mlist.Unlock()
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'ci:o:vh',
+ ['checkonly', 'inputfile=', 'outputfile=', 'verbose', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ # defaults
+ infile = None
+ outfile = None
+ checkonly = 0
+ verbose = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-o', '--outputfile'):
+ outfile = arg
+ elif opt in ('-i', '--inputfile'):
+ infile = arg
+ elif opt in ('-c', '--checkonly'):
+ checkonly = 1
+ elif opt in ('-v', '--verbose'):
+ verbose = 1
+
+ # sanity check
+ if infile is not None and outfile is not None:
+ usage(1, _('Only one of -i or -o is allowed'))
+ if infile is None and outfile is None:
+ usage(1, _('One of -i or -o is required'))
+
+ # get the list name
+ if len(args) <> 1:
+ usage(1, _('List name is required'))
+ listname = args[0].lower().strip()
+
+ if outfile:
+ do_output(listname, outfile)
+ else:
+ do_input(listname, infile, checkonly, verbose)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/convert.py b/bin/convert.py
new file mode 100644
index 00000000..24951b5e
--- /dev/null
+++ b/bin/convert.py
@@ -0,0 +1,44 @@
+#! @PYTHON@
+#
+# Copyright (C) 2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Convert a list's interpolation strings from %-strings to $-strings.
+
+This script is intended to be run as a bin/withlist script, i.e.
+
+% bin/withlist -l -r convert <mylist>
+"""
+
+import paths
+from Mailman import Utils
+from Mailman.i18n import _
+
+def convert(mlist):
+ for attr in ('msg_header', 'msg_footer', 'digest_header', 'digest_footer',
+ 'autoresponse_postings_text', 'autoresponse_admin_text',
+ 'autoresponse_request_text'):
+ s = getattr(mlist, attr)
+ t = Utils.to_dollar(s)
+ setattr(mlist, attr, t)
+ mlist.use_dollar_strings = 1
+ print _('Saving list')
+ mlist.Save()
+
+
+
+if __name__ == '__main__':
+ print _(__doc__.replace('%', '%%'))
diff --git a/bin/dumpdb b/bin/dumpdb
new file mode 100644
index 00000000..be04385b
--- /dev/null
+++ b/bin/dumpdb
@@ -0,0 +1,134 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Dump the contents of any Mailman `database' file.
+
+Usage: %(PROGRAM)s [options] filename
+
+Options:
+
+ --marshal/-m
+ Assume the file contains a Python marshal, overridding any automatic
+ guessing.
+
+ --pickle/-p
+ Assume the file contains a Python pickle, overridding any automatic
+ guessing.
+
+ --noprint/-n
+ Don't attempt to pretty print the object. This is useful if there's
+ some problem with the object and you just want to get an unpickled
+ representation. Useful with `python -i bin/dumpdb <file>'. In that
+ case, the root of the tree will be left in a global called "msg".
+
+ --help/-h
+ Print this help message and exit
+
+If the filename ends with `.db', then it is assumed that the file contains a
+Python marshal. If the file ends with `.pck' then it is assumed to contain a
+Python pickle. In either case, if you want to override the default assumption
+-- or if the file ends in neither suffix -- use the -p or -m flags.
+"""
+
+import sys
+import os
+import getopt
+import pprint
+import cPickle
+
+import paths
+# Import this /after/ paths so that the sys.path is properly hacked
+from email.Generator import Generator
+
+from Mailman.Queue.Switchboard import DumperSwitchboard
+from Mailman.i18n import _
+
+PROGRAM = sys.argv[0]
+COMMASPACE = ', '
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__) % globals()
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'mphn',
+ ['marshal', 'pickle', 'help', 'noprint'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ # Options.
+ # None == guess, 0 == pickle, 1 == marshal
+ filetype = None
+ doprint = 1
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-p', '--pickle'):
+ filetype = 0
+ elif opt in ('-m', '--marshal'):
+ filetype = 1
+ elif opt in ('-n', '--noprint'):
+ doprint = 0
+
+ if len(args) < 1:
+ usage(1, _('No filename given.'))
+ elif len(args) > 1:
+ pargs = COMMASPACE.join(args)
+ usage(1, _('Bad arguments: %(pargs)s'))
+ else:
+ filename = args[0]
+
+ if filetype is None:
+ if filename.endswith('.db'):
+ filetype = 1
+ elif filename.endswith('.pck'):
+ filetype = 0
+ else:
+ usage(1, _('Please specify either -p or -m.'))
+
+ # Handle dbs
+ pp = pprint.PrettyPrinter(indent=4)
+ if filetype == 1:
+ # BAW: this probably doesn't work if there are mixed types of .db
+ # files (i.e. some marshals, some bdbs).
+ d = DumperSwitchboard().read(filename)
+ if doprint:
+ pp.pprint(d)
+ return d
+ else:
+ m = cPickle.load(open(filename))
+ if doprint:
+ pp.pprint(m)
+ return m
+
+
+
+if __name__ == '__main__':
+ msg = main()
diff --git a/bin/find_member b/bin/find_member
new file mode 100755
index 00000000..0656d500
--- /dev/null
+++ b/bin/find_member
@@ -0,0 +1,184 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Find all lists that a member's address is on.
+
+Usage:
+ find_member [options] regex [regex [...]]
+
+Where:
+ --listname=listname
+ -l listname
+ Include only the named list in the search.
+
+ --exclude=listname
+ -x listname
+ Exclude the named list from the search.
+
+ --owners
+ -w
+ Search list owners as well as members.
+
+ --help
+ -h
+ Print this help message and exit.
+
+ regex
+ A Python regular expression to match against.
+
+The interaction between -l and -x 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 is Perl5-like, using 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.
+
+"""
+
+import sys
+import re
+import getopt
+
+import paths
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman.i18n import _
+
+AS_MEMBER = 0x01
+AS_OWNER = 0x02
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def scanlists(options):
+ cres = []
+ for r in options.regexps:
+ cres.append(re.compile(r, re.IGNORECASE))
+ #
+ # dictionary of {address, (listname, ownerp)}
+ matches = {}
+ for listname in options.listnames:
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError:
+ print _('No such list: %(listname)s')
+ continue
+ if options.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(addr, {})
+ aswhat = entries.get(listname, 0)
+ aswhat |= AS_OWNER
+ entries[listname] = aswhat
+ matches[addr] = entries
+ return matches
+
+
+
+class Options:
+ listnames = Utils.list_names()
+ owners = None
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'l:x:wh',
+ ['listname=', 'exclude=', 'owners',
+ 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ options = Options()
+ loptseen = 0
+ excludes = []
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-l', '--listname'):
+ if not loptseen:
+ options.listnames = []
+ loptseen = 1
+ options.listnames.append(arg.lower())
+ elif opt in ('-x', '--exclude'):
+ excludes.append(arg.lower())
+ elif opt in ('-w', '--owners'):
+ options.owners = 1
+
+ for ex in excludes:
+ try:
+ options.listnames.remove(ex)
+ except ValueError:
+ pass
+
+ if not args:
+ usage(1, _('Search regular expression required'))
+
+ options.regexps = args
+
+ if not options.listnames:
+ print _('No lists to search')
+ return
+
+ matches = scanlists(options)
+ 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/bin/fix_url.py b/bin/fix_url.py
new file mode 100644
index 00000000..582a0e76
--- /dev/null
+++ b/bin/fix_url.py
@@ -0,0 +1,92 @@
+#! @PYTHON@
+#
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Reset a list's web_page_url attribute to the default setting.
+
+This script is intended to be run as a bin/withlist script, i.e.
+
+% bin/withlist -l -r fix_url listname [options]
+
+Options:
+ -u urlhost
+ --urlhost=urlhost
+ Look up urlhost in the virtual host table and set the web_page_url and
+ host_name attributes of the list to the values found. This
+ essentially moves the list from one virtual domain to another.
+
+ Without this option, the default web_page_url and host_name values are
+ used.
+
+ -v / --verbose
+ Print what the script is doing.
+
+If run standalone, it prints this help text and exits.
+"""
+
+import sys
+import getopt
+
+import paths
+from Mailman import mm_cfg
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ print _(__doc__.replace('%', '%%'))
+ if msg:
+ print msg
+ sys.exit(code)
+
+
+
+def fix_url(mlist, *args):
+ try:
+ opts, args = getopt.getopt(args, 'u:v', ['urlhost=', 'verbose'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ verbose = 0
+ urlhost = mailhost = None
+ for opt, arg in opts:
+ if opt in ('-u', '--urlhost'):
+ urlhost = arg
+ elif opt in ('-v', '--verbose'):
+ verbose = 1
+
+ if urlhost:
+ web_page_url = mm_cfg.DEFAULT_URL_PATTERN % urlhost
+ mailhost = mm_cfg.VIRTUAL_HOSTS.get(urlhost.lower(), urlhost)
+ else:
+ web_page_url = mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST
+ mailhost = mm_cfg.DEFAULT_EMAIL_HOST
+
+ if verbose:
+ print _('Setting web_page_url to: %(web_page_url)s')
+ mlist.web_page_url = web_page_url
+ if verbose:
+ print _('Setting host_name to: %(mailhost)s')
+ mlist.host_name = mailhost
+ print _('Saving list')
+ mlist.Save()
+ mlist.Unlock()
+
+
+
+if __name__ == '__main__':
+ usage(0)
diff --git a/bin/genaliases b/bin/genaliases
new file mode 100644
index 00000000..289c5bec
--- /dev/null
+++ b/bin/genaliases
@@ -0,0 +1,102 @@
+#! @PYTHON@
+#
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Regenerate Postfix's data/aliases and data/aliases.db files from scratch.
+
+Usage:
+
+ genaliases [options]
+
+Options:
+
+ -h/--help
+ Print this message and exit.
+"""
+
+import sys
+import os
+import getopt
+import fcntl
+
+import paths # path hacking
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'h', ['help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+
+ if args:
+ usage(1)
+
+ # Open up the MTA specific module
+ modulename = 'Mailman.MTA.' + mm_cfg.MTA
+ __import__(modulename)
+ MTA = sys.modules[modulename]
+
+ # Open the text file and Berkeley DB files, truncating any data already
+ # there. We need to acquire a lock so nobody tries to update the files
+ # while we're doing it.
+ lock = MTA.makelock()
+ lock.lock()
+ # Group lists by virtual hostname
+ mlists = {}
+ for listname in Utils.list_names():
+ mlist = MailList.MailList(listname, lock=0)
+ mlists.setdefault(mlist.host_name, []).append(mlist)
+ # Make sure the files are created rw-rw-xxx; it should be okay to be world
+ # readable.
+ omask = os.umask(002)
+ try:
+ MTA.clear()
+ if not mlists:
+ MTA.create(None, nolock=1)
+ else:
+ for hostname, vlists in mlists.items():
+ for mlist in vlists:
+ MTA.create(mlist, nolock=1)
+ finally:
+ os.umask(omask)
+ lock.unlock(unconditionally=1)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/inject b/bin/inject
new file mode 100644
index 00000000..50deba6a
--- /dev/null
+++ b/bin/inject
@@ -0,0 +1,107 @@
+#! @PYTHON@
+#
+# Copyright (C) 2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Inject a message from a file into Mailman's incoming queue.
+
+Usage: inject [options] [filename]
+
+Options:
+
+ -h / --help
+ Print this text and exit.
+
+ -l listname
+ --listname=listname
+ The name of the list to inject this message to. Required.
+
+ -q queuename
+ --queue=queuename
+ 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.
+
+filename is the name of the plaintext message file to inject. If omitted,
+standard input is used.
+"""
+
+import sys
+import os
+import getopt
+
+import paths
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import Post
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'hl:q:L',
+ ['help', 'listname=', 'queue=', 'showqnames'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ qdir = mm_cfg.INQUEUE_DIR
+ listname = None
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-q', '--queue'):
+ qdir = os.path.join(mm_cfg.QUEUE_DIR, arg)
+ if not os.path.isdir(qdir):
+ usage(1, _('Bad queue directory: %(qdir)s'))
+ elif opt in ('-l', '--listname'):
+ listname = arg
+
+ if listname is None:
+ usage(1, _('A list name is required'))
+ elif not Utils.list_exists(listname):
+ usage(1, _('No such list: %(listname)s'))
+
+ if len(args) == 0:
+ # Use standard input
+ msgtext = sys.stdin.read()
+ elif len(args) == 1:
+ fp = open(args[0])
+ msgtext = fp.read()
+ fp.close()
+ else:
+ usage(1)
+
+ Post.inject(listname, msgtext, qdir=qdir)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/list_admins b/bin/list_admins
new file mode 100644
index 00000000..0c4e4ffe
--- /dev/null
+++ b/bin/list_admins
@@ -0,0 +1,101 @@
+#! @PYTHON@
+#
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""List all the owners of a mailing list.
+
+Usage: %(program)s [options] listname ...
+
+Where:
+
+ --all-vhost=vhost
+ -v=vhost
+ List the owners of all the mailing lists for the given virtual host.
+
+ --all
+ -a
+ List the owners of all the mailing lists on this system.
+
+ --help
+ -h
+ Print this help message and exit.
+
+`listname' is the name of the mailing list to print the owners of. You can
+have more than one named list on the command line.
+"""
+
+import sys
+import getopt
+
+import paths
+from Mailman import MailList, Utils
+from Mailman import Errors
+from Mailman.i18n import _
+
+COMMASPACE = ', '
+
+program = sys.argv[0]
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'hv:a',
+ ['help', 'all-vhost=', 'all'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ listnames = args
+ vhost = None
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-a', '--all'):
+ listnames = Utils.list_names()
+ elif opt in ('-v', '--all-vhost'):
+ listnames = Utils.list_names()
+ vhost = arg
+
+ for listname in listnames:
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ print _('No such list: %(listname)s')
+ continue
+
+ if vhost and vhost <> mlist.host_name:
+ continue
+
+ owners = COMMASPACE.join(mlist.owner)
+ print _('List: %(listname)s, \tOwners: %(owners)s')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/list_lists b/bin/list_lists
new file mode 100644
index 00000000..4dd6b55c
--- /dev/null
+++ b/bin/list_lists
@@ -0,0 +1,122 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""List all mailing lists.
+
+Usage: %(program)s [options]
+
+Where:
+
+ -a / --advertised
+ List only those mailing lists that are publically advertised
+
+ --virtual-host-overview=domain
+ -V domain
+ List only those mailing lists that are homed to the given virtual
+ domain. This only works if the VIRTUAL_HOST_OVERVIEW variable is
+ set.
+
+ -b / --bare
+ Displays only the list name, with no description.
+
+ -h / --help
+ Print this text and exit.
+
+"""
+
+import sys
+import getopt
+import paths
+
+from Mailman import mm_cfg
+from Mailman import MailList
+from Mailman import Utils
+from Mailman import Errors
+from Mailman.i18n import _
+
+program = sys.argv[0]
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'abV:h',
+ ['advertised', 'bare',
+ 'virtual-host-overview=',
+ 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ advertised = 0
+ vhost = None
+ bare = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-a', '--advertised'):
+ advertised = 1
+ elif opt in ('-V', '--virtual-host-overview'):
+ vhost = arg
+ elif opt in ('-b', '--bare'):
+ bare = 1
+
+ names = Utils.list_names()
+ names.sort()
+
+ mlists = []
+ longest = 0
+ for n in names:
+ mlist = MailList.MailList(n, lock=0)
+ if advertised and not mlist.advertised:
+ continue
+ if vhost and mm_cfg.VIRTUAL_HOST_OVERVIEW and \
+ vhost.find(mlist.web_page_url) == -1 and \
+ mlist.web_page_url.find(vhost) == -1:
+ continue
+ mlists.append(mlist)
+ longest = max(len(mlist.real_name), longest)
+
+ if not mlists and not bare:
+ print _('No matching mailing lists found')
+ return
+
+ if not bare:
+ print len(mlists), _('matching mailing lists found:')
+
+ format = '%%%ds - %%.%ds' % (longest, 77 - longest)
+ for mlist in mlists:
+ if bare:
+ print mlist.internal_name()
+ else:
+ description = mlist.description or _('[no description available]')
+ print ' ', format % (mlist.real_name, description)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/list_members b/bin/list_members
new file mode 100755
index 00000000..3ad10f51
--- /dev/null
+++ b/bin/list_members
@@ -0,0 +1,232 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""List all the members of a mailing list.
+
+Usage: %(PROGRAM)s [options] listname
+
+Where:
+
+ --output file
+ -o file
+ Write output to specified file instead of standard out.
+
+ --regular / -r
+ Print just the regular (non-digest) members.
+
+ --digest[=kind] / -d [kind]
+ Print just the digest members. Optional argument can be "mime" or
+ "plain" which prints just the digest members receiving that kind of
+ digest.
+
+ --nomail[=why] / -n [why]
+ Print the members that have delivery disabled. Optional argument can
+ be "byadmin", "byuser", "bybounce", or "unknown" which prints just the
+ users who have delivery disabled for that reason. It can also be
+ "enabled" which prints just those member for whom delivery is
+ enabled.
+
+ --fullnames / -f
+ Include the full names in the output.
+
+ --preserve
+ -p
+ Output member addresses case preserved the way they were added to the
+ list. Otherwise, addresses are printed in all lowercase.
+
+ --help
+ -h
+ Print this help message and exit.
+
+ listname is the name of the mailing list to use.
+
+Note that if neither -r or -d is supplied, both regular members are printed
+first, followed by digest members, but no indication is given as to address
+status.
+"""
+
+import sys
+
+import paths
+from Mailman import mm_cfg
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import MemberAdaptor
+from Mailman.i18n import _
+
+from email.Utils import formataddr
+
+PROGRAM = sys.argv[0]
+WHYCHOICES = {'enabled' : MemberAdaptor.ENABLED,
+ 'unknown' : MemberAdaptor.UNKNOWN,
+ 'byuser' : MemberAdaptor.BYUSER,
+ 'byadmin' : MemberAdaptor.BYADMIN,
+ 'bybounce': MemberAdaptor.BYBOUNCE,
+ }
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+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 is None:
+ return status <> MemberAdaptor.ENABLED
+ return status == WHYCHOICES[why]
+
+
+
+def main():
+ # Because of the optional arguments, we can't use getopt. :(
+ outfile = None
+ regular = None
+ digest = None
+ preserve = None
+ nomail = None
+ why = None
+ kind = None
+ fullnames = 0
+
+ # Throw away the first (program) argument
+ args = sys.argv[1:]
+ if not args:
+ usage(0)
+
+ while 1:
+ try:
+ opt = args.pop(0)
+ except IndexError:
+ usage(1)
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-f', '--fullnames'):
+ fullnames = 1
+ elif opt in ('-p', '--preserve'):
+ preserve = 1
+ elif opt in ('-r', '--regular'):
+ regular = 1
+ elif opt in ('-o', '--output'):
+ try:
+ outfile = args.pop(0)
+ except IndexError:
+ usage(1)
+ elif opt == '-n':
+ nomail = 1
+ if args and args[0] in WHYCHOICES.keys():
+ why = args.pop(0)
+ elif opt.startswith('--nomail'):
+ nomail = 1
+ i = opt.find('=')
+ if i >= 0:
+ why = opt[i+1:]
+ if why not in WHYCHOICES.keys():
+ usage(1, _('Bad --nomail option: %(why)s'))
+ elif opt == '-d':
+ digest = 1
+ if args and args[0] in ('mime', 'plain'):
+ kind = args.pop(0)
+ elif opt.startswith('--digest'):
+ digest = 1
+ i = opt.find('=')
+ if i >= 0:
+ kind = opt[i+1:]
+ if kind not in ('mime', 'plain'):
+ usage(1, _('Bad --digest option: %(kind)s'))
+ else:
+ # No more options left, push the last one back on the list
+ args.insert(0, opt)
+ break
+
+ if len(args) <> 1:
+ usage(1)
+
+ listname = args[0].lower().strip()
+
+ if regular is None and digest is None:
+ regular = digest = 1
+
+ if outfile:
+ try:
+ fp = open(outfile, 'w')
+ except IOError:
+ print >> sys.stderr, _('Could not open file for writing:'), outfile
+ sys.exit(1)
+ else:
+ fp = sys.stdout
+
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ print >> sys.stderr, _('No such list: %(listname)s')
+ sys.exit(1)
+
+ # Get the lowercased member addresses
+ rmembers = mlist.getRegularMemberKeys()
+ dmembers = mlist.getDigestMemberKeys()
+
+ if preserve:
+ # Convert to the case preserved addresses
+ rmembers = mlist.getMemberCPAddresses(rmembers)
+ dmembers = mlist.getMemberCPAddresses(dmembers)
+
+ if regular:
+ rmembers.sort()
+ for addr in rmembers:
+ name = fullnames and mlist.getMemberName(addr)
+ # Filter out nomails
+ if nomail and not whymatches(mlist, addr, why):
+ continue
+ enc = sys.getdefaultencoding()
+ s = formataddr((name, addr)).encode(enc, 'replace')
+ print >> fp, s
+ if digest:
+ dmembers.sort()
+ for addr in dmembers:
+ name = fullnames and mlist.getMemberName(addr)
+ # Filter out nomails
+ if nomail and not whymatches(mlist, addr, why):
+ continue
+ # Filter out digest kinds
+ if mlist.getMemberOption(addr, mm_cfg.DisableMime):
+ # They're getting plain text digests
+ if kind == 'mime':
+ continue
+ else:
+ # They're getting MIME digests
+ if kind == 'plain':
+ continue
+ enc = sys.getdefaultencoding()
+ s = formataddr((name, addr)).encode(enc, 'replace')
+ print >> fp, s
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/list_owners b/bin/list_owners
new file mode 100644
index 00000000..baacf27a
--- /dev/null
+++ b/bin/list_owners
@@ -0,0 +1,120 @@
+#! @PYTHON@
+#
+# Copyright (C) 2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""List the owners of a mailing list, or all mailing lists.
+
+Usage: %(PROGRAM)s [options] [listname ...]
+Options:
+
+ -w / --with-listnames
+ 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.
+
+ -m / --moderators
+ Include the list moderators in the output.
+
+ -h / --help
+ Print this help message and exit.
+
+ listname
+ Print the owners of the specified lists. More than one can appear
+ after the options. If there are no listnames provided, the owners of
+ all the lists will be displayed.
+"""
+
+import sys
+import getopt
+
+import paths
+from Mailman import Utils
+from Mailman.MailList import MailList
+from Mailman.i18n import _
+
+PROGRAM = sys.argv[0]
+
+try:
+ True, False
+except NameError:
+ True = 1
+ False = 0
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'wmh',
+ ['with-listnames', 'moderators', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ withnames = moderators = False
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-m', '--moderators'):
+ moderators = True
+ elif opt in ('-w', '--with-listnames'):
+ withnames = True
+
+ listnames = args or Utils.list_names()
+ bylist = {}
+
+ for listname in listnames:
+ mlist = MailList(listname, lock=0)
+ addrs = mlist.owner[:]
+ if moderators:
+ addrs.extend(mlist.moderator)
+ bylist[listname] = addrs
+
+ if withnames:
+ for listname in listnames:
+ unique = {}
+ for addr in bylist[listname]:
+ unique[addr] = 1
+ keys = unique.keys()
+ keys.sort()
+ print listname
+ for k in keys:
+ print '\t', k
+ else:
+ unique = {}
+ for listname in listnames:
+ for addr in bylist[listname]:
+ unique[addr] = 1
+ keys = unique.keys()
+ keys.sort()
+ for k in keys:
+ print k
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/mailmanctl b/bin/mailmanctl
new file mode 100644
index 00000000..0292e1f3
--- /dev/null
+++ b/bin/mailmanctl
@@ -0,0 +1,524 @@
+#! @PYTHON@
+
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""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, and SIGHUP. SIGINT and SIGTERM both
+cause the qrunners to exit cleanly, but the master will only restart qrunners
+that have exited due to a SIGINT. 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, 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.
+
+Usage: %(PROGRAM)s [options] [ start | stop | restart | reopen ]
+
+Options:
+
+ -n/--no-restart
+ Don't restart the qrunners when they exit because of an error or a
+ SIGINT. They are never restarted if they exit in response to a
+ SIGTERM. Use this only for debugging. Only useful if the `start'
+ command is given.
+
+ -u/--run-as-user
+ 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!
+
+ -s/--stale-lock-cleanup
+ If mailmanctl finds an existing master lock, it will normally exit
+ with an error message. With this option, mailmanctl will perform an
+ extra level of checking. If a process matching the host/pid described
+ in the lock file is running, mailmanctl will still exit, but if no
+ matching process is found, mailmanctl will remove the apparently stale
+ lock and make another attempt to claim the master lock.
+
+ -q/--quiet
+ Don't print status messages. Error messages are still printed to
+ standard error.
+
+ -h/--help
+ Print this message and exit.
+
+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
+"""
+
+import sys
+import os
+import time
+import getopt
+import signal
+import errno
+import pwd
+import grp
+import socket
+
+import paths
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import LockFile
+from Mailman.i18n import _
+from Mailman.Logging.Syslog import syslog
+from Mailman.Logging.Utils import LogStdErr
+
+PROGRAM = sys.argv[0]
+COMMASPACE = ', '
+DOT = '.'
+
+# Locking contantsa
+LOCKFILE = os.path.join(mm_cfg.LOCK_DIR, 'master-qrunner')
+# Since we wake up once per day and refresh the lock, the LOCK_LIFETIME
+# needn't be (much) longer than SNOOZE. We pad it 6 hours just to be safe.
+LOCK_LIFETIME = mm_cfg.days(1) + mm_cfg.hours(6)
+SNOOZE = mm_cfg.days(1)
+MAX_RESTARTS = 10
+
+LogStdErr('error', 'mailmanctl', manual_reprime=0)
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def kill_watcher(sig):
+ try:
+ fp = open(mm_cfg.PIDFILE)
+ pidstr = fp.read()
+ fp.close()
+ pid = int(pidstr.strip())
+ except (IOError, ValueError), e:
+ # For i18n convenience
+ pidfile = mm_cfg.PIDFILE
+ print >> sys.stderr, _('PID unreadable in: %(pidfile)s')
+ print >> sys.stderr, e
+ print >> sys.stderr, _('Is qrunner even running?')
+ return
+ try:
+ os.kill(pid, sig)
+ except OSError, e:
+ if e.errno <> errno.ESRCH: raise
+ print >> sys.stderr, _('No child with pid: %(pid)s')
+ print >> sys.stderr, e
+ print >> sys.stderr, _('Stale pid file removed.')
+ os.unlink(mm_cfg.PIDFILE)
+
+
+
+def get_lock_data():
+ # Return the hostname, pid, and tempfile
+ fp = open(LOCKFILE)
+ filename = os.path.split(fp.read().strip())[1]
+ fp.close()
+ parts = filename.split('.')
+ hostname = DOT.join(parts[1:-1])
+ pid = int(parts[-1])
+ return hostname, int(pid), filename
+
+
+def qrunner_state():
+ # 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 hostname
+ # Find out if the process exists by calling kill with a signal 0.
+ try:
+ os.kill(pid, 0)
+ except OSError, e:
+ if e.errno <> errno.ESRCH: raise
+ return 0
+ return 1
+
+
+def acquire_lock_1(force):
+ # Be sure we can acquire the master qrunner lock. If not, it means some
+ # other master qrunner daemon is already going.
+ lock = LockFile.LockFile(LOCKFILE, LOCK_LIFETIME)
+ try:
+ lock.lock(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(LOCKFILE)
+ os.unlink(os.path.join(mm_cfg.LOCK_DIR, tempfile))
+ return acquire_lock_1(force=0)
+
+
+def acquire_lock(force):
+ try:
+ lock = acquire_lock_1(force)
+ return lock
+ except LockFile.TimeOutError:
+ status = qrunner_state()
+ if status == 1:
+ # host matches and proc exists
+ print >> sys.stderr, _("""\
+The master qrunner lock could not be acquired because it appears as if another
+master qrunner is already running.
+""")
+ elif status == 0:
+ # host matches but no proc
+ print >> sys.stderr, _("""\
+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:
+ # host doesn't even match
+ print >> sys.stderr, _("""\
+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 do this manually. Or, if you
+know the lock is stale, re-run mailmanctl with the -s flag.
+
+Lock file: %(LOCKFILE)s
+Lock host: %(status)s
+
+Exiting.""")
+
+
+
+def start_runner(qrname, slice, count):
+ pid = os.fork()
+ if pid:
+ # parent
+ return pid
+ # child
+ #
+ # Craft the command line arguments for the exec() call.
+ rswitch = '--runner=%s:%d:%d' % (qrname, slice, count)
+ # BAW: should argv[0] be `python'?
+ exe = os.path.join(mm_cfg.BIN_DIR, 'qrunner')
+ os.execl(mm_cfg.PYTHON, 'qrunner', exe, rswitch, '-s')
+ # Should never get here
+ raise RuntimeError, 'os.execl() failed'
+
+
+def start_all_runners():
+ kids = {}
+ for qrname, count in mm_cfg.QRUNNERS:
+ for slice in range(count):
+ # queue runner name, slice, numslices, restart count
+ info = (qrname, slice, count, 0)
+ pid = start_runner(qrname, slice, count)
+ kids[pid] = info
+ return kids
+
+
+
+def check_privs():
+ # 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(mm_cfg.MAILMAN_GROUP)[2]
+ uid = pwd.getpwnam(mm_cfg.MAILMAN_USER)[2]
+ myuid = os.getuid()
+ if myuid == 0:
+ os.setgid(gid)
+ os.setuid(uid)
+ elif myuid <> uid:
+ name = mm_cfg.MAILMAN_USER
+ usage(1, _(
+ 'Run this program as root or as the %(name)s user, or use -u.'))
+
+
+
+def main():
+ global quiet
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'hnusq',
+ ['help', 'no-start', 'run-as-user',
+ 'stale-lock-cleanup', 'quiet'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ restart = 1
+ checkprivs = 1
+ force = 0
+ quiet = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-n', '--no-restart'):
+ restart = 0
+ elif opt in ('-u', '--run-as-user'):
+ checkprivs = 0
+ elif opt in ('-s', '--stale-lock-cleanup'):
+ force = 1
+ elif opt in ('-q', '--quiet'):
+ quiet = 1
+
+ if len(args) < 1:
+ usage(1, _('No command given.'))
+ elif len(args) > 1:
+ command = COMMASPACE.join(args)
+ usage(1, _('Bad command: %(command)s'))
+
+ if checkprivs:
+ check_privs()
+ else:
+ print _('Warning! You may encounter permission problems.')
+
+ # Handle the commands
+ command = args[0].lower()
+ if command == 'stop':
+ # Sent the master qrunner process a SIGINT, which is equivalent to
+ # giving cron/qrunner a ctrl-c or KeyboardInterrupt. This will
+ # effectively shut everything down.
+ if not quiet:
+ print _("Shutting down Mailman's master qrunner")
+ kill_watcher(signal.SIGTERM)
+ elif command == 'restart':
+ # Sent the master qrunner process a SIGHUP. This will cause the
+ # master qrunner to kill and restart all the worker qrunners, and to
+ # close and re-open its log files.
+ if not quiet:
+ print _("Restarting Mailman's master qrunner")
+ kill_watcher(signal.SIGINT)
+ elif command == 'reopen':
+ if not quiet:
+ print _('Re-opening all log files')
+ kill_watcher(signal.SIGHUP)
+ elif command == 'start':
+ # Here's the scoop on the processes we're about to create. We'll need
+ # one for each qrunner, and one for a master child process watcher /
+ # lock refresher process.
+ #
+ # The child watcher process simply waits on the pids of the children
+ # qrunners. Unless explicitly disabled by a mailmanctl switch (or the
+ # children are killed with SIGTERM instead of SIGINT), the watcher
+ # will automatically restart any child process that exits. This
+ # allows us to be more robust, and also to implement restart by simply
+ # SIGINT'ing the qrunner children, and letting the watcher restart
+ # them.
+ #
+ # Under normal operation, we have a child per queue. This lets us get
+ # the most out of the available resources, since a qrunner with no
+ # files in its queue directory is pretty cheap, but having a separate
+ # runner process per queue allows for a very responsive system. Some
+ # people want a more traditional (i.e. MM2.0.x) cron-invoked qrunner.
+ # No problem, but using mailmanctl isn't the answer. So while
+ # mailmanctl hard codes some things, others, such as the number of
+ # qrunners per queue, is configurable in mm_cfg.py.
+ #
+ # First, acquire the master mailmanctl lock
+ lock = acquire_lock(force)
+ if not lock:
+ return
+ # Daemon process startup according to Stevens, Advanced Programming in
+ # the UNIX Environment, Chapter 13.
+ pid = os.fork()
+ if pid:
+ # parent
+ if not quiet:
+ print _("Starting Mailman's master qrunner.")
+ # Give up the lock "ownership". This just means the foreground
+ # process won't close/unlock the lock when it finalizes this lock
+ # instance. We'll let the mater watcher subproc own the lock.
+ lock._transfer_to(pid)
+ return
+ # child
+ lock._take_possession()
+ # First, save our pid in a file for "mailmanctl stop" rendezvous. We
+ # want the perms on the .pid file to be rw-rw----
+ omask = os.umask(6)
+ try:
+ fp = open(mm_cfg.PIDFILE, 'w')
+ print >> fp, os.getpid()
+ fp.close()
+ finally:
+ os.umask(omask)
+ # 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 installation home
+ os.chdir(mm_cfg.PREFIX)
+ # Clear our file mode creation umask
+ os.umask(0)
+ # I don't think we have any unneeded file descriptors.
+ #
+ # Now start all the qrunners. This returns a dictionary where the
+ # keys are qrunner pids and the values are tuples of the following
+ # form: (qrname, slice, count). This does its own fork and exec, and
+ # sets up its own signal handlers.
+ kids = start_all_runners()
+ # Set up a SIGALRM handler to refresh the lock once per day. The lock
+ # lifetime is 1day+6hours so this should be plenty.
+ def sigalrm_handler(signum, frame, lock=lock):
+ lock.refresh()
+ signal.alarm(mm_cfg.days(1))
+ signal.signal(signal.SIGALRM, sigalrm_handler)
+ signal.alarm(mm_cfg.days(1))
+ # Set up a SIGHUP handler so that if we get one, we'll pass it along
+ # to all the qrunner children. This will tell them to close and
+ # reopen their log files
+ def sighup_handler(signum, frame, kids=kids):
+ # Closing our syslog will cause it to be re-opened at the next log
+ # print output.
+ syslog.close()
+ for pid in kids.keys():
+ os.kill(pid, signal.SIGHUP)
+ # And just to tweak things...
+ syslog('qrunner',
+ 'Master watcher caught SIGHUP. Re-opening log files.')
+ signal.signal(signal.SIGHUP, sighup_handler)
+ # We also need to install a SIGTERM handler because that's what init
+ # will kill this process with when changing run levels.
+ def sigterm_handler(signum, frame, kids=kids):
+ for pid in kids.keys():
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except OSError, e:
+ if e.errno <> errno.ESRCH: raise
+ syslog('qrunner', 'Master watcher caught SIGTERM. Exiting.')
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ # Finally, we need a SIGINT handler which will cause the sub-qrunners
+ # to exit, but the master will restart SIGINT'd sub-processes unless
+ # the -n flag was given.
+ def sigint_handler(signum, frame, kids=kids):
+ for pid in kids.keys():
+ os.kill(pid, signal.SIGINT)
+ syslog('qrunner', 'Master watcher caught SIGINT. Restarting.')
+ signal.signal(signal.SIGINT, sigint_handler)
+ # Now we're ready to simply do our wait/restart loop. This is the
+ # master qrunner watcher.
+ try:
+ while 1:
+ try:
+ pid, status = os.wait()
+ except OSError, e:
+ # No children? We're done
+ if e.errno == errno.ECHILD:
+ break
+ # If the system call got interrupted, just restart it.
+ elif e.errno <> errno.EINTR:
+ raise
+ continue
+ killsig = exitstatus = None
+ if os.WIFSIGNALED(status):
+ killsig = os.WTERMSIG(status)
+ if os.WIFEXITED(status):
+ exitstatus = os.WEXITSTATUS(status)
+ # We'll restart the process unless we were given the
+ # "no-restart" switch, or if the process was SIGTERM'd or
+ # exitted with a SIGTERM exit status. This lets us better
+ # handle runaway restarts (say, if the subproc had a syntax
+ # error!)
+ restarting = ''
+ if restart:
+ if (exitstatus == None and killsig <> signal.SIGTERM) or \
+ (killsig == None and exitstatus <> signal.SIGTERM):
+ # Then
+ restarting = '[restarting]'
+ qrname, slice, count, restarts = kids[pid]
+ del kids[pid]
+ syslog('qrunner', """\
+Master qrunner detected subprocess exit
+(pid: %d, sig: %s, sts: %s, class: %s, slice: %d/%d) %s""",
+ pid, killsig, exitstatus, qrname,
+ slice+1, count, restarting)
+ # See if we've reached the maximum number of allowable restarts
+ if exitstatus <> signal.SIGINT:
+ restarts += 1
+ if restarts > MAX_RESTARTS:
+ syslog('qrunner', """\
+Qrunner %s reached maximum restart limit of %d, not restarting.""",
+ qrname, MAX_RESTARTS)
+ restarting = ''
+ # Now perhaps restart the process unless it exited with a
+ # SIGTERM or we aren't restarting.
+ if restarting:
+ newpid = start_runner(qrname, slice, count)
+ kids[newpid] = (qrname, slice, count, restarts)
+ finally:
+ # Should we leave the main loop for any reason, we want to be sure
+ # all of our children are exited cleanly. Send SIGTERMs to all
+ # the child processes and wait for them all to exit.
+ for pid in kids.keys():
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except OSError, e:
+ if e.errno == errno.ESRCH:
+ # The child has already exited
+ syslog('qrunner', 'ESRCH on pid: %d', pid)
+ del kids[pid]
+ # Wait for all the children to go away
+ while 1:
+ try:
+ pid, status = os.wait()
+ except OSError, e:
+ if e.errno == errno.ECHILD:
+ break
+ elif e.errno <> errno.EINTR:
+ raise
+ continue
+ # Finally, give up the lock
+ lock.unlock(unconditionally=1)
+ os._exit(0)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/mmsitepass b/bin/mmsitepass
new file mode 100755
index 00000000..8ae8bc72
--- /dev/null
+++ b/bin/mmsitepass
@@ -0,0 +1,105 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Set the site password, prompting from the terminal.
+
+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 users password can be used.
+
+Usage: %(PROGRAM)s [options] [password]
+
+Options:
+
+ -c/--listcreator
+ 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.
+
+ -h/--help
+ Print this help message and exit.
+
+If password is not given on the command line, it will be prompted for.
+"""
+
+import sys
+import getpass
+import getopt
+
+import paths
+from Mailman import Utils
+from Mailman.i18n import _
+
+PROGRAM = sys.argv[0]
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'ch',
+ ['listcreator', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ # Defaults
+ siteadmin = 1
+ pwdesc = _('site')
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-c', '--listcreator'):
+ siteadmin = 0
+ pwdesc = _('list creator')
+
+ if len(args) == 1:
+ pw1 = args[0]
+ else:
+ try:
+ pw1 = getpass.getpass(_('New %(pwdesc)s password: '))
+ pw2 = getpass.getpass(_('Again to confirm password: '))
+ if pw1 <> pw2:
+ print _('Passwords do not match; no changes made.')
+ sys.exit(1)
+ except KeyboardInterrupt:
+ print _('Interrupted...')
+ sys.exit(0)
+ # Set the site password by writing it to a local file. Make sure the
+ # permissions don't allow other+read.
+ Utils.set_global_password(pw1, siteadmin)
+ if Utils.check_global_password(pw1, siteadmin):
+ print _('Password changed.')
+ else:
+ print _('Password change failed.')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/newlist b/bin/newlist
new file mode 100755
index 00000000..503783d9
--- /dev/null
+++ b/bin/newlist
@@ -0,0 +1,219 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Create a new, unpopulated mailing list.
+
+Usage: %(PROGRAM)s [options] [listname [listadmin-addr [admin-password]]]
+
+Options:
+
+ -l language
+ --language language
+ Make the list's preferred language `language', which must be a two
+ letter language code.
+
+ -q/--quiet
+ Normally the administrator is notified by email (after a prompt) that
+ their list has been created. This option suppresses the prompt and
+ notification.
+
+ -h/--help
+ Print this help text and exit.
+
+You can specify as many of the arguments as you want on the command line:
+you will be prompted for the missing ones.
+
+Every Mailman list has two parameters which define the default host name for
+outgoing email, and the default URL for all web interfaces. When you
+configured Mailman, certain defaults were calculated, but if you are running
+multiple virtual Mailman sites, then the defaults may not be appropriate for
+the list you are creating.
+
+You can specify the domain to create your new list in by spelling the listname
+like so:
+
+ mylist@www.mydom.ain
+
+where `www.mydom.ain' should be the base hostname for the URL to this virtual
+hosts's lists. E.g. with is setting people will view the general list
+overviews at http://www.mydom.ain/mailman/listinfo. Also, www.mydom.ain
+should be a key in the VIRTUAL_HOSTS mapping in mm_cfg.py/Defaults.py. It
+will be looked up to give the email hostname. If this can't be found, then
+www.mydom.ain will be used for both the web interface and the email
+interface.
+
+If you spell the list name as just `mylist', then the email hostname will be
+taken from DEFAULT_EMAIL_HOST and the url will be taken from DEFAULT_URL (as
+defined in your Defaults.py file or overridden by settings in mm_cfg.py).
+
+Note that listnames are forced to lowercase.
+"""
+
+import sys
+import os
+import getpass
+import getopt
+import sha
+
+import paths
+from Mailman import mm_cfg
+from Mailman import MailList
+from Mailman import Utils
+from Mailman import Errors
+from Mailman import Message
+from Mailman import i18n
+
+_ = i18n._
+
+PROGRAM = sys.argv[0]
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'hql:',
+ ['help', 'quiet', 'language='])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ lang = mm_cfg.DEFAULT_SERVER_LANGUAGE
+ quiet = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ if opt in ('-q', '--quiet'):
+ quiet = 1
+ if opt in ('-l', '--language'):
+ lang = arg
+
+ # Is the language known?
+ if lang not in mm_cfg.LC_DESCRIPTIONS.keys():
+ usage(1, _('Unknown language: %(lang)s'))
+
+ if len(args) > 0:
+ listname = args[0]
+ else:
+ listname = raw_input(_('Enter the name of the list: '))
+ listname = listname.lower()
+
+ host_name = None
+ web_page_url = None
+ if '@' in listname:
+ listname, domain = listname.split('@', 1)
+ host_name = mm_cfg.VIRTUAL_HOSTS.get(domain, domain)
+ web_page_url = mm_cfg.DEFAULT_URL_PATTERN % domain
+
+ if Utils.list_exists(listname):
+ usage(1, _('List already exists: %(listname)s'))
+
+ if len(args) > 1:
+ owner_mail = args[1]
+ else:
+ owner_mail = raw_input(
+ _('Enter the email of the person running the list: '))
+
+ if len(args) > 2:
+ listpasswd = args[2]
+ else:
+ listpasswd = getpass.getpass(_('Initial %(listname)s password: '))
+ # List passwords cannot be empty
+ listpasswd = listpasswd.strip()
+ if not listpasswd:
+ usage(1, _('The list password cannot be empty'))
+
+ mlist = MailList.MailList()
+ try:
+ pw = sha.new(listpasswd).hexdigest()
+ # Guarantee that all newly created files have the proper permission.
+ # proper group ownership should be assured by the autoconf script
+ # enforcing that all directories have the group sticky bit set
+ oldmask = os.umask(002)
+ try:
+ try:
+ mlist.Create(listname, owner_mail, pw)
+ finally:
+ os.umask(oldmask)
+ except Errors.BadListNameError, s:
+ usage(1, _('Illegal list name: %(s)s'))
+ except Errors.MMBadEmailError, s:
+ usage(1, _('Bad owner email address: %(s)s'))
+ except Errors.MMListAlreadyExistsError:
+ usage(1, _('List already exists: %(listname)s'))
+
+ # Assign domain-specific attributes
+ if host_name:
+ mlist.host_name = host_name
+ mlist.web_page_url = web_page_url
+
+ # And assign the preferred language
+ mlist.preferred_language = lang
+
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+ # Now do the MTA-specific list creation tasks
+ if mm_cfg.MTA:
+ modname = 'Mailman.MTA.' + mm_cfg.MTA
+ __import__(modname)
+ sys.modules[modname].create(mlist)
+
+ # And send the notice to the list owner
+ if not quiet:
+ print _('Hit enter to notify %(listname)s owner...'),
+ sys.stdin.readline()
+ siteadmin = Utils.get_site_email(mlist.host_name, 'admin')
+ text = Utils.maketext(
+ 'newlist.txt',
+ {'listname' : listname,
+ 'password' : listpasswd,
+ 'admin_url' : mlist.GetScriptURL('admin', absolute=1),
+ 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1),
+ 'requestaddr' : mlist.GetRequestEmail(),
+ 'siteowner' : siteadmin,
+ }, 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.
+ otrans = i18n.get_translation()
+ i18n.set_language(mlist.preferred_language)
+ try:
+ msg = Message.UserNotification(
+ owner_mail, siteadmin,
+ _('Your new mailing list: %(listname)s'),
+ text, mlist.preferred_language)
+ msg.send(mlist)
+ finally:
+ i18n.set_translation(otrans)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/pygettext.py b/bin/pygettext.py
new file mode 100755
index 00000000..d6e1b4f7
--- /dev/null
+++ b/bin/pygettext.py
@@ -0,0 +1,545 @@
+#! @PYTHON@
+# Originally written by Barry Warsaw <barry@zope.com>
+#
+# Minimally patched to make it even more xgettext compatible
+# by Peter Funk <pf@artcom-gmbh.de>
+
+"""pygettext -- Python equivalent of xgettext(1)
+
+Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the
+internationalization of C programs. Most of these tools are independent of
+the programming language and can be used from within Python programs. Martin
+von Loewis' work[1] helps considerably in this regard.
+
+There's one problem though; xgettext is the program that scans source code
+looking for message strings, but it groks only C (or C++). Python introduces
+a few wrinkles, such as dual quoting characters, triple quoted strings, and
+raw strings. xgettext understands none of this.
+
+Enter pygettext, which uses Python's standard tokenize module to scan Python
+source code, generating .pot files identical to what GNU xgettext[2] generates
+for C and C++ code. From there, the standard GNU tools can be used.
+
+A word about marking Python strings as candidates for translation. GNU
+xgettext recognizes the following keywords: gettext, dgettext, dcgettext, and
+gettext_noop. But those can be a lot of text to include all over your code.
+C and C++ have a trick: they use the C preprocessor. Most internationalized C
+source includes a #define for gettext() to _() so that what has to be written
+in the source is much less. Thus these are both translatable strings:
+
+ gettext("Translatable String")
+ _("Translatable String")
+
+Python of course has no preprocessor so this doesn't work so well. Thus,
+pygettext searches only for _() by default, but see the -k/--keyword flag
+below for how to augment this.
+
+ [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html
+ [2] http://www.gnu.org/software/gettext/gettext.html
+
+NOTE: pygettext attempts to be option and feature compatible with GNU xgettext
+where ever possible. However some options are still missing or are not fully
+implemented. Also, xgettext's use of command line switches with option
+arguments is broken, and in these cases, pygettext just defines additional
+switches.
+
+Usage: pygettext [options] inputfile ...
+
+Options:
+
+ -a
+ --extract-all
+ Extract all strings.
+
+ -d name
+ --default-domain=name
+ Rename the default output file from messages.pot to name.pot.
+
+ -E
+ --escape
+ Replace non-ASCII characters with octal escape sequences.
+
+ -D
+ --docstrings
+ Extract module, class, method, and function docstrings. These do not
+ need to be wrapped in _() markers, and in fact cannot be for Python to
+ consider them docstrings. (See also the -X option).
+
+ -h
+ --help
+ Print this help message and exit.
+
+ -k word
+ --keyword=word
+ Keywords to look for in addition to the default set, which are:
+ %(DEFAULTKEYWORDS)s
+
+ You can have multiple -k flags on the command line.
+
+ -K
+ --no-default-keywords
+ Disable the default set of keywords (see above). Any keywords
+ explicitly added with the -k/--keyword option are still recognized.
+
+ --no-location
+ Do not write filename/lineno location comments.
+
+ -n
+ --add-location
+ Write filename/lineno location comments indicating where each
+ extracted string is found in the source. These lines appear before
+ each msgid. The style of comments is controlled by the -S/--style
+ option. This is the default.
+
+ -o filename
+ --output=filename
+ Rename the default output file from messages.pot to filename. If
+ filename is `-' then the output is sent to standard out.
+
+ -p dir
+ --output-dir=dir
+ Output files will be placed in directory dir.
+
+ -S stylename
+ --style stylename
+ Specify which style to use for location comments. Two styles are
+ supported:
+
+ Solaris # File: filename, line: line-number
+ GNU #: filename:line
+
+ The style name is case insensitive. GNU style is the default.
+
+ -v
+ --verbose
+ Print the names of the files being processed.
+
+ -V
+ --version
+ Print the version of pygettext and exit.
+
+ -w columns
+ --width=columns
+ Set width of output to columns.
+
+ -x filename
+ --exclude-file=filename
+ Specify a file that contains a list of strings that are not be
+ extracted from the input files. Each string to be excluded must
+ appear on a line by itself in the file.
+
+ -X filename
+ --no-docstrings=filename
+ Specify a file that contains a list of files (one per line) that
+ should not have their docstrings extracted. This is only useful in
+ conjunction with the -D option above.
+
+If `inputfile' is -, standard input is read.
+"""
+
+import os
+import sys
+import time
+import getopt
+import tokenize
+import operator
+
+# for selftesting
+try:
+ import fintl
+ _ = fintl.gettext
+except ImportError:
+ def _(s): return s
+
+__version__ = '1.4'
+
+default_keywords = ['_']
+DEFAULTKEYWORDS = ', '.join(default_keywords)
+
+EMPTYSTRING = ''
+
+
+
+# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
+# there.
+pot_header = _('''\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\\n"
+"POT-Creation-Date: %(time)s\\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
+"Language-Team: LANGUAGE <LL@li.org>\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=CHARSET\\n"
+"Content-Transfer-Encoding: ENCODING\\n"
+"Generated-By: pygettext.py %(version)s\\n"
+
+''')
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__) % globals()
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+escapes = []
+
+def make_escapes(pass_iso8859):
+ global escapes
+ if pass_iso8859:
+ # Allow iso-8859 characters to pass through so that e.g. 'msgid
+ # "Höhe"' would result not result in 'msgid "H\366he"'. Otherwise we
+ # escape any character outside the 32..126 range.
+ mod = 128
+ else:
+ mod = 256
+ for i in range(256):
+ if 32 <= (i % mod) <= 126:
+ escapes.append(chr(i))
+ else:
+ escapes.append("\\%03o" % i)
+ escapes[ord('\\')] = '\\\\'
+ escapes[ord('\t')] = '\\t'
+ escapes[ord('\r')] = '\\r'
+ escapes[ord('\n')] = '\\n'
+ escapes[ord('\"')] = '\\"'
+
+
+def escape(s):
+ global escapes
+ s = list(s)
+ for i in range(len(s)):
+ s[i] = escapes[ord(s[i])]
+ return EMPTYSTRING.join(s)
+
+
+def safe_eval(s):
+ # unwrap quotes, safely
+ return eval(s, {'__builtins__':{}}, {})
+
+
+def normalize(s):
+ # This converts the various Python string types into a format that is
+ # appropriate for .po files, namely much closer to C style.
+ lines = s.split('\n')
+ if len(lines) == 1:
+ s = '"' + escape(s) + '"'
+ else:
+ if not lines[-1]:
+ del lines[-1]
+ lines[-1] = lines[-1] + '\n'
+ for i in range(len(lines)):
+ lines[i] = escape(lines[i])
+ lineterm = '\\n"\n"'
+ s = '""\n"' + lineterm.join(lines) + '"'
+ return s
+
+
+
+class TokenEater:
+ def __init__(self, options):
+ self.__options = options
+ self.__messages = {}
+ self.__state = self.__waiting
+ self.__data = []
+ self.__lineno = -1
+ self.__freshmodule = 1
+ self.__curfile = None
+
+ def __call__(self, ttype, tstring, stup, etup, line):
+ # dispatch
+## import token
+## print >> sys.stderr, 'ttype:', token.tok_name[ttype], \
+## 'tstring:', tstring
+ self.__state(ttype, tstring, stup[0])
+
+ def __waiting(self, ttype, tstring, lineno):
+ opts = self.__options
+ # Do docstring extractions, if enabled
+ if opts.docstrings and not opts.nodocstrings.get(self.__curfile):
+ # module docstring?
+ if self.__freshmodule:
+ if ttype == tokenize.STRING:
+ self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
+ self.__freshmodule = 0
+ elif ttype not in (tokenize.COMMENT, tokenize.NL):
+ self.__freshmodule = 0
+ return
+ # class docstring?
+ if ttype == tokenize.NAME and tstring in ('class', 'def'):
+ self.__state = self.__suiteseen
+ return
+ if ttype == tokenize.NAME and tstring in opts.keywords:
+ self.__state = self.__keywordseen
+
+ def __suiteseen(self, ttype, tstring, lineno):
+ # ignore anything until we see the colon
+ if ttype == tokenize.OP and tstring == ':':
+ self.__state = self.__suitedocstring
+
+ def __suitedocstring(self, ttype, tstring, lineno):
+ # ignore any intervening noise
+ if ttype == tokenize.STRING:
+ self.__addentry(safe_eval(tstring), lineno, isdocstring=1)
+ self.__state = self.__waiting
+ elif ttype not in (tokenize.NEWLINE, tokenize.INDENT,
+ tokenize.COMMENT):
+ # there was no class docstring
+ self.__state = self.__waiting
+
+ def __keywordseen(self, ttype, tstring, lineno):
+ if ttype == tokenize.OP and tstring == '(':
+ self.__data = []
+ self.__lineno = lineno
+ self.__state = self.__openseen
+ else:
+ self.__state = self.__waiting
+
+ def __openseen(self, ttype, tstring, lineno):
+ if ttype == tokenize.OP and tstring == ')':
+ # We've seen the last of the translatable strings. Record the
+ # line number of the first line of the strings and update the list
+ # of messages seen. Reset state for the next batch. If there
+ # were no strings inside _(), then just ignore this entry.
+ if self.__data:
+ self.__addentry(EMPTYSTRING.join(self.__data))
+ self.__state = self.__waiting
+ elif ttype == tokenize.STRING:
+ self.__data.append(safe_eval(tstring))
+ # TBD: should we warn if we seen anything else?
+
+ def __addentry(self, msg, lineno=None, isdocstring=0):
+ if lineno is None:
+ lineno = self.__lineno
+ if not msg in self.__options.toexclude:
+ entry = (self.__curfile, lineno)
+ self.__messages.setdefault(msg, {})[entry] = isdocstring
+
+ def set_filename(self, filename):
+ self.__curfile = filename
+ self.__freshmodule = 1
+
+ def write(self, fp):
+ options = self.__options
+ timestamp = time.ctime(time.time())
+ # The time stamp in the header doesn't have the same format as that
+ # generated by xgettext...
+ print >> fp, pot_header % {'time': timestamp, 'version': __version__}
+ # Sort the entries. First sort each particular entry's keys, then
+ # sort all the entries by their first item.
+ reverse = {}
+ for k, v in self.__messages.items():
+ keys = v.keys()
+ keys.sort()
+ reverse.setdefault(tuple(keys), []).append((k, v))
+ rkeys = reverse.keys()
+ rkeys.sort()
+ for rkey in rkeys:
+ rentries = reverse[rkey]
+ rentries.sort()
+ for k, v in rentries:
+ isdocstring = 0
+ # If the entry was gleaned out of a docstring, then add a
+ # comment stating so. This is to aid translators who may wish
+ # to skip translating some unimportant docstrings.
+ if reduce(operator.__add__, v.values()):
+ isdocstring = 1
+ # k is the message string, v is a dictionary-set of (filename,
+ # lineno) tuples. We want to sort the entries in v first by
+ # file name and then by line number.
+ v = v.keys()
+ v.sort()
+ if not options.writelocations:
+ pass
+ # location comments are different b/w Solaris and GNU:
+ elif options.locationstyle == options.SOLARIS:
+ for filename, lineno in v:
+ d = {'filename': filename, 'lineno': lineno}
+ print >>fp, _(
+ '# File: %(filename)s, line: %(lineno)d') % d
+ elif options.locationstyle == options.GNU:
+ # fit as many locations on one line, as long as the
+ # resulting line length doesn't exceeds 'options.width'
+ locline = '#:'
+ for filename, lineno in v:
+ d = {'filename': filename, 'lineno': lineno}
+ s = _(' %(filename)s:%(lineno)d') % d
+ if len(locline) + len(s) <= options.width:
+ locline = locline + s
+ else:
+ print >> fp, locline
+ locline = "#:" + s
+ if len(locline) > 2:
+ print >> fp, locline
+ if isdocstring:
+ print >> fp, '#, docstring'
+ print >> fp, 'msgid', normalize(k)
+ print >> fp, 'msgstr ""\n'
+
+
+
+def main():
+ global default_keywords
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:],
+ 'ad:DEhk:Kno:p:S:Vvw:x:X:',
+ ['extract-all', 'default-domain=', 'escape', 'help',
+ 'keyword=', 'no-default-keywords',
+ 'add-location', 'no-location', 'output=', 'output-dir=',
+ 'style=', 'verbose', 'version', 'width=', 'exclude-file=',
+ 'docstrings', 'no-docstrings',
+ ])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ # for holding option values
+ class Options:
+ # constants
+ GNU = 1
+ SOLARIS = 2
+ # defaults
+ extractall = 0 # FIXME: currently this option has no effect at all.
+ escape = 0
+ keywords = []
+ outpath = ''
+ outfile = 'messages.pot'
+ writelocations = 1
+ locationstyle = GNU
+ verbose = 0
+ width = 78
+ excludefilename = ''
+ docstrings = 0
+ nodocstrings = {}
+
+ options = Options()
+ locations = {'gnu' : options.GNU,
+ 'solaris' : options.SOLARIS,
+ }
+
+ # parse options
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-a', '--extract-all'):
+ options.extractall = 1
+ elif opt in ('-d', '--default-domain'):
+ options.outfile = arg + '.pot'
+ elif opt in ('-E', '--escape'):
+ options.escape = 1
+ elif opt in ('-D', '--docstrings'):
+ options.docstrings = 1
+ elif opt in ('-k', '--keyword'):
+ options.keywords.append(arg)
+ elif opt in ('-K', '--no-default-keywords'):
+ default_keywords = []
+ elif opt in ('-n', '--add-location'):
+ options.writelocations = 1
+ elif opt in ('--no-location',):
+ options.writelocations = 0
+ elif opt in ('-S', '--style'):
+ options.locationstyle = locations.get(arg.lower())
+ if options.locationstyle is None:
+ usage(1, _('Invalid value for --style: %s') % arg)
+ elif opt in ('-o', '--output'):
+ options.outfile = arg
+ elif opt in ('-p', '--output-dir'):
+ options.outpath = arg
+ elif opt in ('-v', '--verbose'):
+ options.verbose = 1
+ elif opt in ('-V', '--version'):
+ print _('pygettext.py (xgettext for Python) %s') % __version__
+ sys.exit(0)
+ elif opt in ('-w', '--width'):
+ try:
+ options.width = int(arg)
+ except ValueError:
+ usage(1, _('--width argument must be an integer: %s') % arg)
+ elif opt in ('-x', '--exclude-file'):
+ options.excludefilename = arg
+ elif opt in ('-X', '--no-docstrings'):
+ fp = open(arg)
+ try:
+ while 1:
+ line = fp.readline()
+ if not line:
+ break
+ options.nodocstrings[line[:-1]] = 1
+ finally:
+ fp.close()
+
+ # calculate escapes
+ make_escapes(options.escape)
+
+ # calculate all keywords
+ options.keywords.extend(default_keywords)
+
+ # initialize list of strings to exclude
+ if options.excludefilename:
+ try:
+ fp = open(options.excludefilename)
+ options.toexclude = fp.readlines()
+ fp.close()
+ except IOError:
+ print >> sys.stderr, _(
+ "Can't read --exclude-file: %s") % options.excludefilename
+ sys.exit(1)
+ else:
+ options.toexclude = []
+
+ # slurp through all the files
+ eater = TokenEater(options)
+ for filename in args:
+ if filename == '-':
+ if options.verbose:
+ print _('Reading standard input')
+ fp = sys.stdin
+ closep = 0
+ else:
+ if options.verbose:
+ print _('Working on %s') % filename
+ fp = open(filename)
+ closep = 1
+ try:
+ eater.set_filename(filename)
+ try:
+ tokenize.tokenize(fp.readline, eater)
+ except tokenize.TokenError, e:
+ print >> sys.stderr, '%s: %s, line %d, column %d' % (
+ e[0], filename, e[1][0], e[1][1])
+ finally:
+ if closep:
+ fp.close()
+
+ # write the output
+ if options.outfile == '-':
+ fp = sys.stdout
+ closep = 0
+ else:
+ if options.outpath:
+ options.outfile = os.path.join(options.outpath, options.outfile)
+ fp = open(options.outfile, 'w')
+ closep = 1
+ try:
+ eater.write(fp)
+ finally:
+ if closep:
+ fp.close()
+
+
+if __name__ == '__main__':
+ main()
+ # some more test strings
+ _(u'a unicode string')
diff --git a/bin/qrunner b/bin/qrunner
new file mode 100644
index 00000000..bb2f62d3
--- /dev/null
+++ b/bin/qrunner
@@ -0,0 +1,270 @@
+#! @PYTHON@
+
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""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: %(PROGRAM)s [options]
+
+Options:
+
+ -r runner[:slice:range]
+ --runner=runner[:slice:range]
+ 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).
+
+ If using the slice:range form, you better make sure 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.
+
+ --once
+ -o
+ Run each named qrunner exactly once through its main loop. Otherwise,
+ each qrunner runs indefinitely, until the process receives a SIGTERM
+ or SIGINT.
+
+ -l/--list
+ Shows the available qrunner names and exit.
+
+ -v/--verbose
+ Spit out more debugging information to the logs/qrunner log file.
+
+ -s/--subproc
+ 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.
+
+ -h/--help
+ Print this message and exit.
+
+runner is required unless -l or -h is given, and it must be one of the names
+displayed by the -l switch.
+"""
+
+import sys
+import getopt
+import signal
+
+import paths
+from Mailman import mm_cfg
+from Mailman.i18n import _
+from Mailman.Logging.Syslog import syslog
+from Mailman.Logging.Utils import LogStdErr
+
+PROGRAM = sys.argv[0]
+COMMASPACE = ', '
+
+# Flag which says whether we're running under mailmanctl or not.
+AS_SUBPROC = 0
+
+LogStdErr('error', 'qrunner', manual_reprime=0)
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def make_qrunner(name, slice, range, once=0):
+ modulename = 'Mailman.Queue.' + name
+ try:
+ __import__(modulename)
+ except ImportError, e:
+ if AS_SUBPROC:
+ # Exit with SIGTERM exit code so mailmanctl won't try to restart us
+ print >> sys.stderr, 'Cannot import runner module', modulename
+ print >> sys.stderr, e
+ sys.exit(signal.SIGTERM)
+ else:
+ usage(1, e)
+ qrclass = getattr(sys.modules[modulename], name)
+ if once:
+ # Subclass to hack in the setting of the stop flag in _doperiodic()
+ class Once(qrclass):
+ def _doperiodic(self):
+ self.stop()
+ qrunner = Once(slice, range)
+ else:
+ qrunner = qrclass(slice, range)
+ return qrunner
+
+
+
+def set_signals(loop):
+ # Set up the SIGTERM handler for stopping the loop
+ def sigterm_handler(signum, frame, loop=loop):
+ # Exit the qrunner cleanly
+ loop.stop()
+ loop.status = signal.SIGTERM
+ syslog('qrunner', '%s qrunner caught SIGTERM. Stopping.', loop.name())
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ # Set up the SIGINT handler for stopping the loop. For us, SIGINT is
+ # the same as SIGTERM, but our parent treats the exit statuses
+ # differently (it restarts a SIGINT but not a SIGTERM).
+ def sigint_handler(signum, frame, loop=loop):
+ # Exit the qrunner cleanly
+ loop.stop()
+ loop.status = signal.SIGINT
+ syslog('qrunner', '%s qrunner caught SIGINT. Stopping.', loop.name())
+ signal.signal(signal.SIGINT, sigint_handler)
+ # SIGHUP just tells us to close our log files. They'll be
+ # automatically reopened at the next log print :)
+ def sighup_handler(signum, frame, loop=loop):
+ syslog.close()
+ syslog('qrunner', '%s qrunner caught SIGHUP. Reopening logs.',
+ loop.name())
+ signal.signal(signal.SIGHUP, sighup_handler)
+
+
+
+def main():
+ global AS_SUBPROC
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'hlor:vs',
+ ['help', 'list', 'once', 'runner=', 'verbose', 'subproc'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ once = 0
+ runners = []
+ verbose = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-l', '--list'):
+ for runnername, slices in mm_cfg.QRUNNERS:
+ if runnername.endswith('Runner'):
+ name = runnername[:-len('Runner')]
+ else:
+ name = runnername
+ print _('%(name)s runs the %(runnername)s qrunner')
+ print _('All runs all the above qrunners')
+ sys.exit(0)
+ elif opt in ('-o', '--once'):
+ once = 1
+ elif opt in ('-r', '--runner'):
+ runnerspec = arg
+ parts = runnerspec.split(':')
+ if len(parts) == 1:
+ runner = parts[0]
+ slice = 1
+ range = 1
+ elif len(parts) == 3:
+ runner = parts[0]
+ try:
+ slice = int(parts[1])
+ range = int(parts[2])
+ except ValueError:
+ usage(1, 'Bad runner specification: %(runnerspec)s')
+ else:
+ usage(1, 'Bad runner specification: %(runnerspec)s')
+ if runner == 'All':
+ for runnername, slices in mm_cfg.QRUNNERS:
+ runners.append((runnername, slice, range))
+ else:
+ if runner.endswith('Runner'):
+ runners.append((runner, slice, range))
+ else:
+ runners.append((runner + 'Runner', slice, range))
+ elif opt in ('-s', '--subproc'):
+ AS_SUBPROC = 1
+ elif opt in ('-v', '--verbose'):
+ verbose = 1
+
+ if len(args) <> 0:
+ usage(1)
+ if len(runners) == 0:
+ usage(1, _('No runner name given.'))
+
+ # Fast track for one infinite runner
+ if len(runners) == 1 and not once:
+ qrunner = make_qrunner(*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
+ syslog('qrunner', '%s qrunner started.', loop.name())
+ qrunner.run()
+ syslog('qrunner', '%s qrunner exiting.', loop.name())
+ else:
+ # Anything else we have to handle a bit more specially
+ qrunners = []
+ for runner, slice, range in runners:
+ qrunner = make_qrunner(runner, slice, range, 1)
+ qrunners.append(qrunner)
+ # This class is used to manage the main loop
+ class Loop:
+ status = 0
+ def __init__(self):
+ self.__isdone = 0
+ def name(self):
+ return 'Main loop'
+ def stop(self):
+ self.__isdone = 1
+ def isdone(self):
+ return self.__isdone
+ loop = Loop()
+ set_signals(loop)
+ syslog('qrunner', '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 verbose:
+ syslog('qrunner', 'Now doing a %s qrunner iteration',
+ qrunner.__class__.__bases__[0].__name__)
+ qrunner.run()
+ if once:
+ break
+ syslog('qrunner', 'Main qrunner loop exiting.')
+ # All done
+ sys.exit(loop.status)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/remove_members b/bin/remove_members
new file mode 100755
index 00000000..888dfa51
--- /dev/null
+++ b/bin/remove_members
@@ -0,0 +1,179 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Remove members from a list.
+
+Usage:
+ remove_members [options] [listname] [addr1 ...]
+
+Options:
+
+ --file=file
+ -f file
+ Remove member addresses found in the given file. If file is
+ `-', read stdin.
+
+ --all
+ -a
+ Remove all members of the mailing list.
+ (mutually exclusive with --fromall)
+
+ --fromall
+ Removes the given addresses from all the lists on this system
+ regardless of virtual domains if you have any. This option cannot be
+ used -a/--all. Also, you should not specify a listname when using this
+ option.
+
+ --nouserack
+ -n
+ Don't send the user acknowledgements.
+
+ --noadminack
+ -N
+ Don't send the admin acknowledgements.
+
+ --help
+ -h
+ Print this help message and exit.
+
+ listname is the name of the mailing list to use.
+
+ addr1 ... are additional addresses to remove.
+
+"""
+
+import sys
+import getopt
+
+import paths
+from Mailman import MailList
+from Mailman import Utils
+from Mailman import Errors
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+def ReadFile(filename):
+ lines = []
+ if filename == "-":
+ fp = sys.stdin
+ closep = 0
+ else:
+ fp = open(filename)
+ closep = 1
+ lines = filter(None, [line.strip() for line in fp.readlines()])
+ if closep:
+ fp.close()
+ return lines
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'naf:hN',
+ ['all', 'fromall', 'file=', 'help', 'nouserack', 'noadminack'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ if len(args) < 1:
+ usage(1)
+
+ filename = None
+ all = 0
+ alllists = 0
+ userack = 1
+ admin_notif = 1
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-f', '--file'):
+ filename = arg
+ elif opt in ('-a', '--all'):
+ all = 1
+ elif opt == '--fromall':
+ alllists = 1
+ elif opt in ('-n', '--nouserack'):
+ userack = 0
+ elif opt in ('-N', '--noadminack'):
+ admin_notif = 0
+
+ # You probably don't want to delete all the users of all the lists -- Marc
+ if all and alllists:
+ usage(1)
+
+ if alllists:
+ addresses = args
+ else:
+ listname = args[0].lower().strip()
+ addresses = args[1:]
+
+ if alllists:
+ listnames = Utils.list_names()
+ else:
+ listnames = [listname]
+
+ if filename:
+ try:
+ addresses = addresses + ReadFile(filename)
+ except IOError:
+ print _('Could not open file for reading: %(filename)s.')
+
+ for listname in listnames:
+ try:
+ # open locked
+ mlist = MailList.MailList(listname)
+ except Errors.MMListError:
+ print _('Error opening list %(listname)s... skipping.')
+ continue
+
+ if all:
+ addresses = mlist.getMembers()
+
+ try:
+ for addr in addresses:
+ if not mlist.isMember(addr):
+ if not alllists:
+ print _('No such member: %(addr)s')
+ continue
+ mlist.ApprovedDeleteMember(addr,
+ 'bin/remove_members',
+ admin_notif,
+ userack)
+ if alllists:
+ print _("User `%(addr)s' removed from list: %(listname)s.")
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/rmlist b/bin/rmlist
new file mode 100755
index 00000000..6ff0b5c1
--- /dev/null
+++ b/bin/rmlist
@@ -0,0 +1,138 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""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.
+
+Usage:
+ rmlist [-a] [-h] listname
+
+Where:
+ --archives
+ -a
+ Remove the list's archives too, or if the list has already been
+ deleted, remove any residual archives.
+
+ --help
+ -h
+ Print this help message and exit.
+
+"""
+
+import os
+import sys
+import getopt
+import shutil
+
+import paths
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def remove_it(listname, dir, msg):
+ if os.path.islink(dir):
+ print _('Removing %(msg)s')
+ os.unlink(dir)
+ elif os.path.isdir(dir):
+ print _('Removing %(msg)s')
+ shutil.rmtree(dir)
+ else:
+ print _('%(listname)s %(msg)s not found as %(dir)s')
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'ah',
+ ['archives', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ if len(args) <> 1:
+ usage(1)
+ listname = args[0].lower().strip()
+
+ removeArchives = 0
+ for opt, arg in opts:
+ if opt in ('-a', '--archives'):
+ removeArchives = 1
+ elif opt in ('-h', '--help'):
+ usage(0)
+
+ if not Utils.list_exists(listname):
+ if not removeArchives:
+ usage(1, _('No such list (or list already deleted): %(listname)s'))
+ else:
+ print _(
+ 'No such list: %(listname)s. Removing its residual archives.')
+
+ if not removeArchives:
+ print _('Not removing archives. Reinvoke with -a to remove them.')
+
+
+ REMOVABLES = []
+ if Utils.list_exists(listname):
+ mlist = MailList.MailList(listname, lock=0)
+
+ # Do the MTA-specific list deletion tasks
+ if mm_cfg.MTA:
+ modname = 'Mailman.MTA.' + mm_cfg.MTA
+ __import__(modname)
+ sys.modules[modname].remove(mlist)
+
+ REMOVABLES = [
+ (os.path.join('lists', listname), _('list info')),
+ ]
+
+ if removeArchives:
+ REMOVABLES.extend([
+ (os.path.join('archives', 'private', listname),
+ _('private archives')),
+ (os.path.join('archives', 'private', listname + '.mbox'),
+ _('private archives')),
+ (os.path.join('archives', 'public', listname),
+ _('public archives')),
+ (os.path.join('archives', 'public', listname + '.mbox'),
+ _('public archives')),
+ ])
+
+ for dirtmpl, msg in REMOVABLES:
+ dir = os.path.join(mm_cfg.VAR_PREFIX, dirtmpl)
+ remove_it(listname, dir, msg)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/sync_members b/bin/sync_members
new file mode 100755
index 00000000..1bf9a454
--- /dev/null
+++ b/bin/sync_members
@@ -0,0 +1,286 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Synchronize a mailing list's membership with a flat file.
+
+This script is useful if you have a Mailman mailing list and a sendmail
+:include: style list of addresses (also as is used in Majordomo). For every
+address in the file that does not appear in the mailing list, the address is
+added. For every address in the mailing list that does not appear in the
+file, the address is removed. Other options control what happens when an
+address is added or removed.
+
+Usage: %(PROGRAM)s [options] -f file listname
+
+Where `options' are:
+
+ --no-change
+ -n
+ Don't actually make the changes. Instead, print out what would be
+ done to the list.
+
+ --welcome-msg[=<yes|no>]
+ -w[=<yes|no>]
+ Sets whether or not to send the newly added members a welcome
+ message, overriding whatever the list's `send_welcome_msg' setting
+ is. With -w=yes or -w, the welcome message is sent. With -w=no, no
+ message is sent.
+
+ --goodbye-msg[=<yes|no>]
+ -g[=<yes|no>]
+ Sets whether or not to send the goodbye message to removed members,
+ overriding whatever the list's `send_goodbye_msg' setting is. With
+ -g=yes or -g, the goodbye message is sent. With -g=no, no message is
+ sent.
+
+ --digest[=<yes|no>]
+ -d[=<yes|no>]
+ Selects whether to make newly added members receive messages in
+ digests. With -d=yes or -d, they become digest members. With -d=no
+ (or if no -d option given) they are added as regular members.
+
+ --notifyadmin[=<yes|no>]
+ -a[=<yes|no>]
+ Specifies whether the admin should be notified for each subscription
+ or unsubscription. If you're adding a lot of addresses, you
+ definitely want to turn this off! With -a=yes or -a, the admin is
+ notified. With -a=no, the admin is not notified. With no -a option,
+ the default for the list is used.
+
+ --file <filename | ->
+ -f <filename | ->
+ This option is required. It specifies the flat file to synchronize
+ against. Email addresses must appear one per line. If filename is
+ `-' then stdin is used.
+
+ --help
+ -h
+ Print this message.
+
+ listname
+ Required. This specifies the list to synchronize.
+"""
+
+import sys
+
+import paths
+# Import this /after/ paths so that the sys.path is properly hacked
+import email.Utils
+
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import Utils
+from Mailman.UserDesc import UserDesc
+from Mailman.i18n import _
+
+
+
+PROGRAM = sys.argv[0]
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def yesno(opt):
+ i = opt.find('=')
+ yesno = opt[i+1:].lower()
+ if yesno in ('y', 'yes'):
+ return 1
+ elif yesno in ('n', 'no'):
+ return 0
+ else:
+ usage(1, _('Bad choice: %(yesno)s'))
+ # no return
+
+
+def main():
+ dryrun = 0
+ digest = 0
+ welcome = None
+ goodbye = None
+ filename = None
+ listname = None
+ notifyadmin = None
+
+ # TBD: can't use getopt with this command line syntax, which is broken and
+ # should be changed to be getopt compatible.
+ i = 1
+ while i < len(sys.argv):
+ opt = sys.argv[i]
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-n', '--no-change'):
+ dryrun = 1
+ i += 1
+ print _('Dry run mode')
+ elif opt in ('-d', '--digest'):
+ digest = 1
+ i += 1
+ elif opt.startswith('-d=') or opt.startswith('--digest='):
+ digest = yesno(opt)
+ i += 1
+ elif opt in ('-w', '--welcome-msg'):
+ welcome = 1
+ i += 1
+ elif opt.startswith('-w=') or opt.startswith('--welcome-msg='):
+ welcome = yesno(opt)
+ i += 1
+ elif opt in ('-g', '--goodbye-msg'):
+ goodbye = 1
+ i += 1
+ elif opt.startswith('-g=') or opt.startswith('--goodbye-msg='):
+ goodbye = yesno(opt)
+ i += 1
+ elif opt in ('-f', '--file'):
+ if filename is not None:
+ usage(1, _('Only one -f switch allowed'))
+ try:
+ filename = sys.argv[i+1]
+ except IndexError:
+ usage(1, _('No argument to -f given'))
+ i += 2
+ elif opt in ('-a', '--notifyadmin'):
+ notifyadmin = 1
+ i += 1
+ elif opt.startswith('-a=') or opt.startswith('--notifyadmin='):
+ notifyadmin = yesno(opt)
+ i += 1
+ elif opt[0] == '-':
+ usage(1, _('Illegal option: %(opt)s'))
+ else:
+ try:
+ listname = sys.argv[i].lower()
+ i += 1
+ except IndexError:
+ usage(1, _('No listname given'))
+ break
+
+ if listname is None or filename is None:
+ usage(1, _('Must have a listname and a filename'))
+
+ # read the list of addresses to sync to from the file
+ if filename == '-':
+ filemembers = sys.stdin.readlines()
+ else:
+ try:
+ fp = open(filename)
+ except IOError, (code, msg):
+ usage(1, _('Cannot read address file: %(filename)s: %(msg)s'))
+ try:
+ filemembers = fp.readlines()
+ finally:
+ fp.close()
+
+ # strip out lines we don't care about, they are comments (# in first
+ # non-whitespace) or are blank
+ for i in range(len(filemembers)-1, -1, -1):
+ addr = filemembers[i].strip()
+ if addr == '' or addr[:1] == '#':
+ del filemembers[i]
+ print _('Ignore : %(addr)30s')
+
+ # first filter out any invalid addresses
+ filemembers = email.Utils.getaddresses(filemembers)
+ invalid = 0
+ for name, addr in filemembers:
+ try:
+ Utils.ValidateEmail(addr)
+ except Errors.EmailAddressError:
+ print _('Invalid : %(addr)30s')
+ invalid = 1
+ if invalid:
+ print _('You must fix the preceding invalid addresses first.')
+ sys.exit(1)
+
+ # get the locked list object
+ try:
+ mlist = MailList.MailList(listname)
+ except Errors.MMListError, e:
+ print _('No such list: %(listname)s')
+ sys.exit(1)
+
+ try:
+ # Get the list of addresses currently subscribed
+ addrs = {}
+ needsadding = {}
+ matches = {}
+ for addr in mlist.getMemberCPAddresses(mlist.getMembers()):
+ addrs[addr.lower()] = addr
+
+ for name, addr in filemembers:
+ # Any address found in the file that is also in the list can be
+ # ignored. If not found in the list, it must be added later.
+ laddr = addr.lower()
+ if addrs.has_key(laddr):
+ del addrs[laddr]
+ matches[laddr] = 1
+ elif not matches.has_key(laddr):
+ needsadding[laddr] = (name, addr)
+
+ if not needsadding and not addrs:
+ print _('Nothing to do.')
+ sys.exit(0)
+
+ # addrs contains now all the addresses that need removing
+ for laddr, (name, addr) in needsadding.items():
+ pw = Utils.MakeRandomPassword()
+ # should not already be subscribed, otherwise our test above is
+ # broken. Bogosity is if the address is listed in the file more
+ # than once. Second and subsequent ones trigger an
+ # MMAlreadyAMember error. Just catch it and go on.
+ userdesc = UserDesc(addr, name, pw, digest)
+ try:
+ if not dryrun:
+ mlist.ApprovedAddMember(userdesc, welcome, notifyadmin)
+ s = email.Utils.formataddr((name, addr)).encode(enc, 'replace')
+ print _('Added : %(s)s')
+ except Errors.MMAlreadyAMember:
+ pass
+
+ for laddr, addr in addrs.items():
+ # Should be a member, otherwise our test above is broken
+ if not dryrun:
+ try:
+ mlist.ApprovedDeleteMember(addr, admin_notif=notifyadmin,
+ userack=goodbye)
+ except Errors.NotAMemberError:
+ # This can happen if the address is illegal (i.e. can't be
+ # parsed by email.Utils.parseaddr()) but for legacy
+ # reasons is in the database. Use a lower level remove to
+ # get rid of this member's entry
+ mlist.removeMember(addr)
+ name = mlist.getMemberName(laddr) or ''
+ enc = sys.getdefaultencoding()
+ s = email.Utils.formataddr((name, addr)).encode(enc, 'replace')
+ print _('Removed: %(s)s')
+
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/transcheck b/bin/transcheck
new file mode 100755
index 00000000..fbb0dcd8
--- /dev/null
+++ b/bin/transcheck
@@ -0,0 +1,405 @@
+#! @PYTHON@
+#
+# transcheck - (c) 2002 by Simone Piunno <pioppo@ferrara.linux.it>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the version 2.0 of the GNU General Public License as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""
+Check a given Mailman translation, making sure that variables and
+tags referenced in translation are the same variables and tags in
+the original templates and catalog.
+
+Usage:
+
+cd $MAILMAN_DIR
+%(program)s [-q] <lang>
+
+Where <lang> is your country code (e.g. 'it' for Italy) and -q is
+to ask for a brief summary.
+"""
+
+import sys
+import re
+import os
+import getopt
+
+import paths
+from Mailman.i18n import _
+
+program = sys.argv[0]
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+class TransChecker:
+ "check a translation comparing with the original string"
+ def __init__(self, regexp):
+ self.dict = {}
+ self.errs = []
+ self.regexp = re.compile(regexp)
+
+ def checkin(self, string):
+ "scan a string from the original file"
+ for key in self.regexp.findall(string):
+ if self.dict.has_key(key):
+ self.dict[key] += 1
+ else:
+ self.dict[key] = 1
+
+ def checkout(self, string):
+ "scan a translated string"
+ for key in self.regexp.findall(string):
+ if self.dict.has_key(key):
+ self.dict[key] -= 1
+ else:
+ self.errs.append(
+ "%(key)s was not found" %
+ { 'key' : key }
+ )
+
+ def computeErrors(self):
+ "check for differences between checked in and checked out"
+ for key in self.dict.keys():
+ if self.dict[key] < 0:
+ self.errs.append(
+ "Too much %(key)s" %
+ { 'key' : key }
+ )
+ if self.dict[key] > 0:
+ self.errs.append(
+ "Too few %(key)s" %
+ { 'key' : key }
+ )
+ return self.errs
+
+ def status(self):
+ if self.errs:
+ return "FAILED"
+ else:
+ return "OK"
+
+ def errorsAsString(self):
+ msg = ""
+ for err in self.errs:
+ msg += " - %(err)s" % { 'err': err }
+ return msg
+
+ def reset(self):
+ self.dict = {}
+ self.errs = []
+
+
+
+class POParser:
+ "parse a .po file extracting msgids and msgstrs"
+ def __init__(self, filename=""):
+ self.status = 0
+ self.files = []
+ self.msgid = ""
+ self.msgstr = ""
+ self.line = 1
+ self.f = None
+ self.esc = { "n": "\n", "r": "\r", "t": "\t" }
+ if filename:
+ self.f = open(filename)
+
+ def open(self, filename):
+ self.f = open(filename)
+
+ def close(self):
+ self.f.close()
+
+ def parse(self):
+ """States table for the finite-states-machine parser:
+ 0 idle
+ 1 filename-or-comment
+ 2 msgid
+ 3 msgstr
+ 4 end
+ """
+ # each time we can safely re-initialize those vars
+ self.files = []
+ self.msgid = ""
+ self.msgstr = ""
+
+
+ # can't continue if status == 4, this is a dead status
+ if self.status == 4:
+ return 0
+
+ while 1:
+ # continue scanning, char-by-char
+ c = self.f.read(1)
+ if not c:
+ # EOF -> maybe we have a msgstr to save?
+ self.status = 4
+ if self.msgstr:
+ return 1
+ else:
+ return 0
+
+ # keep the line count up-to-date
+ if c == "\n":
+ self.line += 1
+
+ # a pound was detected the previous char...
+ if self.status == 1:
+ if c == ":":
+ # was a line of filenames
+ row = self.f.readline()
+ self.files += row.split()
+ self.line += 1
+ elif c == "\n":
+ # was a single pount on the line
+ pass
+ else:
+ # was a comment... discard
+ self.f.readline()
+ self.line += 1
+ # in every case, we switch to idle status
+ self.status = 0;
+ continue
+
+ # in idle status we search for a '#' or for a 'm'
+ if self.status == 0:
+ if c == "#":
+ # this could be a comment or a filename
+ self.status = 1;
+ continue
+ elif c == "m":
+ # this should be a msgid start...
+ s = self.f.read(4)
+ assert s == "sgid"
+ # so now we search for a '"'
+ self.status = 2
+ continue
+ # in idle only those other chars are possibile
+ assert c in [ "\n", " ", "\t" ]
+
+ # searching for the msgid string
+ if self.status == 2:
+ if c == "\n":
+ # a double LF is not possible here
+ c = self.f.read(1)
+ assert c != "\n"
+ if c == "\"":
+ # ok, this is the start of the string,
+ # now search for the end
+ while 1:
+ c = self.f.read(1)
+ if not c:
+ # EOF, bailout
+ self.status = 4
+ return 0
+ if c == "\\":
+ # a quoted char...
+ c = self.f.read(1)
+ if self.esc.has_key(c):
+ self.msgid += self.esc[c]
+ else:
+ self.msgid += c
+ continue
+ if c == "\"":
+ # end of string found
+ break
+ # a normal char, add it
+ self.msgid += c
+ if c == "m":
+ # this should be a msgstr identifier
+ s = self.f.read(5)
+ assert s == "sgstr"
+ # ok, now search for the msgstr string
+ self.status = 3
+
+ # searching for the msgstr string
+ if self.status == 3:
+ if c == "\n":
+ # a double LF is the end of the msgstr!
+ c = self.f.read(1)
+ if c == "\n":
+ # ok, time to go idle and return
+ self.status = 0
+ self.line += 1
+ return 1
+ if c == "\"":
+ # start of string found
+ while 1:
+ c = self.f.read(1)
+ if not c:
+ # EOF, bail out
+ self.status = 4
+ return 1
+ if c == "\\":
+ # a quoted char...
+ c = self.f.read(1)
+ if self.esc.has_key(c):
+ self.msgid += self.esc[c]
+ else:
+ self.msgid += c
+ continue
+ if c == "\"":
+ # end of string
+ break
+ # a normal char, add it
+ self.msgstr += c
+
+
+
+
+def check_file(translatedFile, originalFile, html=0, quiet=0):
+ """check a translated template against the original one
+ search also <MM-*> tags if html is not zero"""
+
+ if html:
+ c = TransChecker("(%\([^)]+\)[0-9]*[sd]|</?MM-[^>]+>)")
+ else:
+ c = TransChecker("(%\([^)]+\)[0-9]*[sd])")
+
+ try:
+ f = open(originalFile)
+ except IOError:
+ if not quiet:
+ print " - Can'open original file " + originalFile
+ return 1
+
+ while 1:
+ line = f.readline()
+ if not line: break
+ c.checkin(line)
+
+ f.close()
+
+ try:
+ f = open(translatedFile)
+ except IOError:
+ if not quiet:
+ print " - Can'open translated file " + translatedFile
+ return 1
+
+ while 1:
+ line = f.readline()
+ if not line: break
+ c.checkout(line)
+
+ f.close()
+
+ n = 0
+ msg = ""
+ for desc in c.computeErrors():
+ n +=1
+ if not quiet:
+ print " - %(desc)s" % { 'desc': desc }
+ return n
+
+
+
+def check_po(file, quiet=0):
+ "scan the po file comparing msgids with msgstrs"
+ n = 0
+ p = POParser(file)
+ c = TransChecker("(%\([^)]+\)[0-9]*[sdu]|%[0-9]*[sdu])")
+ while p.parse():
+ if p.msgstr:
+ c.reset()
+ c.checkin(p.msgid)
+ c.checkout(p.msgstr)
+ for desc in c.computeErrors():
+ n += 1
+ if not quiet:
+ print " - near line %(line)d %(file)s: %(desc)s" % {
+ 'line': p.line,
+ 'file': p.files,
+ 'desc': desc
+ }
+ p.close()
+ return n
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'qh', ['quiet', 'help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ quiet = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-q', '--quiet'):
+ quiet = 1
+
+ if len(args) <> 1:
+ usage(1)
+
+ lang = args[0]
+
+ isHtml = re.compile("\.html$");
+ isTxt = re.compile("\.txt$");
+
+ numerrors = 0
+ numfiles = 0
+ try:
+ files = os.listdir("templates/" + lang + "/")
+ except:
+ print "can't open templates/%s/" % lang
+ for file in files:
+ fileEN = "templates/en/" + file
+ fileIT = "templates/" + lang + "/" + file
+ errlist = []
+ if isHtml.search(file):
+ if not quiet:
+ print "HTML checking " + fileIT + "... "
+ n = check_file(fileIT, fileEN, html=1, quiet=quiet)
+ if n:
+ numerrors += n
+ numfiles += 1
+ elif isTxt.search(file):
+ if not quiet:
+ print "TXT checking " + fileIT + "... "
+ n = check_file(fileIT, fileEN, html=0, quiet=quiet)
+ if n:
+ numerrors += n
+ numfiles += 1
+
+ else:
+ continue
+
+ file = "messages/" + lang + "/LC_MESSAGES/mailman.po"
+ if not quiet:
+ print "PO checking " + file + "... "
+ n = check_po(file, quiet=quiet)
+ if n:
+ numerrors += n
+ numfiles += 1
+
+ if quiet:
+ print "%(errs)u warnings in %(files)u files" % {
+ 'errs': numerrors,
+ 'files': numfiles
+ }
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/unshunt b/bin/unshunt
new file mode 100644
index 00000000..cd8a538e
--- /dev/null
+++ b/bin/unshunt
@@ -0,0 +1,87 @@
+#! @PYTHON@
+
+# Copyright (C) 2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Move a message from the shunt queue to the original queue.
+
+Usage: %(PROGRAM)s [options] [directory]
+
+Where:
+
+ -h / --help
+ Print help and exit.
+
+Optional `directory' specifies a directory to dequeue from other than
+qfiles/shunt.
+"""
+
+import sys
+import getopt
+
+import paths
+from Mailman import mm_cfg
+from Mailman.Queue.sbcache import get_switchboard
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'h', ['help'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+
+ if len(args) == 0:
+ qdir = mm_cfg.SHUNTQUEUE_DIR
+ elif len(args) == 1:
+ qdir = args[0]
+ else:
+ usage(1)
+
+ sb = get_switchboard(qdir)
+ for filebase in sb.files():
+ try:
+ msg, msgdata = sb.dequeue(filebase)
+ whichq = msgdata.get('whichq', mm_cfg.INQUEUE_DIR)
+ tosb = get_switchboard(whichq)
+ tosb.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)s, skipping:\n%(e)s')
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/update b/bin/update
new file mode 100755
index 00000000..b0b425d3
--- /dev/null
+++ b/bin/update
@@ -0,0 +1,588 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Perform all necessary upgrades.
+
+Usage: %(PROGRAM)s [options]
+
+Options:
+ -f/--force
+ 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.
+
+ -h/--help
+ Print this text and exit.
+
+Use this script to help you update to the latest release of Mailman from
+some previous version. It knows about versions back to 1.0b4 (?).
+"""
+
+import os
+import md5
+import sys
+import time
+import errno
+import getopt
+import shutil
+import marshal
+
+import paths
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman.LockFile import TimeOutError
+from Mailman.i18n import _
+from Mailman.Queue.Switchboard import Switchboard
+from Mailman.OldStyleMemberships import OldStyleMemberships
+from Mailman.MemberAdaptor import BYBOUNCE, ENABLED
+
+FRESH = 0
+NOTFRESH = -1
+
+LMVFILE = os.path.join(mm_cfg.DATA_DIR, 'last_mailman_version')
+PROGRAM = sys.argv[0]
+
+
+
+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 = mm_cfg.HEX_VERSION
+ try:
+ fp = open(LMVFILE)
+ 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(mm_cfg.LOG_DIR):
+ lastversion = NOTFRESH
+ except OSError:
+ pass
+ return (lastversion, thisversion)
+
+
+
+def makeabs(relpath):
+ return os.path.join(mm_cfg.PREFIX, relpath)
+
+def make_varabs(relpath):
+ return os.path.join(mm_cfg.VAR_PREFIX, relpath)
+
+
+def move_language_templates(mlist):
+ listname = mlist.internal_name()
+ print _('Fixing language templates: %(listname)s')
+ # 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(mm_cfg.TEMPLATE_DIR, 'en')):
+ # BAW: get rid of old templates, e.g. admlogin.txt and
+ # handle_opts.html
+ try:
+ fp = open(os.path.join(mm_cfg.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(mm_cfg.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(mm_cfg.TEMPLATE_DIR, gtemplate))
+ # Match against the templates/*.prev template
+ try:
+ fp = open(os.path.join(mm_cfg.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(mm_cfg.TEMPLATE_DIR,
+ gtemplate + '.prev'))
+
+
+
+def dolist(listname):
+ errors = 0
+ mlist = MailList.MailList(listname, lock=0)
+ try:
+ mlist.Lock(0.5)
+ except TimeOutError:
+ print >> sys.stderr, _('WARNING: could not acquire lock for list: '
+ '%(listname)s')
+ 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)s BYBOUNCEs disabled addrs with no bounce info')
+ for addr in noinfo.keys():
+ mlist.setDeliveryStatus(addr, ENABLED)
+
+ # Update the held requests database
+ print _("""Updating the held requests database.""")
+ mlist._UpdateRecords()
+
+ 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):
+ ou = os.umask(0)
+ os.mkdir(mbox_dir, 02775)
+ os.umask(ou)
+ else:
+ # this shouldn't happen, but hey, just in case
+ if not os.path.isdir(mbox_dir):
+ print _("""\
+For some reason, %(mbox_dir)s exists as a file. This won't work with
+b6, so I'm renaming it to %(mbox_dir)s.tmp and proceeding.""")
+ os.rename(mbox_dir, "%s.tmp" % (mbox_dir))
+ ou = os.umask(0)
+ os.mkdir(mbox_dir, 02775)
+ os.umask(ou)
+
+ # 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)s 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)s -- as the active archive, and renaming
+ %(o_pub_mbox_file)s
+to
+ %(o_pub_mbox_file)s.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 _("""\
+%s has both public and private mbox archives. Since this list
+currently uses public archiving, I'm installing the public mbox file
+archive file (%s) as the active one, and renaming
+ %s
+ to
+ %s.preb6
+
+You can integrate that into the archives if you want by using the 'arch'
+script.
+""") % (mlist._internal_name, o_pub_mbox_file, o_pri_mbox_file,
+ o_pri_mbox_file)
+ 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)s
+ to
+ %(newname)s""")
+ else:
+ # directory
+ print _("""\
+ looks like you have a really recent CVS 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)s
+ to
+ %(newname)s""")
+ else: # directory
+ print _("""\
+ looks like you have a really recent CVS 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(mm_cfg.PREFIX, "templates")
+ list_dir = os.path.join(mm_cfg.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 not os.path.exists(n_tmpl):
+ os.rename(o_tmpl, n_tmpl)
+ print _('- moved %(o_tmpl)s to %(n_tmpl)s')
+ else:
+ print _("""\
+- both %(o_tmpl)s and %(n_tmpl)s 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' % (mm_cfg.PREFIX, module)
+ pyc = src + "c"
+ if os.path.isdir(src):
+ print _('removing directory %(src)s and everything underneath')
+ shutil.rmtree(src)
+ elif os.path.exists(src):
+ print _('removing %(src)s')
+ try:
+ os.unlink(src)
+ except os.error, rest:
+ print _("Warning: couldn't remove %(src)s -- %(rest)s")
+ if module.endswith('.py') and os.path.exists(pyc):
+ try:
+ os.unlink(pyc)
+ except os.error, rest:
+ print _("couldn't remove old file %(pyc)s -- %(rest)s")
+
+
+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(mm_cfg.INQUEUE_DIR)
+ for file in os.listdir(mm_cfg.QUEUE_DIR):
+ # Updating means just mokving the .db and .msg files to qfiles/in where
+ # it should be dequeued, converted, and processed normally.
+ if file.endswith('.msg'):
+ oldmsgfile = os.path.join(mm_cfg.QUEUE_DIR, file)
+ newmsgfile = os.path.join(mm_cfg.INQUEUE_DIR, prefix + file)
+ os.rename(oldmsgfile, newmsgfile)
+ elif file.endswith('.db'):
+ olddbfile = os.path.join(mm_cfg.QUEUE_DIR, file)
+ newdbfile = os.path.join(mm_cfg.INQUEUE_DIR, prefix + file)
+ os.rename(olddbfile, newdbfile)
+
+
+
+def main():
+ 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)
+ listnames = Utils.list_names()
+ if not listnames:
+ 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" % mm_cfg.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" % mm_cfg.PREFIX,
+ archive_path_fixer, "")
+ print _('done')
+ for listname in listnames:
+ print _('Updating mailing list: %(listname)s')
+ errors = errors + dolist(listname)
+ print
+ print _('Updating Usenet watermarks')
+ wmfile = os.path.join(mm_cfg.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)s')
+ 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 pending database format and file name has changed.
+ #
+ oldpendingfile = os.path.join(mm_cfg.DATA_DIR, 'pending_subscriptions.db')
+ try:
+ fp = open(oldpendingfile)
+ except IOError, e:
+ if e.errno <> errno.ENOENT: raise
+ else:
+ print _('Updating old pending_subscriptions.db database')
+ from Mailman import Pending
+ db = marshal.load(fp)
+ Pending._update(db)
+ fp.close()
+ os.unlink(oldpendingfile)
+ #
+ # In Mailman 2.1, the qfiles directory has a different structure and a
+ # different content.
+ #
+ 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
+
+""")
+ return errors
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__) % globals()
+ if msg:
+ print >> sys.stderr, msg
+ sys.exit(code)
+
+
+
+if __name__ == '__main__':
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'hf',
+ ['help', 'force'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ if args:
+ usage(1, 'Unexpected arguments: %s' % args)
+
+ force = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-f', '--force'):
+ force = 1
+
+ # calculate the versions
+ lastversion, thisversion = calcversions()
+ hexlversion = hex(lastversion)
+ hextversion = hex(thisversion)
+ if lastversion == thisversion and not force:
+ # nothing to do
+ print _('No updates are necessary.')
+ sys.exit(0)
+ if lastversion > thisversion and not force:
+ print _("""\
+Downgrade detected, from version %(hexlversion)s to version %(hextversion)s
+This is probably not safe.
+Exiting.""")
+ sys.exit(1)
+ print _('Upgrading from version %(hexlversion)s to %(hextversion)s')
+ errors = main()
+ if not errors:
+ # Record the version we just upgraded to
+ fp = open(LMVFILE, 'w')
+ fp.write(hex(mm_cfg.HEX_VERSION) + '\n')
+ fp.close()
+ else:
+ lockdir = mm_cfg.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)s 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/bin/version b/bin/version
new file mode 100644
index 00000000..6e10e869
--- /dev/null
+++ b/bin/version
@@ -0,0 +1,26 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Print the Mailman version.
+"""
+
+import paths
+import Mailman.mm_cfg
+from Mailman.i18n import _
+
+print _('Using Mailman version:'), Mailman.mm_cfg.VERSION
diff --git a/bin/withlist b/bin/withlist
new file mode 100644
index 00000000..a9bd6361
--- /dev/null
+++ b/bin/withlist
@@ -0,0 +1,275 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""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 MailList
+object from Python's interactive interpreter. When running interactively, a
+MailList object called `m' will be available in the global namespace. It also
+loads the class MailList into the global namespace.
+
+Programmatically, you can write a function to operate on a MailList 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 ...]
+
+Options:
+
+ -l / --lock
+ Lock the list when opening. Normally the list is opened unlocked
+ (e.g. for read-only operations). You can always lock the file after
+ the fact by typing `m.Lock()'
+
+ Note that if you use this option, you should explicitly call m.Save()
+ before exiting, since the interpreter's clean up procedure will not
+ automatically save changes to the MailList object (but it will unlock
+ the list).
+
+ -i / --interactive
+ Leaves you at an interactive prompt after all other processing is
+ complete. This is the default unless the -r option is given.
+
+ --run [module.]callable
+ -r [module.]callable
+ This can be used to run a script with the opened MailList object.
+ This works by attempting to import `module' (which must 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
+ MailList 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.
+
+ --all / -a
+ 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.
+
+ --quiet / -q
+ Suppress all status messages.
+
+ --help / -h
+ Print this message and exit
+
+
+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.GetListEmail()
+
+def requestaddr(mlist):
+ print mlist.GetRequestEmail()
+
+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 (unlocked)
+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 (unlocked)
+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 NotAMember
+
+def changepw(mlist, addr, newpasswd):
+ try:
+ mlist.setMemberPassword(addr, newpasswd)
+ mlist.Save()
+ except NotAMember:
+ print 'No address matched:', addr
+
+and run this from the command line:
+%% bin/withlist -l -r changepw mylist somebody@somewhere.org foobar
+"""
+
+import sys
+import getopt
+import code
+
+import paths
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman.i18n import _
+
+# `m' will be the MailList object and `r' will be the results of the callable
+m = None
+r = None
+VERBOSE = 1
+LOCK = 0
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__)
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+def atexit():
+ """Unlock a locked list, but do not implicitly Save() it.
+
+ This does not get run if the interpreter exits because of a signal, or if
+ os._exit() is called. It will get called if an exception occurs though.
+ """
+ global m
+ if not m:
+ return
+ if m.Locked():
+ if VERBOSE:
+ listname = m.internal_name()
+ print >> sys.stderr, _(
+ 'Unlocking (but not saving) list: %(listname)s')
+ m.Unlock()
+ if VERBOSE:
+ print >> sys.stderr, _('Finalizing')
+ del m
+
+
+
+def do_list(listname, args, func):
+ global m
+ # first try to open mailing list
+ if VERBOSE:
+ print >> sys.stderr, _('Loading list %(listname)s'),
+ if LOCK:
+ print >> sys.stderr, _('(locked)')
+ else:
+ print >> sys.stderr, _('(unlocked)')
+
+ try:
+ m = MailList.MailList(listname, lock=LOCK)
+ except Errors.MMUnknownListError:
+ print >> sys.stderr, _('Unknown list: %(listname)s')
+ m = None
+
+ # try to import the module and run the callable
+ if func:
+ return func(m, *args)
+ return None
+
+
+
+def main():
+ global VERBOSE
+ global LOCK
+ global r
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:], 'hlr:qia',
+ ['help', 'lock', 'run=', 'quiet', 'interactive', 'all'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ run = None
+ interact = None
+ all = 0
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-l', '--lock'):
+ LOCK = 1
+ elif opt in ('-r', '--run'):
+ run = arg
+ elif opt in ('-q', '--quiet'):
+ VERBOSE = 0
+ elif opt in ('-i', '--interactive'):
+ interact = 1
+ elif opt in ('-a', '--all'):
+ all = 1
+
+ if len(args) < 1 and not all:
+ usage(1, _('No list name supplied.'))
+
+ if all and not run:
+ usage(1, _('--all requires --run'))
+
+ # The default for interact is 1 unless -r was given
+ if interact is None:
+ if run is None:
+ interact = 1
+ else:
+ interact = 0
+
+ # try to import the module for the callable
+ func = None
+ if run:
+ i = run.find('.')
+ if i < 0:
+ module = run
+ callable = run
+ else:
+ module = run[:i]
+ callable = run[i+1:]
+ if VERBOSE:
+ print >> sys.stderr, _('Importing %(module)s...')
+ mod = __import__(module)
+ if VERBOSE:
+ print >> sys.stderr, _('Running %(module)s.%(callable)s()...')
+ func = getattr(mod, callable)
+
+ if all:
+ r = [do_list(listname, args, func) for listname in Utils.list_names()]
+ else:
+ listname = args.pop(0).lower().strip()
+ r = do_list(listname, args, func)
+
+ # Now go to interactive mode, perhaps
+ if interact:
+ # Attempt to import the readline module, so we emulate the interactive
+ # console as closely as possible. Don't worry if it doesn't import.
+ # readline works by side-effect.
+ try:
+ import readline
+ except ImportError:
+ pass
+ namespace = globals().copy()
+ namespace.update(locals())
+ code.InteractiveConsole(namespace).interact(
+ _("The variable `m' is the %(listname)s MailList instance"))
+
+
+
+sys.exitfunc = atexit
+main()