summaryrefslogtreecommitdiff
path: root/src/mailman/attic
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/attic')
-rw-r--r--src/mailman/attic/Bouncer.py250
-rw-r--r--src/mailman/attic/Defaults.py1324
-rw-r--r--src/mailman/attic/Deliverer.py174
-rw-r--r--src/mailman/attic/Digester.py57
-rw-r--r--src/mailman/attic/MailList.py731
-rw-r--r--src/mailman/attic/SecurityManager.py306
-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
18 files changed, 5300 insertions, 0 deletions
diff --git a/src/mailman/attic/Bouncer.py b/src/mailman/attic/Bouncer.py
new file mode 100644
index 000000000..e2de3c915
--- /dev/null
+++ b/src/mailman/attic/Bouncer.py
@@ -0,0 +1,250 @@
+# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Handle delivery bounces."""
+
+import sys
+import time
+import logging
+
+from email.MIMEMessage import MIMEMessage
+from email.MIMEText import MIMEText
+
+from mailman import Defaults
+from mailman import Message
+from mailman import Utils
+from mailman import i18n
+from mailman.configuration import config
+from mailman.interfaces import DeliveryStatus
+
+EMPTYSTRING = ''
+
+# This constant is supposed to represent the day containing the first midnight
+# after the epoch. We'll add (0,)*6 to this tuple to get a value appropriate
+# for time.mktime().
+ZEROHOUR_PLUSONEDAY = time.localtime(60 * 60 * 24)[:3]
+
+def _(s): return s
+
+REASONS = {
+ DeliveryStatus.by_bounces : _('due to excessive bounces'),
+ DeliveryStatus.by_user : _('by yourself'),
+ DeliveryStatus.by_moderator : _('by the list administrator'),
+ DeliveryStatus.unknown : _('for unknown reasons'),
+ }
+
+_ = i18n._
+
+log = logging.getLogger('mailman.bounce')
+slog = logging.getLogger('mailman.subscribe')
+
+
+
+class _BounceInfo:
+ def __init__(self, member, score, date, noticesleft):
+ self.member = member
+ self.cookie = None
+ self.reset(score, date, noticesleft)
+
+ def reset(self, score, date, noticesleft):
+ self.score = score
+ self.date = date
+ self.noticesleft = noticesleft
+ self.lastnotice = ZEROHOUR_PLUSONEDAY
+
+ def __repr__(self):
+ # For debugging
+ return """\
+<bounce info for member %(member)s
+ current score: %(score)s
+ last bounce date: %(date)s
+ email notices left: %(noticesleft)s
+ last notice date: %(lastnotice)s
+ confirmation cookie: %(cookie)s
+ >""" % self.__dict__
+
+
+
+class Bouncer:
+ def registerBounce(self, member, msg, weight=1.0, day=None):
+ if not self.isMember(member):
+ return
+ info = self.getBounceInfo(member)
+ if day is None:
+ # Use today's date
+ day = time.localtime()[:3]
+ if not isinstance(info, _BounceInfo):
+ # This is the first bounce we've seen from this member
+ info = _BounceInfo(member, weight, day,
+ self.bounce_you_are_disabled_warnings)
+ self.setBounceInfo(member, info)
+ log.info('%s: %s bounce score: %s', self.internal_name(),
+ member, info.score)
+ # Continue to the check phase below
+ elif self.getDeliveryStatus(member) <> DeliveryStatus.enabled:
+ # The user is already disabled, so we can just ignore subsequent
+ # bounces. These are likely due to residual messages that were
+ # sent before disabling the member, but took a while to bounce.
+ log.info('%s: %s residual bounce received',
+ self.internal_name(), member)
+ return
+ elif info.date == day:
+ # We've already scored any bounces for this day, so ignore it.
+ log.info('%s: %s already scored a bounce for date %s',
+ self.internal_name(), member,
+ time.strftime('%d-%b-%Y', day + (0,0,0,0,1,0)))
+ # Continue to check phase below
+ else:
+ # See if this member's bounce information is stale.
+ now = Utils.midnight(day)
+ lastbounce = Utils.midnight(info.date)
+ if lastbounce + self.bounce_info_stale_after < now:
+ # Information is stale, so simply reset it
+ info.reset(weight, day, self.bounce_you_are_disabled_warnings)
+ log.info('%s: %s has stale bounce info, resetting',
+ self.internal_name(), member)
+ else:
+ # Nope, the information isn't stale, so add to the bounce
+ # score and take any necessary action.
+ info.score += weight
+ info.date = day
+ log.info('%s: %s current bounce score: %s',
+ self.internal_name(), member, info.score)
+ # Continue to the check phase below
+ #
+ # Now that we've adjusted the bounce score for this bounce, let's
+ # check to see if the disable-by-bounce threshold has been reached.
+ if info.score >= self.bounce_score_threshold:
+ if config.VERP_PROBES:
+ log.info('sending %s list probe to: %s (score %s >= %s)',
+ self.internal_name(), member, info.score,
+ self.bounce_score_threshold)
+ self.sendProbe(member, msg)
+ info.reset(0, info.date, info.noticesleft)
+ else:
+ self.disableBouncingMember(member, info, msg)
+
+ def disableBouncingMember(self, member, info, msg):
+ # Initialize their confirmation cookie. If we do it when we get the
+ # first bounce, it'll expire by the time we get the disabling bounce.
+ cookie = self.pend_new(Pending.RE_ENABLE, self.internal_name(), member)
+ info.cookie = cookie
+ # Disable them
+ if config.VERP_PROBES:
+ log.info('%s: %s disabling due to probe bounce received',
+ self.internal_name(), member)
+ else:
+ log.info('%s: %s disabling due to bounce score %s >= %s',
+ self.internal_name(), member,
+ info.score, self.bounce_score_threshold)
+ self.setDeliveryStatus(member, DeliveryStatus.by_bounces)
+ self.sendNextNotification(member)
+ if self.bounce_notify_owner_on_disable:
+ self.__sendAdminBounceNotice(member, msg)
+
+ def __sendAdminBounceNotice(self, member, msg):
+ # BAW: This is a bit kludgey, but we're not providing as much
+ # information in the new admin bounce notices as we used to (some of
+ # it was of dubious value). However, we'll provide empty, strange, or
+ # meaningless strings for the unused %()s fields so that the language
+ # translators don't have to provide new templates.
+ text = Utils.maketext(
+ 'bounce.txt',
+ {'listname' : self.real_name,
+ 'addr' : member,
+ 'negative' : '',
+ 'did' : _('disabled'),
+ 'but' : '',
+ 'reenable' : '',
+ 'owneraddr': self.no_reply_address,
+ }, mlist=self)
+ subject = _('Bounce action notification')
+ umsg = Message.UserNotification(self.GetOwnerEmail(),
+ self.no_reply_address,
+ subject,
+ lang=self.preferred_language)
+ # BAW: Be sure you set the type before trying to attach, or you'll get
+ # a MultipartConversionError.
+ umsg.set_type('multipart/mixed')
+ umsg.attach(
+ MIMEText(text, _charset=Utils.GetCharSet(self.preferred_language)))
+ if isinstance(msg, str):
+ umsg.attach(MIMEText(msg))
+ else:
+ umsg.attach(MIMEMessage(msg))
+ umsg.send(self)
+
+ def sendNextNotification(self, member):
+ info = self.getBounceInfo(member)
+ if info is None:
+ return
+ reason = self.getDeliveryStatus(member)
+ if info.noticesleft <= 0:
+ # BAW: Remove them now, with a notification message
+ self.ApprovedDeleteMember(
+ member, 'disabled address',
+ admin_notif=self.bounce_notify_owner_on_removal,
+ userack=1)
+ # Expunge the pending cookie for the user. We throw away the
+ # returned data.
+ self.pend_confirm(info.cookie)
+ if reason == DeliveryStatus.by_bounces:
+ log.info('%s: %s deleted after exhausting notices',
+ self.internal_name(), member)
+ slog.info('%s: %s auto-unsubscribed [reason: %s]',
+ self.internal_name(), member,
+ {DeliveryStatus.by_bounces: 'BYBOUNCE',
+ DeliveryStatus.by_user: 'BYUSER',
+ DeliveryStatus.by_moderator: 'BYADMIN',
+ DeliveryStatus.unknown: 'UNKNOWN'}.get(
+ reason, 'invalid value'))
+ return
+ # Send the next notification
+ confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
+ info.cookie)
+ optionsurl = self.GetOptionsURL(member, absolute=1)
+ reqaddr = self.GetRequestEmail()
+ lang = self.getMemberLanguage(member)
+ txtreason = REASONS.get(reason)
+ if txtreason is None:
+ txtreason = _('for unknown reasons')
+ else:
+ txtreason = _(txtreason)
+ # Give a little bit more detail on bounce disables
+ if reason == DeliveryStatus.by_bounces:
+ date = time.strftime('%d-%b-%Y',
+ time.localtime(Utils.midnight(info.date)))
+ extra = _(' The last bounce received from you was dated %(date)s')
+ txtreason += extra
+ text = Utils.maketext(
+ 'disabled.txt',
+ {'listname' : self.real_name,
+ 'noticesleft': info.noticesleft,
+ 'confirmurl' : confirmurl,
+ 'optionsurl' : optionsurl,
+ 'password' : self.getMemberPassword(member),
+ 'owneraddr' : self.GetOwnerEmail(),
+ 'reason' : txtreason,
+ }, lang=lang, mlist=self)
+ msg = Message.UserNotification(member, reqaddr, text=text, lang=lang)
+ # BAW: See the comment in MailList.py ChangeMemberAddress() for why we
+ # set the Subject this way.
+ del msg['subject']
+ msg['Subject'] = 'confirm ' + info.cookie
+ msg.send(self)
+ info.noticesleft -= 1
+ info.lastnotice = time.localtime()[:3]
diff --git a/src/mailman/attic/Defaults.py b/src/mailman/attic/Defaults.py
new file mode 100644
index 000000000..6f72ed535
--- /dev/null
+++ b/src/mailman/attic/Defaults.py
@@ -0,0 +1,1324 @@
+# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Distributed default settings for significant Mailman config variables."""
+
+from datetime import timedelta
+
+from mailman.interfaces.mailinglist import ReplyToMunging
+
+
+
+class CompatibleTimeDelta(timedelta):
+ def __float__(self):
+ # Convert to float seconds.
+ return (self.days * 24 * 60 * 60 +
+ self.seconds + self.microseconds / 1.0e6)
+
+ def __int__(self):
+ return int(float(self))
+
+
+def seconds(s):
+ return CompatibleTimeDelta(seconds=s)
+
+def minutes(m):
+ return CompatibleTimeDelta(minutes=m)
+
+def hours(h):
+ return CompatibleTimeDelta(hours=h)
+
+def days(d):
+ return CompatibleTimeDelta(days=d)
+
+
+# Some convenient constants
+Yes = yes = On = on = True
+No = no = Off = off = False
+
+
+
+#####
+# General system-wide defaults
+#####
+
+# Should image logos be used? Set this to 0 to disable image logos from "our
+# sponsors" and just use textual links instead (this will also disable the
+# shortcut "favicon"). Otherwise, this should contain the URL base path to
+# the logo images (and must contain the trailing slash).. If you want to
+# disable Mailman's logo footer altogther, hack
+# mailman/htmlformat.py:MailmanLogo(), which also contains the hardcoded links
+# and image names.
+IMAGE_LOGOS = '/icons/'
+
+# The name of the Mailman favicon
+SHORTCUT_ICON = 'mm-icon.png'
+
+# Don't change MAILMAN_URL, unless you want to point it at one of the mirrors.
+MAILMAN_URL = 'http://www.gnu.org/software/mailman/index.html'
+#MAILMAN_URL = 'http://www.list.org/'
+#MAILMAN_URL = 'http://mailman.sf.net/'
+
+DEFAULT_URL_PATTERN = 'http://%s/mailman/'
+
+# This address is used as the from address whenever a message comes from some
+# entity to which there is no natural reply recipient. Set this to a real
+# human or to /dev/null. It will be appended with the hostname of the list
+# involved or the DEFAULT_EMAIL_HOST if none is available. Address must not
+# bounce and it must not point to a Mailman process.
+NO_REPLY_ADDRESS = 'noreply'
+
+# This address is the "site owner" address. Certain messages which must be
+# delivered to a human, but which can't be delivered to a list owner (e.g. a
+# bounce from a list owner), will be sent to this address. It should point to
+# a human.
+SITE_OWNER_ADDRESS = 'changeme@example.com'
+
+# Normally when a site administrator authenticates to a web page with the site
+# password, they get a cookie which authorizes them as the list admin. It
+# makes me nervous to hand out site auth cookies because if this cookie is
+# cracked or intercepted, the intruder will have access to every list on the
+# site. OTOH, it's dang handy to not have to re-authenticate to every list on
+# the site. Set this value to Yes to allow site admin cookies.
+ALLOW_SITE_ADMIN_COOKIES = No
+
+# Command that is used to convert text/html parts into plain text. This
+# should output results to standard output. %(filename)s will contain the
+# name of the temporary file that the program should operate on.
+HTML_TO_PLAIN_TEXT_COMMAND = '/usr/bin/lynx -dump %(filename)s'
+
+# Default password hashing scheme. See 'bin/mmsitepass -P' for a list of
+# available schemes.
+PASSWORD_SCHEME = 'ssha'
+
+# Default run-time directory.
+DEFAULT_VAR_DIRECTORY = '/var/mailman'
+
+
+
+#####
+# Database options
+#####
+
+# Use this to set the SQLAlchemy database engine URL. You generally have one
+# primary database connection for all of Mailman. List data and most rosters
+# will store their data in this database, although external rosters may access
+# other databases in their own way. This string support substitutions using
+# any variable in the Configuration object.
+DEFAULT_DATABASE_URL = 'sqlite:///$DATA_DIR/mailman.db'
+
+
+
+#####
+# Spam avoidance defaults
+#####
+
+# This variable contains a list of tuple of the format:
+#
+# (header, pattern[, chain])
+#
+# which is used to match against the current message's headers. If the
+# pattern matches the given header in the current message, then the named
+# chain is jumped to. header is case-insensitive and should not include the
+# trailing colon. pattern is always matched with re.IGNORECASE. chain is
+# optional; if not given the 'hold' chain is used, but if given it may be any
+# existing chain, such as 'discard', 'reject', or 'accept'.
+#
+# Note that the more searching done, the slower the whole process gets.
+# Header matching is run against all messages coming to either the list, or
+# the -owners address, unless the message is explicitly approved.
+HEADER_MATCHES = []
+
+
+
+#####
+# Web UI defaults
+#####
+
+# Almost all the colors used in Mailman's web interface are parameterized via
+# the following variables. This lets you easily change the color schemes for
+# your preferences without having to do major surgery on the source code.
+# Note that in general, the template colors are not included here since it is
+# easy enough to override the default template colors via site-wide,
+# vdomain-wide, or list-wide specializations.
+
+WEB_BG_COLOR = 'white' # Page background
+WEB_HEADER_COLOR = '#99ccff' # Major section headers
+WEB_SUBHEADER_COLOR = '#fff0d0' # Minor section headers
+WEB_ADMINITEM_COLOR = '#dddddd' # Option field background
+WEB_ADMINPW_COLOR = '#99cccc' # Password box color
+WEB_ERROR_COLOR = 'red' # Error message foreground
+WEB_LINK_COLOR = '' # If true, forces LINK=
+WEB_ALINK_COLOR = '' # If true, forces ALINK=
+WEB_VLINK_COLOR = '' # If true, forces VLINK=
+WEB_HIGHLIGHT_COLOR = '#dddddd' # If true, alternating rows
+ # in listinfo & admin display
+# CGI file extension.
+CGIEXT = ''
+
+
+
+#####
+# Archive defaults
+#####
+
+# The url template for the public archives. This will be used in several
+# places, including the List-Archive: header, links to the archive on the
+# list's listinfo page, and on the list's admin page.
+#
+# This variable supports several substitution variables
+# - $hostname -- the host on which the archive resides
+# - $listname -- the short name of the list being accessed
+# - $fqdn_listname -- the long name of the list being accessed
+PUBLIC_ARCHIVE_URL = 'http://$hostname/pipermail/$fqdn_listname'
+
+# The public Mail-Archive.com service's base url.
+MAIL_ARCHIVE_BASEURL = 'http://go.mail-archive.com/'
+# The posting address for the Mail-Archive.com service
+MAIL_ARCHIVE_RECIPIENT = 'archive@mail-archive.com'
+
+# The command for archiving to a local MHonArc instance.
+MHONARC_COMMAND = """\
+/usr/bin/mhonarc \
+-add \
+-dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db \
+-outdir $VAR_DIR/mhonarc/${listname} \
+-stderr $LOG_DIR/mhonarc \
+-stdout $LOG_DIR/mhonarc \
+-spammode \
+-umask 022"""
+
+# Are archives on or off by default?
+DEFAULT_ARCHIVE = On
+
+# Are archives public or private by default?
+# 0=public, 1=private
+DEFAULT_ARCHIVE_PRIVATE = 0
+
+# ARCHIVE_TO_MBOX
+#-1 - do not do any archiving
+# 0 - do not archive to mbox, use builtin mailman html archiving only
+# 1 - do not use builtin mailman html archiving, archive to mbox only
+# 2 - archive to both mbox and builtin mailman html archiving.
+# See the settings below for PUBLIC_EXTERNAL_ARCHIVER and
+# PRIVATE_EXTERNAL_ARCHIVER which can be used to replace mailman's
+# builtin html archiving with an external archiver. The flat mail
+# mbox file can be useful for searching, and is another way to
+# interface external archivers, etc.
+ARCHIVE_TO_MBOX = 2
+
+# 0 - yearly
+# 1 - monthly
+# 2 - quarterly
+# 3 - weekly
+# 4 - daily
+DEFAULT_ARCHIVE_VOLUME_FREQUENCY = 1
+DEFAULT_DIGEST_VOLUME_FREQUENCY = 1
+
+# These variables control the use of an external archiver. Normally if
+# archiving is turned on (see ARCHIVE_TO_MBOX above and the list's archive*
+# attributes) the internal Pipermail archiver is used. This is the default if
+# both of these variables are set to No. When either is set, the value should
+# be a shell command string which will get passed to os.popen(). This string
+# can contain the following substitution strings:
+#
+# $listname -- gets the internal name of the list
+# $hostname -- gets the email hostname for the list
+#
+# being archived will be substituted for this. Please note that os.popen() is
+# used.
+#
+# Note that if you set one of these variables, you should set both of them
+# (they can be the same string). This will mean your external archiver will
+# be used regardless of whether public or private archives are selected.
+PUBLIC_EXTERNAL_ARCHIVER = No
+PRIVATE_EXTERNAL_ARCHIVER = No
+
+# A filter module that converts from multipart messages to "flat" messages
+# (i.e. containing a single payload). This is required for Pipermail, and you
+# may want to set it to 0 for external archivers. You can also replace it
+# with your own module as long as it contains a process() function that takes
+# a MailList object and a Message object. It should raise
+# Errors.DiscardMessage if it wants to throw the message away. Otherwise it
+# should modify the Message object as necessary.
+ARCHIVE_SCRUBBER = 'mailman.pipeline.scrubber'
+
+# Control parameter whether mailman.Handlers.Scrubber should use message
+# attachment's filename as is indicated by the filename parameter or use
+# 'attachement-xxx' instead. The default is set True because the applications
+# on PC and Mac begin to use longer non-ascii filenames. Historically, it
+# was set False in 2.1.6 for backward compatiblity but it was reset to True
+# for safer operation in mailman-2.1.7.
+SCRUBBER_DONT_USE_ATTACHMENT_FILENAME = True
+
+# Use of attachment filename extension per se is may be dangerous because
+# virus fakes it. You can set this True if you filter the attachment by
+# filename extension
+SCRUBBER_USE_ATTACHMENT_FILENAME_EXTENSION = False
+
+# This variable defines what happens to text/html subparts. They can be
+# stripped completely, escaped, or filtered through an external program. The
+# legal values are:
+# 0 - Strip out text/html parts completely, leaving a notice of the removal in
+# the message. If the outer part is text/html, the entire message is
+# discarded.
+# 1 - Remove any embedded text/html parts, leaving them as HTML-escaped
+# attachments which can be separately viewed. Outer text/html parts are
+# simply HTML-escaped.
+# 2 - Leave it inline, but HTML-escape it
+# 3 - Remove text/html as attachments but don't HTML-escape them. Note: this
+# is very dangerous because it essentially means anybody can send an HTML
+# email to your site containing evil JavaScript or web bugs, or other
+# nasty things, and folks viewing your archives will be susceptible. You
+# should only consider this option if you do heavy moderation of your list
+# postings.
+#
+# Note: given the current archiving code, it is not possible to leave
+# text/html parts inline and un-escaped. I wouldn't think it'd be a good idea
+# to do anyway.
+#
+# The value can also be a string, in which case it is the name of a command to
+# filter the HTML page through. The resulting output is left in an attachment
+# or as the entirety of the message when the outer part is text/html. The
+# format of the string must include a "%(filename)s" which will contain the
+# name of the temporary file that the program should operate on. It should
+# write the processed message to stdout. Set this to
+# HTML_TO_PLAIN_TEXT_COMMAND to specify an HTML to plain text conversion
+# program.
+ARCHIVE_HTML_SANITIZER = 1
+
+# Set this to Yes to enable gzipping of the downloadable archive .txt file.
+# Note that this is /extremely/ inefficient, so an alternative is to just
+# collect the messages in the associated .txt file and run a cron job every
+# night to generate the txt.gz file. See cron/nightly_gzip for details.
+GZIP_ARCHIVE_TXT_FILES = No
+
+# This sets the default `clobber date' policy for the archiver. When a
+# message is to be archived either by Pipermail or an external archiver,
+# Mailman can modify the Date: header to be the date the message was received
+# instead of the Date: in the original message. This is useful if you
+# typically receive messages with outrageous dates. Set this to 0 to retain
+# the date of the original message, or to 1 to always clobber the date. Set
+# it to 2 to perform `smart overrides' on the date; when the date is outside
+# ARCHIVER_ALLOWABLE_SANE_DATE_SKEW (either too early or too late), then the
+# received date is substituted instead.
+ARCHIVER_CLOBBER_DATE_POLICY = 2
+ARCHIVER_ALLOWABLE_SANE_DATE_SKEW = days(15)
+
+# Pipermail archives contain the raw email addresses of the posting authors.
+# Some view this as a goldmine for spam harvesters. Set this to Yes to
+# moderately obscure email addresses, but note that this breaks mailto: URLs
+# in the archives too.
+ARCHIVER_OBSCURES_EMAILADDRS = Yes
+
+# Pipermail assumes that messages bodies contain US-ASCII text.
+# Change this option to define a different character set to be used as
+# the default character set for the archive. The term "character set"
+# is used in MIME to refer to a method of converting a sequence of
+# octets into a sequence of characters. If you change the default
+# charset, you might need to add it to VERBATIM_ENCODING below.
+DEFAULT_CHARSET = None
+
+# Most character set encodings require special HTML entity characters to be
+# quoted, otherwise they won't look right in the Pipermail archives. However
+# some character sets must not quote these characters so that they can be
+# rendered properly in the browsers. The primary issue is multi-byte
+# encodings where the octet 0x26 does not always represent the & character.
+# This variable contains a list of such characters sets which are not
+# HTML-quoted in the archives.
+VERBATIM_ENCODING = ['iso-2022-jp']
+
+# When the archive is public, should Mailman also make the raw Unix mbox file
+# publically available?
+PUBLIC_MBOX = No
+
+
+
+#####
+# Delivery defaults
+#####
+
+# Final delivery module for outgoing mail. This handler is used for message
+# delivery to the list via the smtpd, and to an individual user. This value
+# must be a string naming an IHandler.
+DELIVERY_MODULE = 'smtp-direct'
+
+# MTA should name a module in mailman/MTA which provides the MTA specific
+# functionality for creating and removing lists. Some MTAs like Exim can be
+# configured to automatically recognize new lists, in which case the MTA
+# variable should be set to None. Use 'Manual' to print new aliases to
+# standard out (or send an email to the site list owner) for manual twiddling
+# of an /etc/aliases style file. Use 'Postfix' if you are using the Postfix
+# MTA -- but then also see POSTFIX_STYLE_VIRTUAL_DOMAINS.
+MTA = 'Manual'
+
+# If you set MTA='Postfix', then you also want to set the following variable,
+# depending on whether you're using virtual domains in Postfix, and which
+# style of virtual domain you're using. Set this to the empty list if you're
+# not using virtual domains in Postfix, or if you're using Sendmail-style
+# virtual domains (where all addresses are visible in all domains). If you're
+# using Postfix-style virtual domains, where aliases should only show up in
+# the virtual domain, set this variable to the list of host_name values to
+# write separate virtual entries for. I.e. if you run dom1.ain, dom2.ain, and
+# dom3.ain, but only dom2 and dom3 are virtual, set this variable to the list
+# ['dom2.ain', 'dom3.ain']. Matches are done against the host_name attribute
+# of the mailing lists. See the Postfix section of the installation manual
+# for details.
+POSTFIX_STYLE_VIRTUAL_DOMAINS = []
+
+# We should use a separator in place of '@' for list-etc@dom2.ain in both
+# aliases and mailman-virtual files.
+POSTFIX_VIRTUAL_SEPARATOR = '_at_'
+
+# These variables describe the program to use for regenerating the aliases.db
+# and virtual-mailman.db files, respectively, from the associated plain text
+# files. The file being updated will be appended to this string (with a
+# separating space), so it must be appropriate for os.system().
+POSTFIX_ALIAS_CMD = '/usr/sbin/postalias'
+POSTFIX_MAP_CMD = '/usr/sbin/postmap'
+
+# Ceiling on the number of recipients that can be specified in a single SMTP
+# transaction. Set to 0 to submit the entire recipient list in one
+# transaction. Only used with the SMTPDirect DELIVERY_MODULE.
+SMTP_MAX_RCPTS = 500
+
+# Ceiling on the number of SMTP sessions to perform on a single socket
+# connection. Some MTAs have limits. Set this to 0 to do as many as we like
+# (i.e. your MTA has no limits). Set this to some number great than 0 and
+# Mailman will close the SMTP connection and re-open it after this number of
+# consecutive sessions.
+SMTP_MAX_SESSIONS_PER_CONNECTION = 0
+
+# Maximum number of simultaneous subthreads that will be used for SMTP
+# delivery. After the recipients list is chunked according to SMTP_MAX_RCPTS,
+# each chunk is handed off to the smptd by a separate such thread. If your
+# Python interpreter was not built for threads, this feature is disabled. You
+# can explicitly disable it in all cases by setting MAX_DELIVERY_THREADS to
+# 0. This feature is only supported with the SMTPDirect DELIVERY_MODULE.
+#
+# NOTE: This is an experimental feature and limited testing shows that it may
+# in fact degrade performance, possibly due to Python's global interpreter
+# lock. Use with caution.
+MAX_DELIVERY_THREADS = 0
+
+# SMTP host and port, when DELIVERY_MODULE is 'SMTPDirect'. Make sure the
+# host exists and is resolvable (i.e., if it's the default of "localhost" be
+# sure there's a localhost entry in your /etc/hosts file!)
+SMTPHOST = 'localhost'
+SMTPPORT = 0 # default from smtplib
+
+# Command for direct command pipe delivery to sendmail compatible program,
+# when DELIVERY_MODULE is 'Sendmail'.
+SENDMAIL_CMD = '/usr/lib/sendmail'
+
+# Set these variables if you need to authenticate to your NNTP server for
+# Usenet posting or reading. If no authentication is necessary, specify None
+# for both variables.
+NNTP_USERNAME = None
+NNTP_PASSWORD = None
+
+# Set this if you have an NNTP server you prefer gatewayed lists to use.
+DEFAULT_NNTP_HOST = u''
+
+# These variables controls how headers must be cleansed in order to be
+# accepted by your NNTP server. Some servers like INN reject messages
+# containing prohibited headers, or duplicate headers. The NNTP server may
+# reject the message for other reasons, but there's little that can be
+# programmatically done about that. See mailman/Queue/NewsRunner.py
+#
+# First, these headers (case ignored) are removed from the original message.
+NNTP_REMOVE_HEADERS = ['nntp-posting-host', 'nntp-posting-date', 'x-trace',
+ 'x-complaints-to', 'xref', 'date-received', 'posted',
+ 'posting-version', 'relay-version', 'received']
+
+# Next, these headers are left alone, unless there are duplicates in the
+# original message. Any second and subsequent headers are rewritten to the
+# second named header (case preserved).
+NNTP_REWRITE_DUPLICATE_HEADERS = [
+ ('To', 'X-Original-To'),
+ ('CC', 'X-Original-CC'),
+ ('Content-Transfer-Encoding', 'X-Original-Content-Transfer-Encoding'),
+ ('MIME-Version', 'X-MIME-Version'),
+ ]
+
+# Some list posts and mail to the -owner address may contain DomainKey or
+# DomainKeys Identified Mail (DKIM) signature headers <http://www.dkim.org/>.
+# Various list transformations to the message such as adding a list header or
+# footer or scrubbing attachments or even reply-to munging can break these
+# signatures. It is generally felt that these signatures have value, even if
+# broken and even if the outgoing message is resigned. However, some sites
+# may wish to remove these headers by setting this to Yes.
+REMOVE_DKIM_HEADERS = No
+
+# This is the pipeline which messages sent to the -owner address go through
+OWNER_PIPELINE = [
+ 'SpamDetect',
+ 'Replybot',
+ 'CleanseDKIM',
+ 'OwnerRecips',
+ 'ToOutgoing',
+ ]
+
+
+# This defines a logging subsystem confirmation file, which overrides the
+# default log settings. This is a ConfigParser formatted file which can
+# contain sections named after the logger name (without the leading 'mailman.'
+# common prefix). Each section may contain the following options:
+#
+# - level -- Overrides the default level; this may be any of the
+# standard Python logging levels, case insensitive.
+# - format -- Overrides the default format string; see below.
+# - datefmt -- Overrides the default date format string; see below.
+# - path -- Overrides the default logger path. This may be a relative
+# path name, in which case it is relative to Mailman's LOG_DIR,
+# or it may be an absolute path name. You cannot change the
+# handler class that will be used.
+# - propagate -- Boolean specifying whether to propagate log message from this
+# logger to the root "mailman" logger. You cannot override
+# settings for the root logger.
+#
+# The file name may be absolute, or relative to Mailman's etc directory.
+LOG_CONFIG_FILE = None
+
+# This defines log format strings for the SMTPDirect delivery module (see
+# DELIVERY_MODULE above). Valid %()s string substitutions include:
+#
+# time -- the time in float seconds that it took to complete the smtp
+# hand-off of the message from Mailman to your smtpd.
+#
+# size -- the size of the entire message, in bytes
+#
+# #recips -- the number of actual recipients for this message.
+#
+# #refused -- the number of smtp refused recipients (use this only in
+# SMTP_LOG_REFUSED).
+#
+# listname -- the `internal' name of the mailing list for this posting
+#
+# msg_<header> -- the value of the delivered message's given header. If
+# the message had no such header, then "n/a" will be used. Note though
+# that if the message had multiple such headers, then it is undefined
+# which will be used.
+#
+# allmsg_<header> - Same as msg_<header> above, but if there are multiple
+# such headers in the message, they will all be printed, separated by
+# comma-space.
+#
+# sender -- the "sender" of the messages, which will be the From: or
+# envelope-sender as determeined by the USE_ENVELOPE_SENDER variable
+# below.
+#
+# The format of the entries is a 2-tuple with the first element naming the
+# logger (as a child of the root 'mailman' logger) to print the message to,
+# and the second being a format string appropriate for Python's %-style string
+# interpolation. The file name is arbitrary; qfiles/<name> will be created
+# automatically if it does not exist.
+
+# The format of the message printed for every delivered message, regardless of
+# whether the delivery was successful or not. Set to None to disable the
+# printing of this log message.
+SMTP_LOG_EVERY_MESSAGE = (
+ 'smtp',
+ ('${message-id} smtp to $listname for ${#recips} recips, '
+ 'completed in $time seconds'))
+
+# This will only be printed if there were no immediate smtp failures.
+# Mutually exclusive with SMTP_LOG_REFUSED.
+SMTP_LOG_SUCCESS = (
+ 'post',
+ '${message-id} post to $listname from $sender, size=$size, success')
+
+# This will only be printed if there were any addresses which encountered an
+# immediate smtp failure. Mutually exclusive with SMTP_LOG_SUCCESS.
+SMTP_LOG_REFUSED = (
+ 'post',
+ ('${message-id} post to $listname from $sender, size=$size, '
+ '${#refused} failures'))
+
+# This will be logged for each specific recipient failure. Additional %()s
+# keys are:
+#
+# recipient -- the failing recipient address
+# failcode -- the smtp failure code
+# failmsg -- the actual smtp message, if available
+SMTP_LOG_EACH_FAILURE = (
+ 'smtp-failure',
+ ('${message-id} delivery to $recipient failed with code $failcode: '
+ '$failmsg'))
+
+# These variables control the format and frequency of VERP-like delivery for
+# better bounce detection. VERP is Variable Envelope Return Path, defined
+# here:
+#
+# http://cr.yp.to/proto/verp.txt
+#
+# This involves encoding the address of the recipient as we (Mailman) know it
+# into the envelope sender address (i.e. the SMTP `MAIL FROM:' address).
+# Thus, no matter what kind of forwarding the recipient has in place, should
+# it eventually bounce, we will receive an unambiguous notice of the bouncing
+# address.
+#
+# However, we're technically only "VERP-like" because we're doing the envelope
+# sender encoding in Mailman, not in the MTA. We do require cooperation from
+# the MTA, so you must be sure your MTA can be configured for extended address
+# semantics.
+#
+# The first variable describes how to encode VERP envelopes. It must contain
+# these three string interpolations:
+#
+# %(bounces)s -- the list-bounces mailbox will be set here
+# %(mailbox)s -- the recipient's mailbox will be set here
+# %(host)s -- the recipient's host name will be set here
+#
+# This example uses the default below.
+#
+# FQDN list address is: mylist@dom.ain
+# Recipient is: aperson@a.nother.dom
+#
+# The envelope sender will be mylist-bounces+aperson=a.nother.dom@dom.ain
+#
+# Note that your MTA /must/ be configured to deliver such an addressed message
+# to mylist-bounces!
+VERP_DELIMITER = '+'
+VERP_FORMAT = '%(bounces)s+%(mailbox)s=%(host)s'
+
+# The second describes a regular expression to unambiguously decode such an
+# address, which will be placed in the To: header of the bounce message by the
+# bouncing MTA. Getting this right is critical -- and tricky. Learn your
+# Python regular expressions. It must define exactly three named groups,
+# bounces, mailbox and host, with the same definition as above. It will be
+# compiled case-insensitively.
+VERP_REGEXP = r'^(?P<bounces>[^+]+?)\+(?P<mailbox>[^=]+)=(?P<host>[^@]+)@.*$'
+
+# VERP format and regexp for probe messages
+VERP_PROBE_FORMAT = '%(bounces)s+%(token)s'
+VERP_PROBE_REGEXP = r'^(?P<bounces>[^+]+?)\+(?P<token>[^@]+)@.*$'
+# Set this Yes to activate VERP probe for disabling by bounce
+VERP_PROBES = No
+
+# A perfect opportunity for doing VERP is the password reminders, which are
+# already addressed individually to each recipient. Set this to Yes to enable
+# VERPs on all password reminders.
+VERP_PASSWORD_REMINDERS = No
+
+# Another good opportunity is when regular delivery is personalized. Here
+# again, we're already incurring the performance hit for addressing each
+# individual recipient. Set this to Yes to enable VERPs on all personalized
+# regular deliveries (personalized digests aren't supported yet).
+VERP_PERSONALIZED_DELIVERIES = No
+
+# And finally, we can VERP normal, non-personalized deliveries. However,
+# because it can be a significant performance hit, we allow you to decide how
+# often to VERP regular deliveries. This is the interval, in number of
+# messages, to do a VERP recipient address. The same variable controls both
+# regular and digest deliveries. Set to 0 to disable occasional VERPs, set to
+# 1 to VERP every delivery, or to some number > 1 for only occasional VERPs.
+VERP_DELIVERY_INTERVAL = 0
+
+# For nicer confirmation emails, use a VERP-like format which encodes the
+# confirmation cookie in the reply address. This lets us put a more user
+# friendly Subject: on the message, but requires cooperation from the MTA.
+# Format is like VERP_FORMAT above, but with the following substitutions:
+#
+# $address -- the list-confirm address
+# $cookie -- the confirmation cookie
+VERP_CONFIRM_FORMAT = '$address+$cookie'
+
+# This is analogous to VERP_REGEXP, but for splitting apart the
+# VERP_CONFIRM_FORMAT. MUAs have been observed that mung
+# From: local_part@host
+# into
+# To: "local_part" <local_part@host>
+# when replying, so we skip everything up to '<' if any.
+VERP_CONFIRM_REGEXP = r'^(.*<)?(?P<addr>[^+]+?)\+(?P<cookie>[^@]+)@.*$'
+
+# Set this to Yes to enable VERP-like (more user friendly) confirmations
+VERP_CONFIRMATIONS = No
+
+# This is the maximum number of automatic responses sent to an address because
+# of -request messages or posting hold messages. This limit prevents response
+# loops between Mailman and misconfigured remote email robots. Mailman
+# already inhibits automatic replies to any message labeled with a header
+# "Precendence: bulk|list|junk". This is a fallback safety valve so it should
+# be set fairly high. Set to 0 for no limit (probably useful only for
+# debugging).
+MAX_AUTORESPONSES_PER_DAY = 10
+
+
+
+#####
+# Qrunner defaults
+#####
+
+# Which queues should the qrunner master watchdog spawn? add_qrunner() takes
+# one required argument, which is the name of the qrunner to start
+# (capitalized and without the 'Runner' suffix). Optional second argument
+# specifies the number of parallel processes to fork for each qrunner. If
+# more than one process is used, each will take an equal subdivision of the
+# hash space, so the number must be a power of 2.
+#
+# del_qrunners() takes one argument which is the name of the qrunner not to
+# start. This is used because by default, Mailman starts the Arch, Bounce,
+# Command, Incoming, News, Outgoing, Retry, and Virgin queues.
+#
+# Set this to Yes to use the `Maildir' delivery option. If you change this
+# you will need to re-run bin/genaliases for MTAs that don't use list
+# auto-detection.
+#
+# WARNING: If you want to use Maildir delivery, you /must/ start Mailman's
+# qrunner as root, or you will get permission problems.
+USE_MAILDIR = No
+
+# Set this to Yes to use the `LMTP' delivery option. If you change this
+# you will need to re-run bin/genaliases for MTAs that don't use list
+# auto-detection.
+#
+# You have to set following line in postfix main.cf:
+# transport_maps = hash:<prefix>/data/transport
+# Also needed is following line if your list is in $mydestination:
+# alias_maps = hash:/etc/aliases, hash:<prefix>/data/aliases
+USE_LMTP = No
+
+# Name of the domains which operate on LMTP Mailman only. Currently valid
+# only for Postfix alias generation.
+LMTP_ONLY_DOMAINS = []
+
+# If the list is not present in LMTP_ONLY_DOMAINS, LMTPRunner would return
+# 550 response to the master SMTP agent. This may cause 'bounce spam relay'
+# in that a spammer expects to deliver the message as bounce info to the
+# 'From:' address. You can override this behavior by setting
+# LMTP_ERR_550 = '250 Ok. But, blackholed because mailbox unavailable'.
+LMTP_ERR_550 = '550 Requested action not taken: mailbox unavailable'
+
+# WSGI Server.
+#
+# You must enable PROXY of Apache httpd server and configure to pass Mailman
+# CGI requests to this WSGI Server:
+#
+# ProxyPass /mailman/ http://localhost:2580/mailman/
+#
+# Note that local URI part should be the same.
+# XXX If you are running Apache 2.2, you will probably also want to set
+# ProxyPassReverseCookiePath
+#
+# Also you have to add following line to <prefix>/etc/mailman.cfg
+# add_qrunner('HTTP')
+HTTP_HOST = 'localhost'
+HTTP_PORT = 2580
+
+# After processing every file in the qrunner's slice, how long should the
+# runner sleep for before checking the queue directory again for new files?
+# This can be a fraction of a second, or zero to check immediately
+# (essentially busy-loop as fast as possible).
+QRUNNER_SLEEP_TIME = seconds(1)
+
+# When a message that is unparsable (by the email package) is received, what
+# should we do with it? The most common cause of unparsable messages is
+# broken MIME encapsulation, and the most common cause of that is viruses like
+# Nimda. Set this variable to No to discard such messages, or to Yes to store
+# them in qfiles/bad subdirectory.
+QRUNNER_SAVE_BAD_MESSAGES = Yes
+
+# This flag causes Mailman to fsync() its data files after writing and
+# flushing its contents. While this ensures the data is written to disk,
+# avoiding data loss, it may be a performance killer. Note that this flag
+# affects both message pickles and MailList config.pck files.
+SYNC_AFTER_WRITE = No
+
+# The maximum number of times that the mailmanctl watcher will try to restart
+# a qrunner that exits uncleanly.
+MAX_RESTARTS = 10
+
+
+
+#####
+# General defaults
+#####
+
+# The default language for this server. Whenever we can't figure out the list
+# context or user context, we'll fall back to using this language. This code
+# must be in the list of available language codes.
+DEFAULT_SERVER_LANGUAGE = u'en'
+
+# When allowing only members to post to a mailing list, how is the sender of
+# the message determined? If this variable is set to Yes, then first the
+# message's envelope sender is used, with a fallback to the sender if there is
+# no envelope sender. Set this variable to No to always use the sender.
+#
+# The envelope sender is set by the SMTP delivery and is thus less easily
+# spoofed than the sender, which is typically just taken from the From: header
+# and thus easily spoofed by the end-user. However, sometimes the envelope
+# sender isn't set correctly and this will manifest itself by postings being
+# held for approval even if they appear to come from a list member. If you
+# are having this problem, set this variable to No, but understand that some
+# spoofed messages may get through.
+USE_ENVELOPE_SENDER = No
+
+# Membership tests for posting purposes are usually performed by looking at a
+# set of headers, passing the test if any of their values match a member of
+# the list. Headers are checked in the order given in this variable. The
+# value None means use the From_ (envelope sender) header. Field names are
+# case insensitive.
+SENDER_HEADERS = ('from', None, 'reply-to', 'sender')
+
+# How many members to display at a time on the admin cgi to unsubscribe them
+# or change their options?
+DEFAULT_ADMIN_MEMBER_CHUNKSIZE = 30
+
+# how many bytes of a held message post should be displayed in the admindb web
+# page? Use a negative number to indicate the entire message, regardless of
+# size (though this will slow down rendering those pages).
+ADMINDB_PAGE_TEXT_LIMIT = 4096
+
+# Set this variable to Yes to allow list owners to delete their own mailing
+# lists. You may not want to give them this power, in which case, setting
+# this variable to No instead requires list removal to be done by the site
+# administrator, via the command line script bin/rmlist.
+OWNERS_CAN_DELETE_THEIR_OWN_LISTS = No
+
+# Set this variable to Yes to allow list owners to set the "personalized"
+# flags on their mailing lists. Turning these on tells Mailman to send
+# separate email messages to each user instead of batching them together for
+# delivery to the MTA. This gives each member a more personalized message,
+# but can have a heavy impact on the performance of your system.
+OWNERS_CAN_ENABLE_PERSONALIZATION = No
+
+# Should held messages be saved on disk as Python pickles or as plain text?
+# The former is more efficient since we don't need to go through the
+# parse/generate roundtrip each time, but the latter might be preferred if you
+# want to edit the held message on disk.
+HOLD_MESSAGES_AS_PICKLES = Yes
+
+# This variable controls the order in which list-specific category options are
+# presented in the admin cgi page.
+ADMIN_CATEGORIES = [
+ # First column
+ 'general', 'passwords', 'language', 'members', 'nondigest', 'digest',
+ # Second column
+ 'privacy', 'bounce', 'archive', 'gateway', 'autoreply',
+ 'contentfilter', 'topics',
+ ]
+
+# See "Bitfield for user options" below; make this a sum of those options, to
+# make all new members of lists start with those options flagged. We assume
+# by default that people don't want to receive two copies of posts. Note
+# however that the member moderation flag's initial value is controlled by the
+# list's config variable default_member_moderation.
+DEFAULT_NEW_MEMBER_OPTIONS = 256
+
+# Specify the type of passwords to use, when Mailman generates the passwords
+# itself, as would be the case for membership requests where the user did not
+# fill in a password, or during list creation, when auto-generation of admin
+# passwords was selected.
+#
+# Set this value to Yes for classic Mailman user-friendly(er) passwords.
+# These generate semi-pronounceable passwords which are easier to remember.
+# Set this value to No to use more cryptographically secure, but harder to
+# remember, passwords -- if your operating system and Python version support
+# the necessary feature (specifically that /dev/urandom be available).
+USER_FRIENDLY_PASSWORDS = Yes
+# This value specifies the default lengths of member and list admin passwords
+MEMBER_PASSWORD_LENGTH = 8
+ADMIN_PASSWORD_LENGTH = 10
+
+
+
+#####
+# List defaults. NOTE: Changing these values does NOT change the
+# configuration of an existing list. It only defines the default for new
+# lists you subsequently create.
+#####
+
+# Should a list, by default be advertised? What is the default maximum number
+# of explicit recipients allowed? What is the default maximum message size
+# allowed?
+DEFAULT_LIST_ADVERTISED = Yes
+DEFAULT_MAX_NUM_RECIPIENTS = 10
+DEFAULT_MAX_MESSAGE_SIZE = 40 # KB
+
+# These format strings will be expanded w.r.t. the dictionary for the
+# mailing list instance.
+DEFAULT_SUBJECT_PREFIX = u'[$mlist.real_name] '
+# DEFAULT_SUBJECT_PREFIX = "[$mlist.real_name %%d]" # for numbering
+DEFAULT_MSG_HEADER = u''
+DEFAULT_MSG_FOOTER = u"""\
+_______________________________________________
+$real_name mailing list
+$fqdn_listname
+${listinfo_page}
+"""
+
+# Scrub regular delivery
+DEFAULT_SCRUB_NONDIGEST = False
+
+# Mail command processor will ignore mail command lines after designated max.
+EMAIL_COMMANDS_MAX_LINES = 10
+
+# Is the list owner notified of admin requests immediately by mail, as well as
+# by daily pending-request reminder?
+DEFAULT_ADMIN_IMMED_NOTIFY = Yes
+
+# Is the list owner notified of subscribes/unsubscribes?
+DEFAULT_ADMIN_NOTIFY_MCHANGES = No
+
+# Discard held messages after this days
+DEFAULT_MAX_DAYS_TO_HOLD = 0
+
+# Should list members, by default, have their posts be moderated?
+DEFAULT_DEFAULT_MEMBER_MODERATION = No
+
+# Should non-member posts which are auto-discarded also be forwarded to the
+# moderators?
+DEFAULT_FORWARD_AUTO_DISCARDS = Yes
+
+# What shold happen to non-member posts which are do not match explicit
+# non-member actions?
+# 0 = Accept
+# 1 = Hold
+# 2 = Reject
+# 3 = Discard
+DEFAULT_GENERIC_NONMEMBER_ACTION = 1
+
+# Bounce if 'To:', 'Cc:', or 'Resent-To:' fields don't explicitly name list?
+# This is an anti-spam measure
+DEFAULT_REQUIRE_EXPLICIT_DESTINATION = Yes
+
+# Alternate names acceptable as explicit destinations for this list.
+DEFAULT_ACCEPTABLE_ALIASES = """
+"""
+# For mailing lists that have only other mailing lists for members:
+DEFAULT_UMBRELLA_LIST = No
+
+# For umbrella lists, the suffix for the account part of address for
+# administrative notices (subscription confirmations, password reminders):
+DEFAULT_UMBRELLA_MEMBER_ADMIN_SUFFIX = "-owner"
+
+# This variable controls whether monthly password reminders are sent.
+DEFAULT_SEND_REMINDERS = Yes
+
+# Send welcome messages to new users?
+DEFAULT_SEND_WELCOME_MSG = Yes
+
+# Send goodbye messages to unsubscribed members?
+DEFAULT_SEND_GOODBYE_MSG = Yes
+
+# Wipe sender information, and make it look like the list-admin
+# address sends all messages
+DEFAULT_ANONYMOUS_LIST = No
+
+# {header-name: regexp} spam filtering - we include some for example sake.
+DEFAULT_BOUNCE_MATCHING_HEADERS = u"""
+# Lines that *start* with a '#' are comments.
+to: friend@public.com
+message-id: relay.comanche.denmark.eu
+from: list@listme.com
+from: .*@uplinkpro.com
+"""
+
+# Mailman can be configured to "munge" Reply-To: headers for any passing
+# messages. One the one hand, there are a lot of good reasons not to munge
+# Reply-To: but on the other, people really seem to want this feature. See
+# the help for reply_goes_to_list in the web UI for links discussing the
+# issue.
+# 0 - Reply-To: not munged
+# 1 - Reply-To: set back to the list
+# 2 - Reply-To: set to an explicit value (reply_to_address)
+DEFAULT_REPLY_GOES_TO_LIST = ReplyToMunging.no_munging
+
+# Mailman can be configured to strip any existing Reply-To: header, or simply
+# extend any existing Reply-To: with one based on the above setting.
+DEFAULT_FIRST_STRIP_REPLY_TO = No
+
+# SUBSCRIBE POLICY
+# 0 - open list (only when ALLOW_OPEN_SUBSCRIBE is set to 1) **
+# 1 - confirmation required for subscribes
+# 2 - admin approval required for subscribes
+# 3 - both confirmation and admin approval required
+#
+# ** please do not choose option 0 if you are not allowing open
+# subscribes (next variable)
+DEFAULT_SUBSCRIBE_POLICY = 1
+
+# Does this site allow completely unchecked subscriptions?
+ALLOW_OPEN_SUBSCRIBE = No
+
+# This is the default list of addresses and regular expressions (beginning
+# with ^) that are exempt from approval if SUBSCRIBE_POLICY is 2 or 3.
+DEFAULT_SUBSCRIBE_AUTO_APPROVAL = []
+
+# The default policy for unsubscriptions. 0 (unmoderated unsubscribes) is
+# highly recommended!
+# 0 - unmoderated unsubscribes
+# 1 - unsubscribes require approval
+DEFAULT_UNSUBSCRIBE_POLICY = 0
+
+# Private_roster == 0: anyone can see, 1: members only, 2: admin only.
+DEFAULT_PRIVATE_ROSTER = 1
+
+# When exposing members, make them unrecognizable as email addrs, so
+# web-spiders can't pick up addrs for spam purposes.
+DEFAULT_OBSCURE_ADDRESSES = Yes
+
+# RFC 2369 defines List-* headers which are added to every message sent
+# through to the mailing list membership. These are a very useful aid to end
+# users and should always be added. However, not all MUAs are compliant and
+# if a list's membership has many such users, they may clamor for these
+# headers to be suppressed. By setting this variable to Yes, list owners will
+# be given the option to suppress these headers. By setting it to No, list
+# owners will not be given the option to suppress these headers (although some
+# header suppression may still take place, i.e. for announce-only lists, or
+# lists with no archives).
+ALLOW_RFC2369_OVERRIDES = Yes
+
+# Defaults for content filtering on mailing lists. DEFAULT_FILTER_CONTENT is
+# a flag which if set to true, turns on content filtering.
+DEFAULT_FILTER_CONTENT = No
+
+# DEFAULT_FILTER_MIME_TYPES is a list of MIME types to be removed. This is a
+# list of strings of the format "maintype/subtype" or simply "maintype".
+# E.g. "text/html" strips all html attachments while "image" strips all image
+# types regardless of subtype (jpeg, gif, etc.).
+DEFAULT_FILTER_MIME_TYPES = []
+
+# DEFAULT_PASS_MIME_TYPES is a list of MIME types to be passed through.
+# Format is the same as DEFAULT_FILTER_MIME_TYPES
+DEFAULT_PASS_MIME_TYPES = ['multipart/mixed',
+ 'multipart/alternative',
+ 'text/plain']
+
+# DEFAULT_FILTER_FILENAME_EXTENSIONS is a list of filename extensions to be
+# removed. It is useful because many viruses fake their content-type as
+# harmless ones while keep their extension as executable and expect to be
+# executed when victims 'open' them.
+DEFAULT_FILTER_FILENAME_EXTENSIONS = [
+ 'exe', 'bat', 'cmd', 'com', 'pif', 'scr', 'vbs', 'cpl'
+ ]
+
+# DEFAULT_PASS_FILENAME_EXTENSIONS is a list of filename extensions to be
+# passed through. Format is the same as DEFAULT_FILTER_FILENAME_EXTENSIONS.
+DEFAULT_PASS_FILENAME_EXTENSIONS = []
+
+# Replace multipart/alternative with its first alternative.
+DEFAULT_COLLAPSE_ALTERNATIVES = Yes
+
+# Whether text/html should be converted to text/plain after content filtering
+# is performed. Conversion is done according to HTML_TO_PLAIN_TEXT_COMMAND
+DEFAULT_CONVERT_HTML_TO_PLAINTEXT = Yes
+
+# Default action to take on filtered messages.
+# 0 = Discard, 1 = Reject, 2 = Forward, 3 = Preserve
+DEFAULT_FILTER_ACTION = 0
+
+# Whether to allow list owners to preserve content filtered messages to a
+# special queue on the disk.
+OWNERS_CAN_PRESERVE_FILTERED_MESSAGES = Yes
+
+# Check for administrivia in messages sent to the main list?
+DEFAULT_ADMINISTRIVIA = Yes
+
+
+
+#####
+# Digestification defaults. Same caveat applies here as with list defaults.
+#####
+
+# Will list be available in non-digested form?
+DEFAULT_NONDIGESTABLE = Yes
+
+# Will list be available in digested form?
+DEFAULT_DIGESTABLE = Yes
+DEFAULT_DIGEST_HEADER = u''
+DEFAULT_DIGEST_FOOTER = DEFAULT_MSG_FOOTER
+
+DEFAULT_DIGEST_IS_DEFAULT = No
+DEFAULT_MIME_IS_DEFAULT_DIGEST = No
+DEFAULT_DIGEST_SIZE_THRESHOLD = 30 # KB
+DEFAULT_DIGEST_SEND_PERIODIC = Yes
+
+# Headers which should be kept in both RFC 1153 (plain) and MIME digests. RFC
+# 1153 also specifies these headers in this exact order, so order matters.
+MIME_DIGEST_KEEP_HEADERS = [
+ 'Date', 'From', 'To', 'Cc', 'Subject', 'Message-ID', 'Keywords',
+ # I believe we should also keep these headers though.
+ 'In-Reply-To', 'References', 'Content-Type', 'MIME-Version',
+ 'Content-Transfer-Encoding', 'Precedence', 'Reply-To',
+ # Mailman 2.0 adds these headers
+ 'Message',
+ ]
+
+PLAIN_DIGEST_KEEP_HEADERS = [
+ 'Message', 'Date', 'From',
+ 'Subject', 'To', 'Cc',
+ 'Message-ID', 'Keywords',
+ 'Content-Type',
+ ]
+
+
+
+#####
+# Bounce processing defaults. Same caveat applies here as with list defaults.
+#####
+
+# Should we do any bounced mail response at all?
+DEFAULT_BOUNCE_PROCESSING = Yes
+
+# How often should the bounce qrunner process queued detected bounces?
+REGISTER_BOUNCES_EVERY = minutes(15)
+
+# Bounce processing works like this: when a bounce from a member is received,
+# we look up the `bounce info' for this member. If there is no bounce info,
+# this is the first bounce we've received from this member. In that case, we
+# record today's date, and initialize the bounce score (see below for initial
+# value).
+#
+# If there is existing bounce info for this member, we look at the last bounce
+# receive date. If this date is farther away from today than the `bounce
+# expiration interval', we throw away all the old data and initialize the
+# bounce score as if this were the first bounce from the member.
+#
+# Otherwise, we increment the bounce score. If we can determine whether the
+# bounce was soft or hard (i.e. transient or fatal), then we use a score value
+# of 0.5 for soft bounces and 1.0 for hard bounces. Note that we only score
+# one bounce per day. If the bounce score is then greater than the `bounce
+# threshold' we disable the member's address.
+#
+# After disabling the address, we can send warning messages to the member,
+# providing a confirmation cookie/url for them to use to re-enable their
+# delivery. After a configurable period of time, we'll delete the address.
+# When we delete the address due to bouncing, we'll send one last message to
+# the member.
+
+# Bounce scores greater than this value get disabled.
+DEFAULT_BOUNCE_SCORE_THRESHOLD = 5.0
+
+# Bounce information older than this interval is considered stale, and is
+# discarded.
+DEFAULT_BOUNCE_INFO_STALE_AFTER = days(7)
+
+# The number of notifications to send to the disabled/removed member before we
+# remove them from the list. A value of 0 means we remove the address
+# immediately (with one last notification). Note that the first one is sent
+# upon change of status to disabled.
+DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS = 3
+
+# The interval of time between disabled warnings.
+DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL = days(7)
+
+# Does the list owner get messages to the -bounces (and -admin) address that
+# failed to match by the bounce detector?
+DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER = Yes
+
+# Notifications on bounce actions. The first specifies whether the list owner
+# should get a notification when a member is disabled due to bouncing, while
+# the second specifies whether the owner should get one when the member is
+# removed due to bouncing.
+DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE = Yes
+DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL = Yes
+
+
+
+#####
+# General time limits
+#####
+
+# Default length of time a pending request is live before it is evicted from
+# the pending database.
+PENDING_REQUEST_LIFE = days(3)
+
+# How long should messages which have delivery failures continue to be
+# retried? After this period of time, a message that has failed recipients
+# will be dequeued and those recipients will never receive the message.
+DELIVERY_RETRY_PERIOD = days(5)
+
+# How long should we wait before we retry a temporary delivery failure?
+DELIVERY_RETRY_WAIT = hours(1)
+
+
+
+#####
+# Lock management defaults
+#####
+
+# These variables control certain aspects of lock acquisition and retention.
+# They should be tuned as appropriate for your environment. All variables are
+# specified in units of floating point seconds. YOU MAY NEED TO TUNE THESE
+# VARIABLES DEPENDING ON THE SIZE OF YOUR LISTS, THE PERFORMANCE OF YOUR
+# HARDWARE, NETWORK AND GENERAL MAIL HANDLING CAPABILITIES, ETC.
+
+# This variable specifies how long the lock will be retained for a specific
+# operation on a mailing list. Watch your logs/lock file and if you see a lot
+# of lock breakages, you might need to bump this up. However if you set this
+# too high, a faulty script (or incorrect use of bin/withlist) can prevent the
+# list from being used until the lifetime expires. This is probably one of
+# the most crucial tuning variables in the system.
+LIST_LOCK_LIFETIME = hours(5)
+
+# This variable specifies how long an attempt will be made to acquire a list
+# lock by the incoming qrunner process. If the lock acquisition times out,
+# the message will be re-queued for later delivery.
+LIST_LOCK_TIMEOUT = seconds(10)
+
+# Set this to On to turn on lock debugging messages for the pending requests
+# database, which will be written to logs/locks. If you think you're having
+# lock problems, or just want to tune the locks for your system, turn on lock
+# debugging.
+PENDINGDB_LOCK_DEBUGGING = Off
+
+
+
+#####
+# Nothing below here is user configurable. Most of these values are in this
+# file for internal system convenience. Don't change any of them or override
+# any of them in your mailman.cfg file!
+#####
+
+# Enumeration for Mailman cgi widget types
+Toggle = 1
+Radio = 2
+String = 3
+Text = 4
+Email = 5
+EmailList = 6
+Host = 7
+Number = 8
+FileUpload = 9
+Select = 10
+Topics = 11
+Checkbox = 12
+# An "extended email list". Contents must be an email address or a ^-prefixed
+# regular expression. Used in the sender moderation text boxes.
+EmailListEx = 13
+# Extended spam filter widget
+HeaderFilter = 14
+
+# Actions
+DEFER = 0
+APPROVE = 1
+REJECT = 2
+DISCARD = 3
+SUBSCRIBE = 4
+UNSUBSCRIBE = 5
+ACCEPT = 6
+HOLD = 7
+
+# Standard text field width
+TEXTFIELDWIDTH = 40
+
+# Bitfield for user options. See DEFAULT_NEW_MEMBER_OPTIONS above to set
+# defaults for all new lists.
+Digests = 0 # handled by other mechanism, doesn't need a flag.
+DisableDelivery = 1 # Obsolete; use set/getDeliveryStatus()
+DontReceiveOwnPosts = 2 # Non-digesters only
+AcknowledgePosts = 4
+DisableMime = 8 # Digesters only
+ConcealSubscription = 16
+SuppressPasswordReminder = 32
+ReceiveNonmatchingTopics = 64
+Moderate = 128
+DontReceiveDuplicates = 256
+
+
+# A mapping between short option tags and their flag
+OPTINFO = {'hide' : ConcealSubscription,
+ 'nomail' : DisableDelivery,
+ 'ack' : AcknowledgePosts,
+ 'notmetoo': DontReceiveOwnPosts,
+ 'digest' : 0,
+ 'plain' : DisableMime,
+ 'nodupes' : DontReceiveDuplicates
+ }
+
+# Authentication contexts.
+#
+# Mailman defines the following roles:
+
+# - User, a normal user who has no permissions except to change their personal
+# option settings
+# - List creator, someone who can create and delete lists, but cannot
+# (necessarily) configure the list.
+# - List moderator, someone who can tend to pending requests such as
+# subscription requests, or held messages
+# - List administrator, someone who has total control over a list, can
+# configure it, modify user options for members of the list, subscribe and
+# unsubscribe members, etc.
+# - Site administrator, someone who has total control over the entire site and
+# can do any of the tasks mentioned above. This person usually also has
+# command line access.
+
+UnAuthorized = 0
+AuthUser = 1 # Joe Shmoe User
+AuthCreator = 2 # List Creator / Destroyer
+AuthListAdmin = 3 # List Administrator (total control over list)
+AuthListModerator = 4 # List Moderator (can only handle held requests)
+AuthSiteAdmin = 5 # Site Administrator (total control over everything)
+
+
+
+# Vgg: Language descriptions and charsets dictionary, any new supported
+# language must have a corresponding entry here. Key is the name of the
+# directories that hold the localized texts. Data are tuples with first
+# element being the description, as described in the catalogs, and second
+# element is the language charset. I have chosen code from /usr/share/locale
+# in my GNU/Linux. :-)
+#
+# TK: Now the site admin can select languages for the installation from those
+# in the distribution tarball. We don't touch add_language() function for
+# backward compatibility. You may have to add your own language in your
+# mailman.cfg file, if it is not included in the distribution even if you had
+# put language files in source directory and configured by `--with-languages'
+# option.
+def _(s):
+ return s
+
+_DEFAULT_LANGUAGE_DATA = {
+ 'ar': (_('Arabic'), 'utf-8'),
+ 'ca': (_('Catalan'), 'iso-8859-1'),
+ 'cs': (_('Czech'), 'iso-8859-2'),
+ 'da': (_('Danish'), 'iso-8859-1'),
+ 'de': (_('German'), 'iso-8859-1'),
+ 'en': (_('English (USA)'), 'us-ascii'),
+ 'es': (_('Spanish (Spain)'), 'iso-8859-1'),
+ 'et': (_('Estonian'), 'iso-8859-15'),
+ 'eu': (_('Euskara'), 'iso-8859-15'), # Basque
+ 'fi': (_('Finnish'), 'iso-8859-1'),
+ 'fr': (_('French'), 'iso-8859-1'),
+ 'hr': (_('Croatian'), 'iso-8859-2'),
+ 'hu': (_('Hungarian'), 'iso-8859-2'),
+ 'ia': (_('Interlingua'), 'iso-8859-15'),
+ 'it': (_('Italian'), 'iso-8859-1'),
+ 'ja': (_('Japanese'), 'euc-jp'),
+ 'ko': (_('Korean'), 'euc-kr'),
+ 'lt': (_('Lithuanian'), 'iso-8859-13'),
+ 'nl': (_('Dutch'), 'iso-8859-1'),
+ 'no': (_('Norwegian'), 'iso-8859-1'),
+ 'pl': (_('Polish'), 'iso-8859-2'),
+ 'pt': (_('Portuguese'), 'iso-8859-1'),
+ 'pt_BR': (_('Portuguese (Brazil)'), 'iso-8859-1'),
+ 'ro': (_('Romanian'), 'iso-8859-2'),
+ 'ru': (_('Russian'), 'koi8-r'),
+ 'sr': (_('Serbian'), 'utf-8'),
+ 'sl': (_('Slovenian'), 'iso-8859-2'),
+ 'sv': (_('Swedish'), 'iso-8859-1'),
+ 'tr': (_('Turkish'), 'iso-8859-9'),
+ 'uk': (_('Ukrainian'), 'utf-8'),
+ 'vi': (_('Vietnamese'), 'utf-8'),
+ 'zh_CN': (_('Chinese (China)'), 'utf-8'),
+ 'zh_TW': (_('Chinese (Taiwan)'), 'utf-8'),
+}
+
+
+del _
diff --git a/src/mailman/attic/Deliverer.py b/src/mailman/attic/Deliverer.py
new file mode 100644
index 000000000..0ba3a01bb
--- /dev/null
+++ b/src/mailman/attic/Deliverer.py
@@ -0,0 +1,174 @@
+# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""Mixin class with message delivery routines."""
+
+from __future__ import with_statement
+
+import logging
+
+from email.MIMEMessage import MIMEMessage
+from email.MIMEText import MIMEText
+
+from mailman import Errors
+from mailman import Message
+from mailman import Utils
+from mailman import i18n
+from mailman.configuration import config
+
+_ = i18n._
+
+log = logging.getLogger('mailman.error')
+mlog = logging.getLogger('mailman.mischief')
+
+
+
+class Deliverer:
+ def MailUserPassword(self, user):
+ listfullname = self.fqdn_listname
+ requestaddr = self.GetRequestEmail()
+ # find the lowercased version of the user's address
+ adminaddr = self.GetBouncesEmail()
+ assert self.isMember(user)
+ if not self.getMemberPassword(user):
+ # The user's password somehow got corrupted. Generate a new one
+ # for him, after logging this bogosity.
+ log.error('User %s had a false password for list %s',
+ user, self.internal_name())
+ waslocked = self.Locked()
+ if not waslocked:
+ self.Lock()
+ try:
+ self.setMemberPassword(user, Utils.MakeRandomPassword())
+ self.Save()
+ finally:
+ if not waslocked:
+ self.Unlock()
+ # Now send the user his password
+ cpuser = self.getMemberCPAddress(user)
+ recipient = self.GetMemberAdminEmail(cpuser)
+ subject = _('%(listfullname)s mailing list reminder')
+ # Get user's language and charset
+ lang = self.getMemberLanguage(user)
+ cset = Utils.GetCharSet(lang)
+ password = self.getMemberPassword(user)
+ # TK: Make unprintables to ?
+ # The list owner should allow users to set language options if they
+ # want to use non-us-ascii characters in password and send it back.
+ password = unicode(password, cset, 'replace').encode(cset, 'replace')
+ # get the text from the template
+ text = Utils.maketext(
+ 'userpass.txt',
+ {'user' : cpuser,
+ 'listname' : self.real_name,
+ 'fqdn_lname' : self.GetListEmail(),
+ 'password' : password,
+ 'options_url': self.GetOptionsURL(user, absolute=True),
+ 'requestaddr': requestaddr,
+ 'owneraddr' : self.GetOwnerEmail(),
+ }, lang=lang, mlist=self)
+ msg = Message.UserNotification(recipient, adminaddr, subject, text,
+ lang)
+ msg['X-No-Archive'] = 'yes'
+ msg.send(self, verp=config.VERP_PERSONALIZED_DELIVERIES)
+
+ def ForwardMessage(self, msg, text=None, subject=None, tomoderators=True):
+ # Wrap the message as an attachment
+ if text is None:
+ text = _('No reason given')
+ if subject is None:
+ text = _('(no subject)')
+ text = MIMEText(Utils.wrap(text),
+ _charset=Utils.GetCharSet(self.preferred_language))
+ attachment = MIMEMessage(msg)
+ notice = Message.OwnerNotification(
+ self, subject, tomoderators=tomoderators)
+ # Make it look like the message is going to the -owner address
+ notice.set_type('multipart/mixed')
+ notice.attach(text)
+ notice.attach(attachment)
+ notice.send(self)
+
+ def SendHostileSubscriptionNotice(self, listname, address):
+ # Some one was invited to one list but tried to confirm to a different
+ # list. We inform both list owners of the bogosity, but be careful
+ # not to reveal too much information.
+ selfname = self.internal_name()
+ mlog.error('%s was invited to %s but confirmed to %s',
+ address, listname, selfname)
+ # First send a notice to the attacked list
+ msg = Message.OwnerNotification(
+ self,
+ _('Hostile subscription attempt detected'),
+ Utils.wrap(_("""%(address)s was invited to a different mailing
+list, but in a deliberate malicious attempt they tried to confirm the
+invitation to your list. We just thought you'd like to know. No further
+action by you is required.""")))
+ msg.send(self)
+ # Now send a notice to the invitee list
+ try:
+ # Avoid import loops
+ from mailman.MailList import MailList
+ mlist = MailList(listname, lock=False)
+ except Errors.MMListError:
+ # Oh well
+ return
+ with i18n.using_language(mlist.preferred_language):
+ msg = Message.OwnerNotification(
+ mlist,
+ _('Hostile subscription attempt detected'),
+ Utils.wrap(_("""You invited %(address)s to your list, but in a
+deliberate malicious attempt, they tried to confirm the invitation to a
+different list. We just thought you'd like to know. No further action by you
+is required.""")))
+ msg.send(mlist)
+
+ def sendProbe(self, member, msg):
+ listname = self.real_name
+ # Put together the substitution dictionary.
+ d = {'listname': listname,
+ 'address': member,
+ 'optionsurl': self.GetOptionsURL(member, absolute=True),
+ 'owneraddr': self.GetOwnerEmail(),
+ }
+ text = Utils.maketext('probe.txt', d,
+ lang=self.getMemberLanguage(member),
+ mlist=self)
+ # Calculate the VERP'd sender address for bounce processing of the
+ # probe message.
+ token = self.pend_new(Pending.PROBE_BOUNCE, member, msg)
+ probedict = {
+ 'bounces': self.internal_name() + '-bounces',
+ 'token': token,
+ }
+ probeaddr = '%s@%s' % ((config.VERP_PROBE_FORMAT % probedict),
+ self.host_name)
+ # Calculate the Subject header, in the member's preferred language
+ ulang = self.getMemberLanguage(member)
+ with i18n.using_language(ulang):
+ subject = _('%(listname)s mailing list probe message')
+ outer = Message.UserNotification(member, probeaddr, subject,
+ lang=ulang)
+ outer.set_type('multipart/mixed')
+ text = MIMEText(text, _charset=Utils.GetCharSet(ulang))
+ outer.attach(text)
+ outer.attach(MIMEMessage(msg))
+ # Turn off further VERP'ing in the final delivery step. We set
+ # probe_token for the OutgoingRunner to more easily handling local
+ # rejects of probe messages.
+ outer.send(self, envsender=probeaddr, verp=False, probe_token=token)
diff --git a/src/mailman/attic/Digester.py b/src/mailman/attic/Digester.py
new file mode 100644
index 000000000..a88d08abc
--- /dev/null
+++ b/src/mailman/attic/Digester.py
@@ -0,0 +1,57 @@
+# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""Mixin class with list-digest handling methods and settings."""
+
+import os
+import errno
+
+from mailman import Errors
+from mailman import Utils
+from mailman.Handlers import ToDigest
+from mailman.configuration import config
+from mailman.i18n import _
+
+
+
+class Digester:
+ def send_digest_now(self):
+ # Note: Handler.ToDigest.send_digests() handles bumping the digest
+ # volume and issue number.
+ digestmbox = os.path.join(self.fullpath(), 'digest.mbox')
+ try:
+ try:
+ mboxfp = None
+ # See if there's a digest pending for this mailing list
+ if os.stat(digestmbox).st_size > 0:
+ mboxfp = open(digestmbox)
+ ToDigest.send_digests(self, mboxfp)
+ os.unlink(digestmbox)
+ finally:
+ if mboxfp:
+ mboxfp.close()
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ # List has no outstanding digests
+ return False
+ return True
+
+ def bump_digest_volume(self):
+ self.volume += 1
+ self.next_digest_number = 1
diff --git a/src/mailman/attic/MailList.py b/src/mailman/attic/MailList.py
new file mode 100644
index 000000000..2d538f026
--- /dev/null
+++ b/src/mailman/attic/MailList.py
@@ -0,0 +1,731 @@
+# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""The class representing a Mailman mailing list.
+
+Mixes in many task-specific classes.
+"""
+
+from __future__ import with_statement
+
+import os
+import re
+import sys
+import time
+import errno
+import shutil
+import socket
+import urllib
+import cPickle
+import logging
+import marshal
+import email.Iterators
+
+from UserDict import UserDict
+from cStringIO import StringIO
+from string import Template
+from types import MethodType
+from urlparse import urlparse
+from zope.interface import implements
+
+from email.Header import Header
+from email.Utils import getaddresses, formataddr, parseaddr
+
+from Mailman import Errors
+from Mailman import Utils
+from Mailman import Version
+from Mailman import database
+from Mailman.UserDesc import UserDesc
+from Mailman.configuration import config
+from Mailman.interfaces import *
+
+# Base classes
+from Mailman.Archiver import Archiver
+from Mailman.Bouncer import Bouncer
+from Mailman.Digester import Digester
+from Mailman.SecurityManager import SecurityManager
+
+# GUI components package
+from Mailman import Gui
+
+# Other useful classes
+from Mailman import i18n
+from Mailman import MemberAdaptor
+from Mailman import Message
+
+_ = i18n._
+
+DOT = '.'
+EMPTYSTRING = ''
+OR = '|'
+
+clog = logging.getLogger('mailman.config')
+elog = logging.getLogger('mailman.error')
+vlog = logging.getLogger('mailman.vette')
+slog = logging.getLogger('mailman.subscribe')
+
+
+
+# Use mixins here just to avoid having any one chunk be too large.
+class MailList(object, Archiver, Digester, SecurityManager, Bouncer):
+
+ implements(
+ IMailingList,
+ IMailingListAddresses,
+ IMailingListIdentity,
+ IMailingListRosters,
+ )
+
+ def __init__(self, data):
+ self._data = data
+ # Only one level of mixin inheritance allowed.
+ for baseclass in self.__class__.__bases__:
+ if hasattr(baseclass, '__init__'):
+ baseclass.__init__(self)
+ # Initialize the web u/i components.
+ self._gui = []
+ for component in dir(Gui):
+ if component.startswith('_'):
+ continue
+ self._gui.append(getattr(Gui, component)())
+ # Give the extension mechanism a chance to process this list.
+ try:
+ from Mailman.ext import init_mlist
+ except ImportError:
+ pass
+ else:
+ init_mlist(self)
+
+ def __getattr__(self, name):
+ missing = object()
+ if name.startswith('_'):
+ return getattr(super(MailList, self), name)
+ # Delegate to the database model object if it has the attribute.
+ obj = getattr(self._data, name, missing)
+ if obj is not missing:
+ return obj
+ # Finally, delegate to one of the gui components.
+ for guicomponent in self._gui:
+ obj = getattr(guicomponent, name, missing)
+ if obj is not missing:
+ return obj
+ # Nothing left to delegate to, so it's got to be an error.
+ raise AttributeError(name)
+
+ def __repr__(self):
+ return '<mailing list "%s" at %x>' % (self.fqdn_listname, id(self))
+
+
+ def GetConfirmJoinSubject(self, listname, cookie):
+ if config.VERP_CONFIRMATIONS and cookie:
+ cset = i18n.get_translation().charset() or \
+ Utils.GetCharSet(self.preferred_language)
+ subj = Header(
+ _('Your confirmation is required to join the %(listname)s mailing list'),
+ cset, header_name='subject')
+ return subj
+ else:
+ return 'confirm ' + cookie
+
+ def GetConfirmLeaveSubject(self, listname, cookie):
+ if config.VERP_CONFIRMATIONS and cookie:
+ cset = i18n.get_translation().charset() or \
+ Utils.GetCharSet(self.preferred_language)
+ subj = Header(
+ _('Your confirmation is required to leave the %(listname)s mailing list'),
+ cset, header_name='subject')
+ return subj
+ else:
+ return 'confirm ' + cookie
+
+ def GetMemberAdminEmail(self, member):
+ """Usually the member addr, but modified for umbrella lists.
+
+ Umbrella lists have other mailing lists as members, and so admin stuff
+ like confirmation requests and passwords must not be sent to the
+ member addresses - the sublists - but rather to the administrators of
+ the sublists. This routine picks the right address, considering
+ regular member address to be their own administrative addresses.
+
+ """
+ if not self.umbrella_list:
+ return member
+ else:
+ acct, host = tuple(member.split('@'))
+ return "%s%s@%s" % (acct, self.umbrella_member_suffix, host)
+
+ def GetScriptURL(self, target, absolute=False):
+ if absolute:
+ return self.web_page_url + target + '/' + self.fqdn_listname
+ else:
+ return Utils.ScriptURL(target) + '/' + self.fqdn_listname
+
+ def GetOptionsURL(self, user, obscure=False, absolute=False):
+ url = self.GetScriptURL('options', absolute)
+ if obscure:
+ user = Utils.ObscureEmail(user)
+ return '%s/%s' % (url, urllib.quote(user.lower()))
+
+
+ #
+ # Web API support via administrative categories
+ #
+ def GetConfigCategories(self):
+ class CategoryDict(UserDict):
+ def __init__(self):
+ UserDict.__init__(self)
+ self.keysinorder = config.ADMIN_CATEGORIES[:]
+ def keys(self):
+ return self.keysinorder
+ def items(self):
+ items = []
+ for k in config.ADMIN_CATEGORIES:
+ items.append((k, self.data[k]))
+ return items
+ def values(self):
+ values = []
+ for k in config.ADMIN_CATEGORIES:
+ values.append(self.data[k])
+ return values
+
+ categories = CategoryDict()
+ # Only one level of mixin inheritance allowed
+ for gui in self._gui:
+ k, v = gui.GetConfigCategory()
+ categories[k] = (v, gui)
+ return categories
+
+ def GetConfigSubCategories(self, category):
+ for gui in self._gui:
+ if hasattr(gui, 'GetConfigSubCategories'):
+ # Return the first one that knows about the given subcategory
+ subcat = gui.GetConfigSubCategories(category)
+ if subcat is not None:
+ return subcat
+ return None
+
+ def GetConfigInfo(self, category, subcat=None):
+ for gui in self._gui:
+ if hasattr(gui, 'GetConfigInfo'):
+ value = gui.GetConfigInfo(self, category, subcat)
+ if value:
+ return value
+
+
+ #
+ # Membership management front-ends and assertion checks
+ #
+ def InviteNewMember(self, userdesc, text=''):
+ """Invite a new member to the list.
+
+ This is done by creating a subscription pending for the user, and then
+ crafting a message to the member informing them of the invitation.
+ """
+ invitee = userdesc.address
+ Utils.ValidateEmail(invitee)
+ # check for banned address
+ pattern = Utils.get_pattern(invitee, self.ban_list)
+ if pattern:
+ raise Errors.MembershipIsBanned(pattern)
+ # Hack alert! Squirrel away a flag that only invitations have, so
+ # that we can do something slightly different when an invitation
+ # subscription is confirmed. In those cases, we don't need further
+ # admin approval, even if the list is so configured. The flag is the
+ # list name to prevent invitees from cross-subscribing.
+ userdesc.invitation = self.internal_name()
+ cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc)
+ requestaddr = self.getListAddress('request')
+ confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
+ cookie)
+ listname = self.real_name
+ text += Utils.maketext(
+ 'invite.txt',
+ {'email' : invitee,
+ 'listname' : listname,
+ 'hostname' : self.host_name,
+ 'confirmurl' : confirmurl,
+ 'requestaddr': requestaddr,
+ 'cookie' : cookie,
+ 'listowner' : self.GetOwnerEmail(),
+ }, mlist=self)
+ sender = self.GetRequestEmail(cookie)
+ msg = Message.UserNotification(
+ invitee, sender,
+ text=text, lang=self.preferred_language)
+ subj = self.GetConfirmJoinSubject(listname, cookie)
+ del msg['subject']
+ msg['Subject'] = subj
+ msg.send(self)
+
+ def AddMember(self, userdesc, remote=None):
+ """Front end to member subscription.
+
+ This method enforces subscription policy, validates values, sends
+ notifications, and any other grunt work involved in subscribing a
+ user. It eventually calls ApprovedAddMember() to do the actual work
+ of subscribing the user.
+
+ userdesc is an instance with the following public attributes:
+
+ address -- the unvalidated email address of the member
+ fullname -- the member's full name (i.e. John Smith)
+ digest -- a flag indicating whether the user wants digests or not
+ language -- the requested default language for the user
+ password -- the user's password
+
+ Other attributes may be defined later. Only address is required; the
+ others all have defaults (fullname='', digests=0, language=list's
+ preferred language, password=generated).
+
+ remote is a string which describes where this add request came from.
+ """
+ assert self.Locked()
+ # Suck values out of userdesc, apply defaults, and reset the userdesc
+ # attributes (for passing on to ApprovedAddMember()). Lowercase the
+ # addr's domain part.
+ email = Utils.LCDomain(userdesc.address)
+ name = getattr(userdesc, 'fullname', '')
+ lang = getattr(userdesc, 'language', self.preferred_language)
+ digest = getattr(userdesc, 'digest', None)
+ password = getattr(userdesc, 'password', Utils.MakeRandomPassword())
+ if digest is None:
+ if self.nondigestable:
+ digest = 0
+ else:
+ digest = 1
+ # Validate the e-mail address to some degree.
+ Utils.ValidateEmail(email)
+ if self.isMember(email):
+ raise Errors.MMAlreadyAMember, email
+ if email.lower() == self.GetListEmail().lower():
+ # Trying to subscribe the list to itself!
+ raise Errors.InvalidEmailAddress
+ realname = self.real_name
+ # Is the subscribing address banned from this list?
+ pattern = Utils.get_pattern(email, self.ban_list)
+ if pattern:
+ vlog.error('%s banned subscription: %s (matched: %s)',
+ realname, email, pattern)
+ raise Errors.MembershipIsBanned, pattern
+ # Sanity check the digest flag
+ if digest and not self.digestable:
+ raise Errors.MMCantDigestError
+ elif not digest and not self.nondigestable:
+ raise Errors.MMMustDigestError
+
+ userdesc.address = email
+ userdesc.fullname = name
+ userdesc.digest = digest
+ userdesc.language = lang
+ userdesc.password = password
+
+ # Apply the list's subscription policy. 0 means open subscriptions; 1
+ # means the user must confirm; 2 means the admin must approve; 3 means
+ # the user must confirm and then the admin must approve
+ if self.subscribe_policy == 0:
+ self.ApprovedAddMember(userdesc, whence=remote or '')
+ elif self.subscribe_policy == 1 or self.subscribe_policy == 3:
+ # User confirmation required. BAW: this should probably just
+ # accept a userdesc instance.
+ cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc)
+ # Send the user the confirmation mailback
+ if remote is None:
+ by = remote = ''
+ else:
+ by = ' ' + remote
+ remote = _(' from %(remote)s')
+
+ recipient = self.GetMemberAdminEmail(email)
+ confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
+ cookie)
+ text = Utils.maketext(
+ 'verify.txt',
+ {'email' : email,
+ 'listaddr' : self.GetListEmail(),
+ 'listname' : realname,
+ 'cookie' : cookie,
+ 'requestaddr' : self.getListAddress('request'),
+ 'remote' : remote,
+ 'listadmin' : self.GetOwnerEmail(),
+ 'confirmurl' : confirmurl,
+ }, lang=lang, mlist=self)
+ msg = Message.UserNotification(
+ recipient, self.GetRequestEmail(cookie),
+ text=text, lang=lang)
+ # BAW: See ChangeMemberAddress() for why we do it this way...
+ del msg['subject']
+ msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie)
+ msg['Reply-To'] = self.GetRequestEmail(cookie)
+ msg.send(self)
+ who = formataddr((name, email))
+ slog.info('%s: pending %s %s', self.internal_name(), who, by)
+ raise Errors.MMSubscribeNeedsConfirmation
+ elif self.HasAutoApprovedSender(email):
+ # no approval necessary:
+ self.ApprovedAddMember(userdesc)
+ else:
+ # Subscription approval is required. Add this entry to the admin
+ # requests database. BAW: this should probably take a userdesc
+ # just like above.
+ self.HoldSubscription(email, name, password, digest, lang)
+ raise Errors.MMNeedApproval, _(
+ 'subscriptions to %(realname)s require moderator approval')
+
+ def DeleteMember(self, name, whence=None, admin_notif=None, userack=True):
+ realname, email = parseaddr(name)
+ if self.unsubscribe_policy == 0:
+ self.ApprovedDeleteMember(name, whence, admin_notif, userack)
+ else:
+ self.HoldUnsubscription(email)
+ raise Errors.MMNeedApproval, _(
+ 'unsubscriptions require moderator approval')
+
+ def ChangeMemberAddress(self, oldaddr, newaddr, globally):
+ # Changing a member address consists of verifying the new address,
+ # making sure the new address isn't already a member, and optionally
+ # going through the confirmation process.
+ #
+ # Most of these checks are copied from AddMember
+ newaddr = Utils.LCDomain(newaddr)
+ Utils.ValidateEmail(newaddr)
+ # Raise an exception if this email address is already a member of the
+ # list, but only if the new address is the same case-wise as the old
+ # address and we're not doing a global change.
+ if not globally and newaddr == oldaddr and self.isMember(newaddr):
+ raise Errors.MMAlreadyAMember
+ if newaddr == self.GetListEmail().lower():
+ raise Errors.InvalidEmailAddress
+ realname = self.real_name
+ # Don't allow changing to a banned address. MAS: maybe we should
+ # unsubscribe the oldaddr too just for trying, but that's probably
+ # too harsh.
+ pattern = Utils.get_pattern(newaddr, self.ban_list)
+ if pattern:
+ vlog.error('%s banned address change: %s -> %s (matched: %s)',
+ realname, oldaddr, newaddr, pattern)
+ raise Errors.MembershipIsBanned, pattern
+ # Pend the subscription change
+ cookie = self.pend_new(Pending.CHANGE_OF_ADDRESS,
+ oldaddr, newaddr, globally)
+ confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
+ cookie)
+ lang = self.getMemberLanguage(oldaddr)
+ text = Utils.maketext(
+ 'verify.txt',
+ {'email' : newaddr,
+ 'listaddr' : self.GetListEmail(),
+ 'listname' : realname,
+ 'cookie' : cookie,
+ 'requestaddr': self.getListAddress('request'),
+ 'remote' : '',
+ 'listadmin' : self.GetOwnerEmail(),
+ 'confirmurl' : confirmurl,
+ }, lang=lang, mlist=self)
+ # BAW: We don't pass the Subject: into the UserNotification
+ # constructor because it will encode it in the charset of the language
+ # being used. For non-us-ascii charsets, this means it will probably
+ # quopri quote it, and thus replies will also be quopri encoded. But
+ # CommandRunner doesn't yet grok such headers. So, just set the
+ # Subject: in a separate step, although we have to delete the one
+ # UserNotification adds.
+ msg = Message.UserNotification(
+ newaddr, self.GetRequestEmail(cookie),
+ text=text, lang=lang)
+ del msg['subject']
+ msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie)
+ msg['Reply-To'] = self.GetRequestEmail(cookie)
+ msg.send(self)
+
+ def ApprovedChangeMemberAddress(self, oldaddr, newaddr, globally):
+ # Check here for banned address in case address was banned after
+ # confirmation was mailed. MAS: If it's global change should we just
+ # skip this list and proceed to the others? For now we'll throw the
+ # exception.
+ pattern = Utils.get_pattern(newaddr, self.ban_list)
+ if pattern:
+ raise Errors.MembershipIsBanned, pattern
+ # It's possible they were a member of this list, but choose to change
+ # their membership globally. In that case, we simply remove the old
+ # address.
+ if self.getMemberCPAddress(oldaddr) == newaddr:
+ self.removeMember(oldaddr)
+ else:
+ self.changeMemberAddress(oldaddr, newaddr)
+ self.log_and_notify_admin(oldaddr, newaddr)
+ # If globally is true, then we also include every list for which
+ # oldaddr is a member.
+ if not globally:
+ return
+ for listname in config.list_manager.names:
+ # Don't bother with ourselves
+ if listname == self.internal_name():
+ continue
+ mlist = MailList(listname, lock=0)
+ if mlist.host_name <> self.host_name:
+ continue
+ if not mlist.isMember(oldaddr):
+ continue
+ # If new address is banned from this list, just skip it.
+ if Utils.get_pattern(newaddr, mlist.ban_list):
+ continue
+ mlist.Lock()
+ try:
+ # Same logic as above, re newaddr is already a member
+ if mlist.getMemberCPAddress(oldaddr) == newaddr:
+ mlist.removeMember(oldaddr)
+ else:
+ mlist.changeMemberAddress(oldaddr, newaddr)
+ mlist.log_and_notify_admin(oldaddr, newaddr)
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+ def log_and_notify_admin(self, oldaddr, newaddr):
+ """Log member address change and notify admin if requested."""
+ slog.info('%s: changed member address from %s to %s',
+ self.internal_name(), oldaddr, newaddr)
+ if self.admin_notify_mchanges:
+ with i18n.using_language(self.preferred_language):
+ realname = self.real_name
+ subject = _('%(realname)s address change notification')
+ name = self.getMemberName(newaddr)
+ if name is None:
+ name = ''
+ if isinstance(name, unicode):
+ name = name.encode(Utils.GetCharSet(self.preferred_language),
+ 'replace')
+ text = Utils.maketext(
+ 'adminaddrchgack.txt',
+ {'name' : name,
+ 'oldaddr' : oldaddr,
+ 'newaddr' : newaddr,
+ 'listname': self.real_name,
+ }, mlist=self)
+ msg = Message.OwnerNotification(self, subject, text)
+ msg.send(self)
+
+
+ #
+ # Confirmation processing
+ #
+ def ProcessConfirmation(self, cookie, context=None):
+ rec = self.pend_confirm(cookie)
+ if rec is None:
+ raise Errors.MMBadConfirmation, 'No cookie record for %s' % cookie
+ try:
+ op = rec[0]
+ data = rec[1:]
+ except ValueError:
+ raise Errors.MMBadConfirmation, 'op-less data %s' % (rec,)
+ if op == Pending.SUBSCRIPTION:
+ whence = 'via email confirmation'
+ try:
+ userdesc = data[0]
+ # If confirmation comes from the web, context should be a
+ # UserDesc instance which contains overrides of the original
+ # subscription information. If it comes from email, then
+ # context is a Message and isn't relevant, so ignore it.
+ if isinstance(context, UserDesc):
+ userdesc += context
+ whence = 'via web confirmation'
+ addr = userdesc.address
+ fullname = userdesc.fullname
+ password = userdesc.password
+ digest = userdesc.digest
+ lang = userdesc.language
+ except ValueError:
+ raise Errors.MMBadConfirmation, 'bad subscr data %s' % (data,)
+ # Hack alert! Was this a confirmation of an invitation?
+ invitation = getattr(userdesc, 'invitation', False)
+ # We check for both 2 (approval required) and 3 (confirm +
+ # approval) because the policy could have been changed in the
+ # middle of the confirmation dance.
+ if invitation:
+ if invitation <> self.internal_name():
+ # Not cool. The invitee was trying to subscribe to a
+ # different list than they were invited to. Alert both
+ # list administrators.
+ self.SendHostileSubscriptionNotice(invitation, addr)
+ raise Errors.HostileSubscriptionError
+ elif self.subscribe_policy in (2, 3) and \
+ not self.HasAutoApprovedSender(addr):
+ self.HoldSubscription(addr, fullname, password, digest, lang)
+ name = self.real_name
+ raise Errors.MMNeedApproval, _(
+ 'subscriptions to %(name)s require administrator approval')
+ self.ApprovedAddMember(userdesc, whence=whence)
+ return op, addr, password, digest, lang
+ elif op == Pending.UNSUBSCRIPTION:
+ addr = data[0]
+ # Log file messages don't need to be i18n'd
+ if isinstance(context, Message.Message):
+ whence = 'email confirmation'
+ else:
+ whence = 'web confirmation'
+ # Can raise NotAMemberError if they unsub'd via other means
+ self.ApprovedDeleteMember(addr, whence=whence)
+ return op, addr
+ elif op == Pending.CHANGE_OF_ADDRESS:
+ oldaddr, newaddr, globally = data
+ self.ApprovedChangeMemberAddress(oldaddr, newaddr, globally)
+ return op, oldaddr, newaddr
+ elif op == Pending.HELD_MESSAGE:
+ id = data[0]
+ approved = None
+ # Confirmation should be coming from email, where context should
+ # be the confirming message. If the message does not have an
+ # Approved: header, this is a discard. If it has an Approved:
+ # header that does not match the list password, then we'll notify
+ # the list administrator that they used the wrong password.
+ # Otherwise it's an approval.
+ if isinstance(context, Message.Message):
+ # See if it's got an Approved: header, either in the headers,
+ # or in the first text/plain section of the response. For
+ # robustness, we'll accept Approve: as well.
+ approved = context.get('Approved', context.get('Approve'))
+ if not approved:
+ try:
+ subpart = list(email.Iterators.typed_subpart_iterator(
+ context, 'text', 'plain'))[0]
+ except IndexError:
+ subpart = None
+ if subpart:
+ s = StringIO(subpart.get_payload())
+ while True:
+ line = s.readline()
+ if not line:
+ break
+ if not line.strip():
+ continue
+ i = line.find(':')
+ if i > 0:
+ if (line[:i].lower() == 'approve' or
+ line[:i].lower() == 'approved'):
+ # then
+ approved = line[i+1:].strip()
+ break
+ # Is there an approved header?
+ if approved is not None:
+ # Does it match the list password? Note that we purposefully
+ # do not allow the site password here.
+ if self.Authenticate([config.AuthListAdmin,
+ config.AuthListModerator],
+ approved) <> config.UnAuthorized:
+ action = config.APPROVE
+ else:
+ # The password didn't match. Re-pend the message and
+ # inform the list moderators about the problem.
+ self.pend_repend(cookie, rec)
+ raise Errors.MMBadPasswordError
+ else:
+ action = config.DISCARD
+ try:
+ self.HandleRequest(id, action)
+ except KeyError:
+ # Most likely because the message has already been disposed of
+ # via the admindb page.
+ elog.error('Could not process HELD_MESSAGE: %s', id)
+ return (op,)
+ elif op == Pending.RE_ENABLE:
+ member = data[1]
+ self.setDeliveryStatus(member, MemberAdaptor.ENABLED)
+ return op, member
+ else:
+ assert 0, 'Bad op: %s' % op
+
+ def ConfirmUnsubscription(self, addr, lang=None, remote=None):
+ if lang is None:
+ lang = self.getMemberLanguage(addr)
+ cookie = self.pend_new(Pending.UNSUBSCRIPTION, addr)
+ confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
+ cookie)
+ realname = self.real_name
+ if remote is not None:
+ by = " " + remote
+ remote = _(" from %(remote)s")
+ else:
+ by = ""
+ remote = ""
+ text = Utils.maketext(
+ 'unsub.txt',
+ {'email' : addr,
+ 'listaddr' : self.GetListEmail(),
+ 'listname' : realname,
+ 'cookie' : cookie,
+ 'requestaddr' : self.getListAddress('request'),
+ 'remote' : remote,
+ 'listadmin' : self.GetOwnerEmail(),
+ 'confirmurl' : confirmurl,
+ }, lang=lang, mlist=self)
+ msg = Message.UserNotification(
+ addr, self.GetRequestEmail(cookie),
+ text=text, lang=lang)
+ # BAW: See ChangeMemberAddress() for why we do it this way...
+ del msg['subject']
+ msg['Subject'] = self.GetConfirmLeaveSubject(realname, cookie)
+ msg['Reply-To'] = self.GetRequestEmail(cookie)
+ msg.send(self)
+
+
+ #
+ # Miscellaneous stuff
+ #
+
+ def HasAutoApprovedSender(self, sender):
+ """Returns True and logs if sender matches address or pattern
+ in subscribe_auto_approval. Otherwise returns False.
+ """
+ auto_approve = False
+ if Utils.get_pattern(sender, self.subscribe_auto_approval):
+ auto_approve = True
+ vlog.info('%s: auto approved subscribe from %s',
+ self.internal_name(), sender)
+ return auto_approve
+
+
+ #
+ # Multilingual (i18n) support
+ #
+ def set_languages(self, *language_codes):
+ # XXX FIXME not to use a database entity directly.
+ from Mailman.database.model import Language
+ # Don't use the language_codes property because that will add the
+ # default server language. The effect would be that the default
+ # server language would never get added to the list's list of
+ # languages.
+ requested_codes = set(language_codes)
+ enabled_codes = set(config.languages.enabled_codes)
+ self.available_languages = [
+ Language(code) for code in requested_codes & enabled_codes]
+
+ def add_language(self, language_code):
+ self.available_languages.append(Language(language_code))
+
+ @property
+ def language_codes(self):
+ # Callers of this method expect a list of language codes
+ available_codes = set(self.available_languages)
+ enabled_codes = set(config.languages.enabled_codes)
+ codes = available_codes & enabled_codes
+ # If we don't add this, and the site admin has never added any
+ # language support to the list, then the general admin page may have a
+ # blank field where the list owner is supposed to chose the list's
+ # preferred language.
+ if config.DEFAULT_SERVER_LANGUAGE not in codes:
+ codes.add(config.DEFAULT_SERVER_LANGUAGE)
+ return list(codes)
diff --git a/src/mailman/attic/SecurityManager.py b/src/mailman/attic/SecurityManager.py
new file mode 100644
index 000000000..8d4a30592
--- /dev/null
+++ b/src/mailman/attic/SecurityManager.py
@@ -0,0 +1,306 @@
+# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Handle passwords and sanitize approved messages."""
+
+# There are current 5 roles defined in Mailman, as codified in Defaults.py:
+# user, list-creator, list-moderator, list-admin, site-admin.
+#
+# Here's how we do cookie based authentication.
+#
+# Each role (see above) has an associated password, which is currently the
+# only way to authenticate a role (in the future, we'll authenticate a
+# user and assign users to roles).
+#
+# Each cookie has the following ingredients: the authorization context's
+# secret (i.e. the password, and a timestamp. We generate an SHA1 hex
+# digest of these ingredients, which we call the 'mac'. We then marshal
+# up a tuple of the timestamp and the mac, hexlify that and return that as
+# a cookie keyed off the authcontext. Note that authenticating the user
+# also requires the user's email address to be included in the cookie.
+#
+# The verification process is done in CheckCookie() below. It extracts
+# the cookie, unhexlifies and unmarshals the tuple, extracting the
+# timestamp. Using this, and the shared secret, the mac is calculated,
+# and it must match the mac passed in the cookie. If so, they're golden,
+# otherwise, access is denied.
+#
+# It is still possible for an adversary to attempt to brute force crack
+# the password if they obtain the cookie, since they can extract the
+# timestamp and create macs based on password guesses. They never get a
+# cleartext version of the password though, so security rests on the
+# difficulty and expense of retrying the cgi dialog for each attempt. It
+# also relies on the security of SHA1.
+
+import os
+import re
+import sha
+import time
+import urllib
+import Cookie
+import logging
+import marshal
+import binascii
+
+from urlparse import urlparse
+
+from Mailman import Defaults
+from Mailman import Errors
+from Mailman import Utils
+from Mailman import passwords
+from Mailman.configuration import config
+
+log = logging.getLogger('mailman.error')
+dlog = logging.getLogger('mailman.debug')
+
+SLASH = '/'
+
+
+
+class SecurityManager:
+ def AuthContextInfo(self, authcontext, user=None):
+ # authcontext may be one of AuthUser, AuthListModerator,
+ # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator
+ # context.
+ #
+ # user is ignored unless authcontext is AuthUser
+ #
+ # Return the authcontext's secret and cookie key. If the authcontext
+ # doesn't exist, return the tuple (None, None). If authcontext is
+ # AuthUser, but the user isn't a member of this mailing list, a
+ # NotAMemberError will be raised. If the user's secret is None, raise
+ # a MMBadUserError.
+ key = urllib.quote(self.fqdn_listname) + '+'
+ if authcontext == Defaults.AuthUser:
+ if user is None:
+ # A bad system error
+ raise TypeError('No user supplied for AuthUser context')
+ secret = self.getMemberPassword(user)
+ userdata = urllib.quote(Utils.ObscureEmail(user), safe='')
+ key += 'user+%s' % userdata
+ elif authcontext == Defaults.AuthListModerator:
+ secret = self.mod_password
+ key += 'moderator'
+ elif authcontext == Defaults.AuthListAdmin:
+ secret = self.password
+ key += 'admin'
+ # BAW: AuthCreator
+ elif authcontext == Defaults.AuthSiteAdmin:
+ sitepass = Utils.get_global_password()
+ if config.ALLOW_SITE_ADMIN_COOKIES and sitepass:
+ secret = sitepass
+ key = 'site'
+ else:
+ # BAW: this should probably hand out a site password based
+ # cookie, but that makes me a bit nervous, so just treat site
+ # admin as a list admin since there is currently no site
+ # admin-only functionality.
+ secret = self.password
+ key += 'admin'
+ else:
+ return None, None
+ return key, secret
+
+ def Authenticate(self, authcontexts, response, user=None):
+ # Given a list of authentication contexts, check to see if the
+ # response matches one of the passwords. authcontexts must be a
+ # sequence, and if it contains the context AuthUser, then the user
+ # argument must not be None.
+ #
+ # Return the authcontext from the argument sequence that matches the
+ # response, or UnAuthorized.
+ for ac in authcontexts:
+ if ac == Defaults.AuthCreator:
+ ok = Utils.check_global_password(response, siteadmin=False)
+ if ok:
+ return Defaults.AuthCreator
+ elif ac == Defaults.AuthSiteAdmin:
+ ok = Utils.check_global_password(response)
+ if ok:
+ return Defaults.AuthSiteAdmin
+ elif ac == Defaults.AuthListAdmin:
+ # The password for the list admin and list moderator are not
+ # kept as plain text, but instead as an sha hexdigest. The
+ # response being passed in is plain text, so we need to
+ # digestify it first.
+ key, secret = self.AuthContextInfo(ac)
+ if secret is None:
+ continue
+ if passwords.check_response(secret, response):
+ return ac
+ elif ac == Defaults.AuthListModerator:
+ # The list moderator password must be sha'd
+ key, secret = self.AuthContextInfo(ac)
+ if secret and passwords.check_response(secret, response):
+ return ac
+ elif ac == Defaults.AuthUser:
+ if user is not None:
+ try:
+ if self.authenticateMember(user, response):
+ return ac
+ except Errors.NotAMemberError:
+ pass
+ else:
+ # What is this context???
+ log.error('Bad authcontext: %s', ac)
+ raise ValueError('Bad authcontext: %s' % ac)
+ return Defaults.UnAuthorized
+
+ def WebAuthenticate(self, authcontexts, response, user=None):
+ # Given a list of authentication contexts, check to see if the cookie
+ # contains a matching authorization, falling back to checking whether
+ # the response matches one of the passwords. authcontexts must be a
+ # sequence, and if it contains the context AuthUser, then the user
+ # argument should not be None.
+ #
+ # Returns a flag indicating whether authentication succeeded or not.
+ for ac in authcontexts:
+ ok = self.CheckCookie(ac, user)
+ if ok:
+ return True
+ # Check passwords
+ ac = self.Authenticate(authcontexts, response, user)
+ if ac:
+ print self.MakeCookie(ac, user)
+ return True
+ return False
+
+ def _cookie_path(self):
+ script_name = os.environ.get('SCRIPT_NAME', '')
+ return SLASH.join(script_name.split(SLASH)[:-1]) + SLASH
+
+ def MakeCookie(self, authcontext, user=None):
+ key, secret = self.AuthContextInfo(authcontext, user)
+ if key is None or secret is None or not isinstance(secret, basestring):
+ raise ValueError
+ # Timestamp
+ issued = int(time.time())
+ # Get a digest of the secret, plus other information.
+ mac = sha.new(secret + repr(issued)).hexdigest()
+ # Create the cookie object.
+ c = Cookie.SimpleCookie()
+ c[key] = binascii.hexlify(marshal.dumps((issued, mac)))
+ c[key]['path'] = self._cookie_path()
+ # We use session cookies, so don't set 'expires' or 'max-age' keys.
+ # Set the RFC 2109 required header.
+ c[key]['version'] = 1
+ return c
+
+ def ZapCookie(self, authcontext, user=None):
+ # We can throw away the secret.
+ key, secret = self.AuthContextInfo(authcontext, user)
+ # Logout of the session by zapping the cookie. For safety both set
+ # max-age=0 (as per RFC2109) and set the cookie data to the empty
+ # string.
+ c = Cookie.SimpleCookie()
+ c[key] = ''
+ c[key]['path'] = self._cookie_path()
+ c[key]['max-age'] = 0
+ # Don't set expires=0 here otherwise it'll force a persistent cookie
+ c[key]['version'] = 1
+ return c
+
+ def CheckCookie(self, authcontext, user=None):
+ # Two results can occur: we return 1 meaning the cookie authentication
+ # succeeded for the authorization context, we return 0 meaning the
+ # authentication failed.
+ #
+ # Dig out the cookie data, which better be passed on this cgi
+ # environment variable. If there's no cookie data, we reject the
+ # authentication.
+ cookiedata = os.environ.get('HTTP_COOKIE')
+ if not cookiedata:
+ return False
+ # We can't use the Cookie module here because it isn't liberal in what
+ # it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and
+ # you get a CookieError. :(. All we care about is accessing the
+ # cookie data via getitem, so we'll use our own parser, which returns
+ # a dictionary.
+ c = parsecookie(cookiedata)
+ # If the user was not supplied, but the authcontext is AuthUser, we
+ # can try to glean the user address from the cookie key. There may be
+ # more than one matching key (if the user has multiple accounts
+ # subscribed to this list), but any are okay.
+ if authcontext == Defaults.AuthUser:
+ if user:
+ usernames = [user]
+ else:
+ usernames = []
+ prefix = urllib.quote(self.fqdn_listname) + '+user+'
+ for k in c.keys():
+ if k.startswith(prefix):
+ usernames.append(k[len(prefix):])
+ # If any check out, we're golden. Note: '@'s are no longer legal
+ # values in cookie keys.
+ for user in [Utils.UnobscureEmail(u) for u in usernames]:
+ ok = self.__checkone(c, authcontext, user)
+ if ok:
+ return True
+ return False
+ else:
+ return self.__checkone(c, authcontext, user)
+
+ def __checkone(self, c, authcontext, user):
+ # Do the guts of the cookie check, for one authcontext/user
+ # combination.
+ try:
+ key, secret = self.AuthContextInfo(authcontext, user)
+ except Errors.NotAMemberError:
+ return False
+ if key not in c or not isinstance(secret, basestring):
+ return False
+ # Undo the encoding we performed in MakeCookie() above. BAW: I
+ # believe this is safe from exploit because marshal can't be forced to
+ # load recursive data structures, and it can't be forced to execute
+ # any unexpected code. The worst that can happen is that either the
+ # client will have provided us bogus data, in which case we'll get one
+ # of the caught exceptions, or marshal format will have changed, in
+ # which case, the cookie decoding will fail. In either case, we'll
+ # simply request reauthorization, resulting in a new cookie being
+ # returned to the client.
+ try:
+ data = marshal.loads(binascii.unhexlify(c[key]))
+ issued, received_mac = data
+ except (EOFError, ValueError, TypeError, KeyError):
+ return False
+ # Make sure the issued timestamp makes sense
+ now = time.time()
+ if now < issued:
+ return False
+ # Calculate what the mac ought to be based on the cookie's timestamp
+ # and the shared secret.
+ mac = sha.new(secret + repr(issued)).hexdigest()
+ if mac <> received_mac:
+ return False
+ # Authenticated!
+ return True
+
+
+
+splitter = re.compile(';\s*')
+
+def parsecookie(s):
+ c = {}
+ for line in s.splitlines():
+ for p in splitter.split(line):
+ try:
+ k, v = p.split('=', 1)
+ except ValueError:
+ pass
+ else:
+ c[k] = v
+ return c
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()