summaryrefslogtreecommitdiff
path: root/src/mailman/attic/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/attic/bin')
-rwxr-xr-xsrc/mailman/attic/bin/clone_member219
-rw-r--r--src/mailman/attic/bin/discard120
-rw-r--r--src/mailman/attic/bin/fix_url.py93
-rw-r--r--src/mailman/attic/bin/list_admins101
-rw-r--r--src/mailman/attic/bin/msgfmt.py203
-rw-r--r--src/mailman/attic/bin/po2templ.py90
-rw-r--r--src/mailman/attic/bin/pygettext.py545
-rwxr-xr-xsrc/mailman/attic/bin/remove_members186
-rw-r--r--src/mailman/attic/bin/reset_pw.py83
-rwxr-xr-xsrc/mailman/attic/bin/sync_members286
-rw-r--r--src/mailman/attic/bin/templ2pot.py120
-rwxr-xr-xsrc/mailman/attic/bin/transcheck412
12 files changed, 2458 insertions, 0 deletions
diff --git a/src/mailman/attic/bin/clone_member b/src/mailman/attic/bin/clone_member
new file mode 100755
index 000000000..1f2a03aca
--- /dev/null
+++ b/src/mailman/attic/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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 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/src/mailman/attic/bin/discard b/src/mailman/attic/bin/discard
new file mode 100644
index 000000000..c30198441
--- /dev/null
+++ b/src/mailman/attic/bin/discard
@@ -0,0 +1,120 @@
+#! @PYTHON@
+#
+# Copyright (C) 2003 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+"""Discard held messages.
+
+Usage:
+ discard [options] file ...
+
+Options:
+ --help / -h
+ Print this help message and exit.
+
+ --quiet / -q
+ Don't print status messages.
+"""
+
+# TODO: add command line arguments for specifying other actions than DISCARD,
+# and also for specifying other __handlepost() arguments, i.e. comment,
+# preserve, forward, addr
+
+import os
+import re
+import sys
+import getopt
+
+import paths
+from Mailman import mm_cfg
+from Mailman.MailList import MailList
+from Mailman.i18n import _
+
+try:
+ True, False
+except NameError:
+ True = 1
+ False = 0
+
+cre = re.compile(r'heldmsg-(?P<listname>.*)-(?P<id>[0-9]+)\.(pck|txt)$')
+
+
+
+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:], 'hq', ['help', 'quiet'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ quiet = False
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-q', '--quiet'):
+ quiet = True
+
+ files = args
+ if not files:
+ print _('Nothing to do.')
+
+ # Mapping from listnames to sequence of request ids
+ discards = {}
+
+ # Cruise through all the named files, collating by mailing list. We'll
+ # lock the list once, process all holds for that list and move on.
+ for f in files:
+ basename = os.path.basename(f)
+ mo = cre.match(basename)
+ if not mo:
+ print >> sys.stderr, _('Ignoring non-held message: %(f)s')
+ continue
+ listname, id = mo.group('listname', 'id')
+ try:
+ id = int(id)
+ except (ValueError, TypeError):
+ print >> sys.stderr, _('Ignoring held msg w/bad id: %(f)s')
+ continue
+ discards.setdefault(listname, []).append(id)
+
+ # Now do the discards
+ for listname, ids in discards.items():
+ mlist = MailList(listname)
+ try:
+ for id in ids:
+ # No comment, no preserve, no forward, no forwarding address
+ mlist.HandleRequest(id, mm_cfg.DISCARD, '', False, False, '')
+ if not quiet:
+ print _('Discarded held msg #%(id)s for list %(listname)s')
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/attic/bin/fix_url.py b/src/mailman/attic/bin/fix_url.py
new file mode 100644
index 000000000..30618a1a3
--- /dev/null
+++ b/src/mailman/attic/bin/fix_url.py
@@ -0,0 +1,93 @@
+#! @PYTHON@
+#
+# Copyright (C) 2001-2009 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# 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.configuration import config
+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 = config.DEFAULT_URL_PATTERN % urlhost
+ mailhost = config.VIRTUAL_HOSTS.get(urlhost.lower(), urlhost)
+ else:
+ web_page_url = config.DEFAULT_URL_PATTERN % config.DEFAULT_URL_HOST
+ mailhost = config.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/src/mailman/attic/bin/list_admins b/src/mailman/attic/bin/list_admins
new file mode 100644
index 000000000..c628a42dc
--- /dev/null
+++ b/src/mailman/attic/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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 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/src/mailman/attic/bin/msgfmt.py b/src/mailman/attic/bin/msgfmt.py
new file mode 100644
index 000000000..8a2d4e66e
--- /dev/null
+++ b/src/mailman/attic/bin/msgfmt.py
@@ -0,0 +1,203 @@
+#! /usr/bin/env python
+# -*- coding: iso-8859-1 -*-
+# Written by Martin v. Löwis <loewis@informatik.hu-berlin.de>
+
+"""Generate binary message catalog from textual translation description.
+
+This program converts a textual Uniforum-style message catalog (.po file) into
+a binary GNU catalog (.mo file). This is essentially the same function as the
+GNU msgfmt program, however, it is a simpler implementation.
+
+Usage: msgfmt.py [OPTIONS] filename.po
+
+Options:
+ -o file
+ --output-file=file
+ Specify the output file to write to. If omitted, output will go to a
+ file named filename.mo (based off the input file name).
+
+ -h
+ --help
+ Print this message and exit.
+
+ -V
+ --version
+ Display version information and exit.
+"""
+
+import sys
+import os
+import getopt
+import struct
+import array
+
+__version__ = "1.1"
+
+MESSAGES = {}
+
+
+
+def usage(code, msg=''):
+ print >> sys.stderr, __doc__
+ if msg:
+ print >> sys.stderr, msg
+ sys.exit(code)
+
+
+
+def add(id, str, fuzzy):
+ "Add a non-fuzzy translation to the dictionary."
+ global MESSAGES
+ if not fuzzy and str:
+ MESSAGES[id] = str
+
+
+
+def generate():
+ "Return the generated output."
+ global MESSAGES
+ keys = MESSAGES.keys()
+ # the keys are sorted in the .mo file
+ keys.sort()
+ offsets = []
+ ids = strs = ''
+ for id in keys:
+ # For each string, we need size and file offset. Each string is NUL
+ # terminated; the NUL does not count into the size.
+ offsets.append((len(ids), len(id), len(strs), len(MESSAGES[id])))
+ ids += id + '\0'
+ strs += MESSAGES[id] + '\0'
+ output = ''
+ # The header is 7 32-bit unsigned integers. We don't use hash tables, so
+ # the keys start right after the index tables.
+ # translated string.
+ keystart = 7*4+16*len(keys)
+ # and the values start after the keys
+ valuestart = keystart + len(ids)
+ koffsets = []
+ voffsets = []
+ # The string table first has the list of keys, then the list of values.
+ # Each entry has first the size of the string, then the file offset.
+ for o1, l1, o2, l2 in offsets:
+ koffsets += [l1, o1+keystart]
+ voffsets += [l2, o2+valuestart]
+ offsets = koffsets + voffsets
+ output = struct.pack("Iiiiiii",
+ 0x950412deL, # Magic
+ 0, # Version
+ len(keys), # # of entries
+ 7*4, # start of key index
+ 7*4+len(keys)*8, # start of value index
+ 0, 0) # size and offset of hash table
+ output += array.array("i", offsets).tostring()
+ output += ids
+ output += strs
+ return output
+
+
+
+def make(filename, outfile):
+ ID = 1
+ STR = 2
+
+ # Compute .mo name from .po name and arguments
+ if filename.endswith('.po'):
+ infile = filename
+ else:
+ infile = filename + '.po'
+ if outfile is None:
+ outfile = os.path.splitext(infile)[0] + '.mo'
+
+ try:
+ lines = open(infile).readlines()
+ except IOError, msg:
+ print >> sys.stderr, msg
+ sys.exit(1)
+
+ section = None
+ fuzzy = 0
+
+ # Parse the catalog
+ lno = 0
+ for l in lines:
+ lno += 1
+ # If we get a comment line after a msgstr, this is a new entry
+ if l[0] == '#' and section == STR:
+ add(msgid, msgstr, fuzzy)
+ section = None
+ fuzzy = 0
+ # Record a fuzzy mark
+ if l[:2] == '#,' and l.find('fuzzy'):
+ fuzzy = 1
+ # Skip comments
+ if l[0] == '#':
+ continue
+ # Now we are in a msgid section, output previous section
+ if l.startswith('msgid'):
+ if section == STR:
+ add(msgid, msgstr, fuzzy)
+ section = ID
+ l = l[5:]
+ msgid = msgstr = ''
+ # Now we are in a msgstr section
+ elif l.startswith('msgstr'):
+ section = STR
+ l = l[6:]
+ # Skip empty lines
+ l = l.strip()
+ if not l:
+ continue
+ # XXX: Does this always follow Python escape semantics?
+ l = eval(l)
+ if section == ID:
+ msgid += l
+ elif section == STR:
+ msgstr += l
+ else:
+ print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
+ 'before:'
+ print >> sys.stderr, l
+ sys.exit(1)
+ # Add last entry
+ if section == STR:
+ add(msgid, msgstr, fuzzy)
+
+ # Compute output
+ output = generate()
+
+ try:
+ open(outfile,"wb").write(output)
+ except IOError,msg:
+ print >> sys.stderr, msg
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'hVo:',
+ ['help', 'version', 'output-file='])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ outfile = None
+ # parse options
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ elif opt in ('-V', '--version'):
+ print >> sys.stderr, "msgfmt.py", __version__
+ sys.exit(0)
+ elif opt in ('-o', '--output-file'):
+ outfile = arg
+ # do it
+ if not args:
+ print >> sys.stderr, 'No input file given'
+ print >> sys.stderr, "Try `msgfmt --help' for more information."
+ return
+
+ for filename in args:
+ make(filename, outfile)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/attic/bin/po2templ.py b/src/mailman/attic/bin/po2templ.py
new file mode 100644
index 000000000..86eae96b9
--- /dev/null
+++ b/src/mailman/attic/bin/po2templ.py
@@ -0,0 +1,90 @@
+#! @PYTHON@
+#
+# Copyright (C) 2005-2009 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+# Author: Tokio Kikuchi <tkikuchi@is.kochi-u.ac.jp>
+
+
+"""po2templ.py
+
+Extract templates from language po file.
+
+Usage: po2templ.py languages
+"""
+
+import re
+import sys
+
+cre = re.compile('^#:\s*templates/en/(?P<filename>.*?):1')
+
+
+
+def do_lang(lang):
+ in_template = False
+ in_msg = False
+ msgstr = ''
+ fp = file('messages/%s/LC_MESSAGES/mailman.po' % lang)
+ try:
+ for line in fp:
+ m = cre.search(line)
+ if m:
+ in_template = True
+ in_msg = False
+ filename = m.group('filename')
+ outfilename = 'templates/%s/%s' % (lang, filename)
+ continue
+ if in_template and line.startswith('#,'):
+ if line.strip() == '#, fuzzy':
+ in_template = False
+ continue
+ if in_template and line.startswith('msgstr'):
+ line = line[7:]
+ in_msg = True
+ if in_msg:
+ if not line.strip():
+ in_template = False
+ in_msg = False
+ if len(msgstr) > 1 and outfilename:
+ # exclude no translation ... 1 is for LF only
+ outfile = file(outfilename, 'w')
+ try:
+ outfile.write(msgstr)
+ outfile.write('\n')
+ finally:
+ outfile.close()
+ outfilename = ''
+ msgstr = ''
+ continue
+ msgstr += eval(line)
+ finally:
+ fp.close()
+ if len(msgstr) > 1 and outfilename:
+ # flush remaining msgstr (last template file)
+ outfile = file(outfilename, 'w')
+ try:
+ outfile.write(msgstr)
+ outfile.write('\n')
+ finally:
+ outfile.close()
+
+
+
+if __name__ == '__main__':
+ langs = sys.argv[1:]
+ for lang in langs:
+ do_lang(lang)
diff --git a/src/mailman/attic/bin/pygettext.py b/src/mailman/attic/bin/pygettext.py
new file mode 100644
index 000000000..84421ee8c
--- /dev/null
+++ b/src/mailman/attic/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[o-umlaut]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/src/mailman/attic/bin/remove_members b/src/mailman/attic/bin/remove_members
new file mode 100755
index 000000000..a7b4ebb47
--- /dev/null
+++ b/src/mailman/attic/bin/remove_members
@@ -0,0 +1,186 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998-2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# 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. If not specified, the list
+ default value is used.
+
+ --noadminack
+ -N
+ Don't send the admin acknowledgements. If not specified, the list
+ default value is used.
+
+ --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 _
+
+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 ReadFile(filename):
+ lines = []
+ if filename == "-":
+ fp = sys.stdin
+ closep = False
+ else:
+ fp = open(filename)
+ closep = True
+ 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)
+
+ filename = None
+ all = False
+ alllists = False
+ # None means use list default
+ userack = None
+ admin_notif = None
+
+ 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 = True
+ elif opt == '--fromall':
+ alllists = True
+ elif opt in ('-n', '--nouserack'):
+ userack = False
+ elif opt in ('-N', '--noadminack'):
+ admin_notif = False
+
+ if len(args) < 1 and not (filename and alllists):
+ usage(1)
+
+ # 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/src/mailman/attic/bin/reset_pw.py b/src/mailman/attic/bin/reset_pw.py
new file mode 100644
index 000000000..453c8b849
--- /dev/null
+++ b/src/mailman/attic/bin/reset_pw.py
@@ -0,0 +1,83 @@
+#! @PYTHON@
+#
+# Copyright (C) 2004-2009 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+# Inspired by Florian Weimer.
+
+"""Reset the passwords for members of a mailing list.
+
+This script resets all the passwords of a mailing list's members. It can also
+be used to reset the lists of all members of all mailing lists, but it is your
+responsibility to let the users know that their passwords have been changed.
+
+This script is intended to be run as a bin/withlist script, i.e.
+
+% bin/withlist -l -r reset_pw listname [options]
+
+Options:
+ -v / --verbose
+ Print what the script is doing.
+"""
+
+import sys
+import getopt
+
+import paths
+from Mailman import Utils
+from Mailman.i18n import _
+
+
+
+def usage(code, msg=''):
+ if code:
+ fd = sys.stderr
+ else:
+ fd = sys.stdout
+ print >> fd, _(__doc__.replace('%', '%%'))
+ if msg:
+ print >> fd, msg
+ sys.exit(code)
+
+
+
+def reset_pw(mlist, *args):
+ try:
+ opts, args = getopt.getopt(args, 'v', ['verbose'])
+ except getopt.error, msg:
+ usage(1, msg)
+
+ verbose = False
+ for opt, args in opts:
+ if opt in ('-v', '--verbose'):
+ verbose = True
+
+ listname = mlist.internal_name()
+ if verbose:
+ print _('Changing passwords for list: %(listname)s')
+
+ for member in mlist.getMembers():
+ randompw = Utils.MakeRandomPassword()
+ mlist.setMemberPassword(member, randompw)
+ if verbose:
+ print _('New password for member %(member)40s: %(randompw)s')
+
+ mlist.Save()
+
+
+
+if __name__ == '__main__':
+ usage(0)
diff --git a/src/mailman/attic/bin/sync_members b/src/mailman/attic/bin/sync_members
new file mode 100755
index 000000000..4a21624c1
--- /dev/null
+++ b/src/mailman/attic/bin/sync_members
@@ -0,0 +1,286 @@
+#! @PYTHON@
+#
+# Copyright (C) 1998-2003 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 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)
+
+ enc = sys.getdefaultencoding()
+ # 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
+ name = mlist.getMemberName(laddr) or ''
+ 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)
+ 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/src/mailman/attic/bin/templ2pot.py b/src/mailman/attic/bin/templ2pot.py
new file mode 100644
index 000000000..0253cc2cd
--- /dev/null
+++ b/src/mailman/attic/bin/templ2pot.py
@@ -0,0 +1,120 @@
+#! @PYTHON@
+# Code stolen from pygettext.py
+# by Tokio Kikuchi <tkikuchi@is.kochi-u.ac.jp>
+
+"""templ2pot.py -- convert mailman template (en) to pot format.
+
+Usage: templ2pot.py inputfile ...
+
+Options:
+
+ -h, --help
+
+Inputfiles are english templates. Outputs are written to stdout.
+"""
+
+import sys
+import getopt
+
+
+
+try:
+ import paths
+ from Mailman.i18n import _
+except ImportError:
+ def _(s): return s
+
+EMPTYSTRING = ''
+
+
+
+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[o-umlaut]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 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.splitlines()
+ 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
+
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:],
+ 'h',
+ ['help',]
+ )
+ except getopt.error, msg:
+ usage(1, msg)
+
+ # parse options
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+
+ # calculate escapes
+ make_escapes(0)
+
+ for filename in args:
+ print '#: %s:1' % filename
+ s = file(filename).read()
+ print '#, template'
+ print 'msgid', normalize(s)
+ print 'msgstr ""\n'
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/mailman/attic/bin/transcheck b/src/mailman/attic/bin/transcheck
new file mode 100755
index 000000000..73910e771
--- /dev/null
+++ b/src/mailman/attic/bin/transcheck
@@ -0,0 +1,412 @@
+#! @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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 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, escaped=None):
+ self.dict = {}
+ self.errs = []
+ self.regexp = re.compile(regexp)
+ self.escaped = None
+ if escaped:
+ self.escaped = re.compile(escaped)
+
+ def checkin(self, string):
+ "scan a string from the original file"
+ for key in self.regexp.findall(string):
+ if self.escaped and self.escaped.match(key):
+ continue
+ 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.escaped and self.escaped.match(key):
+ continue
+ 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()