diff options
Diffstat (limited to 'bin')
-rw-r--r-- | bin/.cvsignore | 2 | ||||
-rw-r--r-- | bin/Makefile.in | 78 | ||||
-rwxr-xr-x | bin/add_members | 297 | ||||
-rw-r--r-- | bin/arch | 187 | ||||
-rw-r--r-- | bin/b4b5-archfix | 96 | ||||
-rw-r--r-- | bin/change_pw | 209 | ||||
-rwxr-xr-x | bin/check_db | 153 | ||||
-rwxr-xr-x | bin/check_perms | 362 | ||||
-rw-r--r-- | bin/cleanarch | 165 | ||||
-rwxr-xr-x | bin/clone_member | 219 | ||||
-rw-r--r-- | bin/config_list | 339 | ||||
-rw-r--r-- | bin/convert.py | 44 | ||||
-rw-r--r-- | bin/dumpdb | 134 | ||||
-rwxr-xr-x | bin/find_member | 184 | ||||
-rw-r--r-- | bin/fix_url.py | 92 | ||||
-rw-r--r-- | bin/genaliases | 102 | ||||
-rw-r--r-- | bin/inject | 107 | ||||
-rw-r--r-- | bin/list_admins | 101 | ||||
-rw-r--r-- | bin/list_lists | 122 | ||||
-rwxr-xr-x | bin/list_members | 232 | ||||
-rw-r--r-- | bin/list_owners | 120 | ||||
-rw-r--r-- | bin/mailmanctl | 524 | ||||
-rwxr-xr-x | bin/mmsitepass | 105 | ||||
-rwxr-xr-x | bin/newlist | 219 | ||||
-rwxr-xr-x | bin/pygettext.py | 545 | ||||
-rw-r--r-- | bin/qrunner | 270 | ||||
-rwxr-xr-x | bin/remove_members | 179 | ||||
-rwxr-xr-x | bin/rmlist | 138 | ||||
-rwxr-xr-x | bin/sync_members | 286 | ||||
-rwxr-xr-x | bin/transcheck | 405 | ||||
-rw-r--r-- | bin/unshunt | 87 | ||||
-rwxr-xr-x | bin/update | 588 | ||||
-rw-r--r-- | bin/version | 26 | ||||
-rw-r--r-- | bin/withlist | 275 |
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 </> to <> + desc = re.sub('<', '<', desc) + desc = re.sub('>', '>', 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() |