summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/attic/Bouncer.py250
-rw-r--r--src/attic/Defaults.py1324
-rw-r--r--src/attic/Deliverer.py174
-rw-r--r--src/attic/Digester.py57
-rw-r--r--src/attic/MailList.py731
-rw-r--r--src/attic/Mailbox.py106
-rw-r--r--src/attic/SecurityManager.py306
-rw-r--r--src/attic/add_members.py186
-rwxr-xr-xsrc/attic/bin/clone_member219
-rw-r--r--src/attic/bin/discard120
-rw-r--r--src/attic/bin/fix_url.py93
-rw-r--r--src/attic/bin/list_admins101
-rw-r--r--src/attic/bin/msgfmt.py203
-rw-r--r--src/attic/bin/po2templ.py90
-rw-r--r--src/attic/bin/pygettext.py545
-rwxr-xr-xsrc/attic/bin/remove_members186
-rw-r--r--src/attic/bin/reset_pw.py83
-rwxr-xr-xsrc/attic/bin/sync_members286
-rw-r--r--src/attic/bin/templ2pot.py120
-rwxr-xr-xsrc/attic/bin/transcheck412
-rw-r--r--src/attic/cmd_confirm.py98
-rw-r--r--src/attic/cmd_help.py93
-rw-r--r--src/attic/cmd_info.py50
-rw-r--r--src/attic/cmd_leave.py21
-rw-r--r--src/attic/cmd_lists.py65
-rw-r--r--src/attic/cmd_password.py123
-rw-r--r--src/attic/cmd_remove.py21
-rw-r--r--src/attic/cmd_set.py360
-rw-r--r--src/attic/cmd_unsubscribe.py88
-rw-r--r--src/attic/cmd_who.py152
-rw-r--r--src/attic/digests.py426
-rw-r--r--src/attic/docs/OLD-NEWS.txt2835
-rw-r--r--src/attic/docs/man/add_members.160
-rw-r--r--src/attic/docs/man/check_db.160
-rw-r--r--src/attic/docs/man/check_perms.146
-rw-r--r--src/attic/docs/man/clone_member.171
-rw-r--r--src/attic/docs/man/find_member.164
-rw-r--r--src/attic/docs/man/list_members.178
-rw-r--r--src/attic/docs/man/remove_members.163
-rw-r--r--src/attic/docs/man/sync_members.181
-rw-r--r--src/attic/docs/man/transcheck.141
-rw-r--r--src/attic/docs/posting-flow-chart.ps735
-rw-r--r--src/web/Cgi/Auth.py60
-rw-r--r--src/web/Cgi/__init__.py0
-rw-r--r--src/web/Cgi/admin.py1433
-rw-r--r--src/web/Cgi/admindb.py813
-rw-r--r--src/web/Cgi/confirm.py834
-rw-r--r--src/web/Cgi/create.py400
-rw-r--r--src/web/Cgi/edithtml.py175
-rw-r--r--src/web/Cgi/listinfo.py207
-rw-r--r--src/web/Cgi/options.py1000
-rw-r--r--src/web/Cgi/private.py190
-rw-r--r--src/web/Cgi/rmlist.py243
-rw-r--r--src/web/Cgi/roster.py130
-rw-r--r--src/web/Cgi/subscribe.py252
-rw-r--r--src/web/Cgi/wsgi_app.py286
-rw-r--r--src/web/Gui/Archive.py45
-rw-r--r--src/web/Gui/Autoresponse.py99
-rw-r--r--src/web/Gui/Bounce.py195
-rw-r--r--src/web/Gui/ContentFilter.py199
-rw-r--r--src/web/Gui/Digest.py161
-rw-r--r--src/web/Gui/GUIBase.py209
-rw-r--r--src/web/Gui/General.py464
-rw-r--r--src/web/Gui/Language.py128
-rw-r--r--src/web/Gui/Membership.py34
-rw-r--r--src/web/Gui/NonDigest.py158
-rw-r--r--src/web/Gui/Passwords.py31
-rw-r--r--src/web/Gui/Privacy.py537
-rw-r--r--src/web/Gui/Topics.py162
-rw-r--r--src/web/Gui/Usenet.py140
-rw-r--r--src/web/Gui/__init__.py33
-rw-r--r--src/web/HTMLFormatter.py437
-rw-r--r--src/web/__init__.py0
-rw-r--r--src/web/htmlformat.py670
74 files changed, 0 insertions, 20948 deletions
diff --git a/src/attic/Bouncer.py b/src/attic/Bouncer.py
deleted file mode 100644
index e2de3c915..000000000
--- a/src/attic/Bouncer.py
+++ /dev/null
@@ -1,250 +0,0 @@
-# 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/attic/Defaults.py b/src/attic/Defaults.py
deleted file mode 100644
index 6f72ed535..000000000
--- a/src/attic/Defaults.py
+++ /dev/null
@@ -1,1324 +0,0 @@
-# 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/attic/Deliverer.py b/src/attic/Deliverer.py
deleted file mode 100644
index 0ba3a01bb..000000000
--- a/src/attic/Deliverer.py
+++ /dev/null
@@ -1,174 +0,0 @@
-# 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/attic/Digester.py b/src/attic/Digester.py
deleted file mode 100644
index a88d08abc..000000000
--- a/src/attic/Digester.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# 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/attic/MailList.py b/src/attic/MailList.py
deleted file mode 100644
index 2d538f026..000000000
--- a/src/attic/MailList.py
+++ /dev/null
@@ -1,731 +0,0 @@
-# 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/attic/Mailbox.py b/src/attic/Mailbox.py
deleted file mode 100644
index 3a2f079c4..000000000
--- a/src/attic/Mailbox.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# 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/>.
-
-"""Extend mailbox.UnixMailbox.
-"""
-
-import sys
-import email
-import mailbox
-
-from email.errors import MessageParseError
-from email.generator import Generator
-
-from mailman.Message import Message
-from mailman.config import config
-
-
-
-def _safeparser(fp):
- try:
- return email.message_from_file(fp, Message)
- except MessageParseError:
- # Don't return None since that will stop a mailbox iterator
- return ''
-
-
-
-class Mailbox(mailbox.PortableUnixMailbox):
- def __init__(self, fp):
- mailbox.PortableUnixMailbox.__init__(self, fp, _safeparser)
-
- # msg should be an rfc822 message or a subclass.
- def AppendMessage(self, msg):
- # Check the last character of the file and write a newline if it isn't
- # a newline (but not at the beginning of an empty file).
- try:
- self.fp.seek(-1, 2)
- except IOError, e:
- # Assume the file is empty. We can't portably test the error code
- # returned, since it differs per platform.
- pass
- else:
- if self.fp.read(1) <> '\n':
- self.fp.write('\n')
- # Seek to the last char of the mailbox
- self.fp.seek(1, 2)
- # Create a Generator instance to write the message to the file
- g = Generator(self.fp)
- g.flatten(msg, unixfrom=True)
- # Add one more trailing newline for separation with the next message
- # to be appended to the mbox.
- print >> self.fp
-
-
-
-# This stuff is used by pipermail.py:processUnixMailbox(). It provides an
-# opportunity for the built-in archiver to scrub archived messages of nasty
-# things like attachments and such...
-def _archfactory(mailbox):
- # The factory gets a file object, but it also needs to have a MailList
- # object, so the clearest <wink> way to do this is to build a factory
- # function that has a reference to the mailbox object, which in turn holds
- # a reference to the mailing list. Nested scopes would help here, BTW,
- # but we can't rely on them being around (e.g. Python 2.0).
- def scrubber(fp, mailbox=mailbox):
- msg = _safeparser(fp)
- if msg == '':
- return msg
- return mailbox.scrub(msg)
- return scrubber
-
-
-class ArchiverMailbox(Mailbox):
- # This is a derived class which is instantiated with a reference to the
- # MailList object. It is build such that the factory calls back into its
- # scrub() method, giving the scrubber module a chance to do its thing
- # before the message is archived.
- def __init__(self, fp, mlist):
- scrubber_module = config.scrubber.archive_scrubber
- if scrubber_module:
- __import__(scrubber_module)
- self._scrubber = sys.modules[scrubber_module].process
- else:
- self._scrubber = None
- self._mlist = mlist
- mailbox.PortableUnixMailbox.__init__(self, fp, _archfactory(self))
-
- def scrub(self, msg):
- if self._scrubber:
- return self._scrubber(self._mlist, msg)
- else:
- return msg
diff --git a/src/attic/SecurityManager.py b/src/attic/SecurityManager.py
deleted file mode 100644
index 8d4a30592..000000000
--- a/src/attic/SecurityManager.py
+++ /dev/null
@@ -1,306 +0,0 @@
-# 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/attic/add_members.py b/src/attic/add_members.py
deleted file mode 100644
index 540c0facb..000000000
--- a/src/attic/add_members.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import sys
-import codecs
-
-from cStringIO import StringIO
-from email.utils import parseaddr
-
-from mailman import Utils
-from mailman import i18n
-from mailman.app.membership import add_member
-from mailman.config import config
-from mailman.core import errors
-from mailman.email.message import UserNotification
-from mailman.interfaces.member import AlreadySubscribedError, DeliveryMode
-from mailman.options import SingleMailingListOptions
-
-_ = i18n._
-
-
-
-class ScriptOptions(SingleMailingListOptions):
- usage=_("""\
-%prog [options]
-
-Add members to a list. 'listname' is the name of the Mailman list you are
-adding members to; the list must already exist.
-
-You must supply at least one of -r and -d options. At most one of the
-files can be '-'.
-""")
-
- def add_options(self):
- super(ScriptOptions, self).add_options()
- self.parser.add_option(
- '-r', '--regular-members-file',
- type='string', dest='regular', help=_("""\
-A file containing addresses of the members to be added, one address per line.
-This list of people become non-digest members. If file is '-', read addresses
-from stdin."""))
- self.parser.add_option(
- '-d', '--digest-members-file',
- type='string', dest='digest', help=_("""\
-Similar to -r, but these people become digest members."""))
- self.parser.add_option(
- '-w', '--welcome-msg',
- type='yesno', metavar='<y|n>', help=_("""\
-Set whether or not to send the list members a welcome message, overriding
-whatever the list's 'send_welcome_msg' setting is."""))
- self.parser.add_option(
- '-a', '--admin-notify',
- type='yesno', metavar='<y|n>', help=_("""\
-Set whether or not to send the list administrators a notification on the
-success/failure of these subscriptions, overriding whatever the list's
-'admin_notify_mchanges' setting is."""))
-
- def sanity_check(self):
- if not self.options.listname:
- self.parser.error(_('Missing listname'))
- if len(self.arguments) > 0:
- self.parser.print_error(_('Unexpected arguments'))
- if self.options.regular is None and self.options.digest is None:
- parser.error(_('At least one of -r or -d is required'))
- if self.options.regular == '-' and self.options.digest == '-':
- parser.error(_("-r and -d cannot both be '-'"))
-
-
-
-def readfile(filename):
- if filename == '-':
- fp = sys.stdin
- else:
- # XXX Need to specify other encodings.
- fp = codecs.open(filename, encoding='utf-8')
- # Strip all the lines of whitespace and discard blank lines
- try:
- return set(line.strip() for line in fp if line)
- finally:
- if fp is not sys.stdin:
- fp.close()
-
-
-
-class Tee:
- def __init__(self, outfp):
- self._outfp = outfp
-
- def write(self, msg):
- sys.stdout.write(msg)
- self._outfp.write(msg)
-
-
-
-def addall(mlist, subscribers, delivery_mode, ack, admin_notify, outfp):
- tee = Tee(outfp)
- for subscriber in subscribers:
- try:
- fullname, address = parseaddr(subscriber)
- # Watch out for the empty 8-bit string.
- if not fullname:
- fullname = u''
- password = Utils.MakeRandomPassword()
- add_member(mlist, address, fullname, password, delivery_mode,
- unicode(config.mailman.default_language))
- # XXX Support ack and admin_notify
- except AlreadySubscribedError:
- print >> tee, _('Already a member: $subscriber')
- except errors.InvalidEmailAddress:
- if not address:
- print >> tee, _('Bad/Invalid email address: blank line')
- else:
- print >> tee, _('Bad/Invalid email address: $subscriber')
- else:
- print >> tee, _('Subscribing: $subscriber')
-
-
-
-def main():
- options = ScriptOptions()
- options.initialize()
-
- fqdn_listname = options.options.listname
- mlist = config.db.list_manager.get(fqdn_listname)
- if mlist is None:
- parser.error(_('No such list: $fqdn_listname'))
-
- # Set up defaults.
- send_welcome_msg = (options.options.welcome_msg
- if options.options.welcome_msg is not None
- else mlist.send_welcome_msg)
- admin_notify = (options.options.admin_notify
- if options.options.admin_notify is not None
- else mlist.admin_notify)
-
- with i18n.using_language(mlist.preferred_language):
- if options.options.digest:
- dmembers = readfile(options.options.digest)
- else:
- dmembers = set()
- if options.options.regular:
- nmembers = readfile(options.options.regular)
- else:
- nmembers = set()
-
- if not dmembers and not nmembers:
- print _('Nothing to do.')
- sys.exit(0)
-
- outfp = StringIO()
- if nmembers:
- addall(mlist, nmembers, DeliveryMode.regular,
- send_welcome_msg, admin_notify, outfp)
-
- if dmembers:
- addall(mlist, dmembers, DeliveryMode.mime_digests,
- send_welcome_msg, admin_notify, outfp)
-
- config.db.commit()
-
- if admin_notify:
- subject = _('$mlist.real_name subscription notification')
- msg = UserNotification(
- mlist.owner, mlist.no_reply_address, subject,
- outfp.getvalue(), mlist.preferred_language)
- msg.send(mlist)
-
-
-
-if __name__ == '__main__':
- main()
diff --git a/src/attic/bin/clone_member b/src/attic/bin/clone_member
deleted file mode 100755
index 1f2a03aca..000000000
--- a/src/attic/bin/clone_member
+++ /dev/null
@@ -1,219 +0,0 @@
-#! @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/attic/bin/discard b/src/attic/bin/discard
deleted file mode 100644
index c30198441..000000000
--- a/src/attic/bin/discard
+++ /dev/null
@@ -1,120 +0,0 @@
-#! @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/attic/bin/fix_url.py b/src/attic/bin/fix_url.py
deleted file mode 100644
index 30618a1a3..000000000
--- a/src/attic/bin/fix_url.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#! @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/attic/bin/list_admins b/src/attic/bin/list_admins
deleted file mode 100644
index c628a42dc..000000000
--- a/src/attic/bin/list_admins
+++ /dev/null
@@ -1,101 +0,0 @@
-#! @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/attic/bin/msgfmt.py b/src/attic/bin/msgfmt.py
deleted file mode 100644
index 8a2d4e66e..000000000
--- a/src/attic/bin/msgfmt.py
+++ /dev/null
@@ -1,203 +0,0 @@
-#! /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/attic/bin/po2templ.py b/src/attic/bin/po2templ.py
deleted file mode 100644
index 86eae96b9..000000000
--- a/src/attic/bin/po2templ.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#! @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/attic/bin/pygettext.py b/src/attic/bin/pygettext.py
deleted file mode 100644
index 84421ee8c..000000000
--- a/src/attic/bin/pygettext.py
+++ /dev/null
@@ -1,545 +0,0 @@
-#! @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/attic/bin/remove_members b/src/attic/bin/remove_members
deleted file mode 100755
index a7b4ebb47..000000000
--- a/src/attic/bin/remove_members
+++ /dev/null
@@ -1,186 +0,0 @@
-#! @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/attic/bin/reset_pw.py b/src/attic/bin/reset_pw.py
deleted file mode 100644
index 453c8b849..000000000
--- a/src/attic/bin/reset_pw.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#! @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/attic/bin/sync_members b/src/attic/bin/sync_members
deleted file mode 100755
index 4a21624c1..000000000
--- a/src/attic/bin/sync_members
+++ /dev/null
@@ -1,286 +0,0 @@
-#! @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/attic/bin/templ2pot.py b/src/attic/bin/templ2pot.py
deleted file mode 100644
index 0253cc2cd..000000000
--- a/src/attic/bin/templ2pot.py
+++ /dev/null
@@ -1,120 +0,0 @@
-#! @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/attic/bin/transcheck b/src/attic/bin/transcheck
deleted file mode 100755
index 73910e771..000000000
--- a/src/attic/bin/transcheck
+++ /dev/null
@@ -1,412 +0,0 @@
-#! @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()
diff --git a/src/attic/cmd_confirm.py b/src/attic/cmd_confirm.py
deleted file mode 100644
index 0e2b7ad43..000000000
--- a/src/attic/cmd_confirm.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""
- confirm <confirmation-string>
- Confirm an action. The confirmation-string is required and should be
- supplied by a mailback confirmation notice.
-"""
-
-from mailman import errors
-from mailman import Pending
-from mailman.config import config
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- if len(args) <> 1:
- res.results.append(_('Usage:'))
- res.results.append(gethelp(mlist))
- return STOP
- cookie = args[0]
- try:
- results = mlist.ProcessConfirmation(cookie, res.msg)
- except Errors.MMBadConfirmation, e:
- # Express in approximate days
- days = int(config.PENDING_REQUEST_LIFE / config.days(1) + 0.5)
- res.results.append(_("""\
-Invalid confirmation string. Note that confirmation strings expire
-approximately %(days)s days after the initial subscription request. If your
-confirmation has expired, please try to re-submit your original request or
-message."""))
- except Errors.MMNeedApproval:
- res.results.append(_("""\
-Your request has been forwarded to the list moderator for approval."""))
- except Errors.MMAlreadyAMember:
- # Some other subscription request for this address has
- # already succeeded.
- res.results.append(_('You are already subscribed.'))
- except Errors.NotAMemberError:
- # They've already been unsubscribed
- res.results.append(_("""\
-You are not currently a member. Have you already unsubscribed or changed
-your email address?"""))
- except Errors.MembershipIsBanned:
- owneraddr = mlist.GetOwnerEmail()
- res.results.append(_("""\
-You are currently banned from subscribing to this list. If you think this
-restriction is erroneous, please contact the list owners at
-%(owneraddr)s."""))
- except Errors.HostileSubscriptionError:
- res.results.append(_("""\
-You were not invited to this mailing list. The invitation has been discarded,
-and both list administrators have been alerted."""))
- except Errors.MMBadPasswordError:
- res.results.append(_("""\
-Bad approval password given. Held message is still being held."""))
- else:
- if ((results[0] == Pending.SUBSCRIPTION and mlist.send_welcome_msg)
- or
- (results[0] == Pending.UNSUBSCRIPTION and mlist.send_goodbye_msg)):
- # We don't also need to send a confirmation succeeded message
- res.respond = 0
- else:
- res.results.append(_('Confirmation succeeded'))
- # Consume any other confirmation strings with the same cookie so
- # the user doesn't get a misleading "unprocessed" message.
- match = 'confirm ' + cookie
- unprocessed = []
- for line in res.commands:
- if line.lstrip() == match:
- continue
- unprocessed.append(line)
- res.commands = unprocessed
- # Process just one confirmation string per message
- return STOP
diff --git a/src/attic/cmd_help.py b/src/attic/cmd_help.py
deleted file mode 100644
index 30c8dc4d6..000000000
--- a/src/attic/cmd_help.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""
- help
- Print this help message.
-"""
-
-import os
-import sys
-
-from mailman import Utils
-from mailman.config import config
-from mailman.i18n import _
-
-EMPTYSTRING = ''
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- # Get the help text introduction
- mlist = res.mlist
- # Since this message is personalized, add some useful information if the
- # address requesting help is a member of the list.
- msg = res.msg
- for sender in msg.senders:
- if mlist.isMember(sender):
- memberurl = mlist.GetOptionsURL(sender, absolute=1)
- urlhelp = _(
- 'You can access your personal options via the following url:')
- res.results.append(urlhelp)
- res.results.append(memberurl)
- # Get a blank line in the output.
- res.results.append('')
- break
- # build the specific command helps from the module docstrings
- modhelps = {}
- import mailman.Commands
- path = os.path.dirname(os.path.abspath(mailman.Commands.__file__))
- for file in os.listdir(path):
- if not file.startswith('cmd_') or not file.endswith('.py'):
- continue
- module = os.path.splitext(file)[0]
- modname = 'mailman.Commands.' + module
- try:
- __import__(modname)
- except ImportError:
- continue
- cmdname = module[4:]
- help = None
- if hasattr(sys.modules[modname], 'gethelp'):
- help = sys.modules[modname].gethelp(mlist)
- if help:
- modhelps[cmdname] = help
- # Now sort the command helps
- helptext = []
- keys = modhelps.keys()
- keys.sort()
- for cmd in keys:
- helptext.append(modhelps[cmd])
- commands = EMPTYSTRING.join(helptext)
- # Now craft the response
- helptext = Utils.maketext(
- 'help.txt',
- {'listname' : mlist.real_name,
- 'version' : config.VERSION,
- 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1),
- 'requestaddr' : mlist.GetRequestEmail(),
- 'adminaddr' : mlist.GetOwnerEmail(),
- 'commands' : commands,
- }, mlist=mlist, lang=res.msgdata['lang'], raw=1)
- # Now add to the response
- res.results.append('help')
- res.results.append(helptext)
diff --git a/src/attic/cmd_info.py b/src/attic/cmd_info.py
deleted file mode 100644
index 3bdea178f..000000000
--- a/src/attic/cmd_info.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""
- info
- Get information about this mailing list.
-"""
-
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- if args:
- res.results.append(gethelp(mlist))
- return STOP
- listname = mlist.real_name
- description = mlist.description or _('n/a')
- postaddr = mlist.posting_address
- requestaddr = mlist.request_address
- owneraddr = mlist.owner_address
- listurl = mlist.script_url('listinfo')
- res.results.append(_('List name: %(listname)s'))
- res.results.append(_('Description: %(description)s'))
- res.results.append(_('Postings to: %(postaddr)s'))
- res.results.append(_('List Helpbot: %(requestaddr)s'))
- res.results.append(_('List Owners: %(owneraddr)s'))
- res.results.append(_('More information: %(listurl)s'))
diff --git a/src/attic/cmd_leave.py b/src/attic/cmd_leave.py
deleted file mode 100644
index 5844824f7..000000000
--- a/src/attic/cmd_leave.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""The `leave' command is synonymous with `unsubscribe'.
-"""
-
-from mailman.Commands.cmd_unsubscribe import process
diff --git a/src/attic/cmd_lists.py b/src/attic/cmd_lists.py
deleted file mode 100644
index 234ef46fc..000000000
--- a/src/attic/cmd_lists.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""
- lists
- See a list of the public mailing lists on this GNU Mailman server.
-"""
-
-from mailman.MailList import MailList
-from mailman.config import config
-from mailman.i18n import _
-
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- if args:
- res.results.append(_('Usage:'))
- res.results.append(gethelp(mlist))
- return STOP
- hostname = mlist.host_name
- res.results.append(_('Public mailing lists at %(hostname)s:'))
- i = 1
- for listname in sorted(config.list_manager.names):
- if listname == mlist.internal_name():
- xlist = mlist
- else:
- xlist = MailList(listname, lock=0)
- # We can mention this list if you already know about it
- if not xlist.advertised and xlist is not mlist:
- continue
- # Skip the list if it isn't in the same virtual domain.
- if xlist.host_name <> mlist.host_name:
- continue
- realname = xlist.real_name
- description = xlist.description or _('n/a')
- requestaddr = xlist.GetRequestEmail()
- if i > 1:
- res.results.append('')
- res.results.append(_('%(i)3d. List name: %(realname)s'))
- res.results.append(_(' Description: %(description)s'))
- res.results.append(_(' Requests to: %(requestaddr)s'))
- i += 1
diff --git a/src/attic/cmd_password.py b/src/attic/cmd_password.py
deleted file mode 100644
index 545da0cb5..000000000
--- a/src/attic/cmd_password.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""
- password [<oldpassword> <newpassword>] [address=<address>]
- Retrieve or change your password. With no arguments, this returns
- your current password. With arguments <oldpassword> and <newpassword>
- you can change your password.
-
- If you're posting from an address other than your membership address,
- specify your membership address with `address=<address>' (no brackets
- around the email address, and no quotes!). Note that in this case the
- response is always sent to the subscribed address.
-"""
-
-from email.Utils import parseaddr
-
-from mailman.config import config
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- address = None
- if not args:
- # They just want to get their existing password
- realname, address = parseaddr(res.msg['from'])
- if mlist.isMember(address):
- password = mlist.getMemberPassword(address)
- res.results.append(_('Your password is: %(password)s'))
- # Prohibit multiple password retrievals.
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- elif len(args) == 1 and args[0].startswith('address='):
- # They want their password, but they're posting from a different
- # address. We /must/ return the password to the subscribed address.
- address = args[0][8:]
- res.returnaddr = address
- if mlist.isMember(address):
- password = mlist.getMemberPassword(address)
- res.results.append(_('Your password is: %(password)s'))
- # Prohibit multiple password retrievals.
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- elif len(args) == 2:
- # They are changing their password
- oldpasswd = args[0]
- newpasswd = args[1]
- realname, address = parseaddr(res.msg['from'])
- if mlist.isMember(address):
- if mlist.Authenticate((config.AuthUser, config.AuthListAdmin),
- oldpasswd, address):
- mlist.setMemberPassword(address, newpasswd)
- res.results.append(_('Password successfully changed.'))
- else:
- res.results.append(_("""\
-You did not give the correct old password, so your password has not been
-changed. Use the no argument version of the password command to retrieve your
-current password, then try again."""))
- res.results.append(_('\nUsage:'))
- res.results.append(gethelp(mlist))
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- elif len(args) == 3 and args[2].startswith('address='):
- # They want to change their password, and they're sending this from a
- # different address than what they're subscribed with. Be sure the
- # response goes to the subscribed address.
- oldpasswd = args[0]
- newpasswd = args[1]
- address = args[2][8:]
- res.returnaddr = address
- if mlist.isMember(address):
- if mlist.Authenticate((config.AuthUser, config.AuthListAdmin),
- oldpasswd, address):
- mlist.setMemberPassword(address, newpasswd)
- res.results.append(_('Password successfully changed.'))
- else:
- res.results.append(_("""\
-You did not give the correct old password, so your password has not been
-changed. Use the no argument version of the password command to retrieve your
-current password, then try again."""))
- res.results.append(_('\nUsage:'))
- res.results.append(gethelp(mlist))
- return STOP
- else:
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
diff --git a/src/attic/cmd_remove.py b/src/attic/cmd_remove.py
deleted file mode 100644
index 8f3ce9669..000000000
--- a/src/attic/cmd_remove.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""The `remove' command is synonymous with `unsubscribe'.
-"""
-
-from mailman.Commands.cmd_unsubscribe import process
diff --git a/src/attic/cmd_set.py b/src/attic/cmd_set.py
deleted file mode 100644
index 020bc3636..000000000
--- a/src/attic/cmd_set.py
+++ /dev/null
@@ -1,360 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-from email.Utils import parseaddr, formatdate
-
-from mailman import Errors
-from mailman import MemberAdaptor
-from mailman import i18n
-from mailman.config import config
-
-def _(s): return s
-
-OVERVIEW = _("""
- set ...
- Set or view your membership options.
-
- Use `set help' (without the quotes) to get a more detailed list of the
- options you can change.
-
- Use `set show' (without the quotes) to view your current option
- settings.
-""")
-
-DETAILS = _("""
- set help
- Show this detailed help.
-
- set show [address=<address>]
- View your current option settings. If you're posting from an address
- other than your membership address, specify your membership address
- with `address=<address>' (no brackets around the email address, and no
- quotes!).
-
- set authenticate <password> [address=<address>]
- To set any of your options, you must include this command first, along
- with your membership password. If you're posting from an address
- other than your membership address, specify your membership address
- with `address=<address>' (no brackets around the email address, and no
- quotes!).
-
- set ack on
- set ack off
- When the `ack' option is turned on, you will receive an
- acknowledgement message whenever you post a message to the list.
-
- set digest plain
- set digest mime
- set digest off
- When the `digest' option is turned off, you will receive postings
- immediately when they are posted. Use `set digest plain' if instead
- you want to receive postings bundled into a plain text digest
- (i.e. RFC 1153 digest). Use `set digest mime' if instead you want to
- receive postings bundled together into a MIME digest.
-
- set delivery on
- set delivery off
- Turn delivery on or off. This does not unsubscribe you, but instead
- tells Mailman not to deliver messages to you for now. This is useful
- if you're going on vacation. Be sure to use `set delivery on' when
- you return from vacation!
-
- set myposts on
- set myposts off
- Use `set myposts off' to not receive copies of messages you post to
- the list. This has no effect if you're receiving digests.
-
- set hide on
- set hide off
- Use `set hide on' to conceal your email address when people request
- the membership list.
-
- set duplicates on
- set duplicates off
- Use `set duplicates off' if you want Mailman to not send you messages
- if your address is explicitly mentioned in the To: or Cc: fields of
- the message. This can reduce the number of duplicate postings you
- will receive.
-
- set reminders on
- set reminders off
- Use `set reminders off' if you want to disable the monthly password
- reminder for this mailing list.
-""")
-
-_ = i18n._
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(OVERVIEW)
-
-
-
-class SetCommands:
- def __init__(self):
- self.__address = None
- self.__authok = 0
-
- def process(self, res, args):
- if not args:
- res.results.append(_(DETAILS))
- return STOP
- subcmd = args.pop(0)
- methname = 'set_' + subcmd
- method = getattr(self, methname, None)
- if method is None:
- res.results.append(_('Bad set command: %(subcmd)s'))
- res.results.append(_(DETAILS))
- return STOP
- return method(res, args)
-
- def set_help(self, res, args=1):
- res.results.append(_(DETAILS))
- if args:
- return STOP
-
- def _usage(self, res):
- res.results.append(_('Usage:'))
- return self.set_help(res)
-
- def set_show(self, res, args):
- mlist = res.mlist
- if not args:
- realname, address = parseaddr(res.msg['from'])
- elif len(args) == 1 and args[0].startswith('address='):
- # Send the results to the address, not the From: dude
- address = args[0][8:]
- res.returnaddr = address
- else:
- return self._usage(res)
- if not mlist.isMember(address):
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- res.results.append(_('Your current option settings:'))
- opt = mlist.getMemberOption(address, config.AcknowledgePosts)
- onoff = opt and _('on') or _('off')
- res.results.append(_(' ack %(onoff)s'))
- # Digests are a special ternary value
- digestsp = mlist.getMemberOption(address, config.Digests)
- if digestsp:
- plainp = mlist.getMemberOption(address, config.DisableMime)
- if plainp:
- res.results.append(_(' digest plain'))
- else:
- res.results.append(_(' digest mime'))
- else:
- res.results.append(_(' digest off'))
- # If their membership is disabled, let them know why
- status = mlist.getDeliveryStatus(address)
- how = None
- if status == MemberAdaptor.ENABLED:
- status = _('delivery on')
- elif status == MemberAdaptor.BYUSER:
- status = _('delivery off')
- how = _('by you')
- elif status == MemberAdaptor.BYADMIN:
- status = _('delivery off')
- how = _('by the admin')
- elif status == MemberAdaptor.BYBOUNCE:
- status = _('delivery off')
- how = _('due to bounces')
- else:
- assert status == MemberAdaptor.UNKNOWN
- status = _('delivery off')
- how = _('for unknown reasons')
- changetime = mlist.getDeliveryStatusChangeTime(address)
- if how and changetime > 0:
- date = formatdate(changetime)
- res.results.append(_(' %(status)s (%(how)s on %(date)s)'))
- else:
- res.results.append(' ' + status)
- opt = mlist.getMemberOption(address, config.DontReceiveOwnPosts)
- # sense is reversed
- onoff = (not opt) and _('on') or _('off')
- res.results.append(_(' myposts %(onoff)s'))
- opt = mlist.getMemberOption(address, config.ConcealSubscription)
- onoff = opt and _('on') or _('off')
- res.results.append(_(' hide %(onoff)s'))
- opt = mlist.getMemberOption(address, config.DontReceiveDuplicates)
- # sense is reversed
- onoff = (not opt) and _('on') or _('off')
- res.results.append(_(' duplicates %(onoff)s'))
- opt = mlist.getMemberOption(address, config.SuppressPasswordReminder)
- # sense is reversed
- onoff = (not opt) and _('on') or _('off')
- res.results.append(_(' reminders %(onoff)s'))
-
- def set_authenticate(self, res, args):
- mlist = res.mlist
- if len(args) == 1:
- realname, address = parseaddr(res.msg['from'])
- password = args[0]
- elif len(args) == 2 and args[1].startswith('address='):
- password = args[0]
- address = args[1][8:]
- else:
- return self._usage(res)
- # See if the password matches
- if not mlist.isMember(address):
- listname = mlist.real_name
- res.results.append(
- _('You are not a member of the %(listname)s mailing list'))
- return STOP
- if not mlist.Authenticate((config.AuthUser,
- config.AuthListAdmin),
- password, address):
- res.results.append(_('You did not give the correct password'))
- return STOP
- self.__authok = 1
- self.__address = address
-
- def _status(self, res, arg):
- status = arg.lower()
- if status == 'on':
- flag = 1
- elif status == 'off':
- flag = 0
- else:
- res.results.append(_('Bad argument: %(arg)s'))
- self._usage(res)
- return -1
- # See if we're authenticated
- if not self.__authok:
- res.results.append(_('Not authenticated'))
- self._usage(res)
- return -1
- return flag
-
- def set_ack(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- mlist.setMemberOption(self.__address, config.AcknowledgePosts, status)
- res.results.append(_('ack option set'))
-
- def set_digest(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- if not self.__authok:
- res.results.append(_('Not authenticated'))
- self._usage(res)
- return STOP
- arg = args[0].lower()
- if arg == 'off':
- try:
- mlist.setMemberOption(self.__address, config.Digests, 0)
- except Errors.AlreadyReceivingRegularDeliveries:
- pass
- elif arg == 'plain':
- try:
- mlist.setMemberOption(self.__address, config.Digests, 1)
- except Errors.AlreadyReceivingDigests:
- pass
- mlist.setMemberOption(self.__address, config.DisableMime, 1)
- elif arg == 'mime':
- try:
- mlist.setMemberOption(self.__address, config.Digests, 1)
- except Errors.AlreadyReceivingDigests:
- pass
- mlist.setMemberOption(self.__address, config.DisableMime, 0)
- else:
- res.results.append(_('Bad argument: %(arg)s'))
- self._usage(res)
- return STOP
- res.results.append(_('digest option set'))
-
- def set_delivery(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # Delivery status is handled differently than other options. If
- # status is true (set delivery on), then we enable delivery.
- # Otherwise, we have to use the setDeliveryStatus() interface to
- # specify that delivery was disabled by the user.
- if status:
- mlist.setDeliveryStatus(self.__address, MemberAdaptor.ENABLED)
- res.results.append(_('delivery enabled'))
- else:
- mlist.setDeliveryStatus(self.__address, MemberAdaptor.BYUSER)
- res.results.append(_('delivery disabled by user'))
-
- def set_myposts(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # sense is reversed
- mlist.setMemberOption(self.__address, config.DontReceiveOwnPosts,
- not status)
- res.results.append(_('myposts option set'))
-
- def set_hide(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- mlist.setMemberOption(self.__address, config.ConcealSubscription,
- status)
- res.results.append(_('hide option set'))
-
- def set_duplicates(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # sense is reversed
- mlist.setMemberOption(self.__address, config.DontReceiveDuplicates,
- not status)
- res.results.append(_('duplicates option set'))
-
- def set_reminders(self, res, args):
- mlist = res.mlist
- if len(args) <> 1:
- return self._usage(res)
- status = self._status(res, args[0])
- if status < 0:
- return STOP
- # sense is reversed
- mlist.setMemberOption(self.__address, config.SuppressPasswordReminder,
- not status)
- res.results.append(_('reminder option set'))
-
-
-
-def process(res, args):
- # We need to keep some state between set commands
- if not getattr(res, 'setstate', None):
- res.setstate = SetCommands()
- res.setstate.process(res, args)
diff --git a/src/attic/cmd_unsubscribe.py b/src/attic/cmd_unsubscribe.py
deleted file mode 100644
index 456b8089d..000000000
--- a/src/attic/cmd_unsubscribe.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""
- unsubscribe [password] [address=<address>]
- Unsubscribe from the mailing list. If given, your password must match
- your current password. If omitted, a confirmation email will be sent
- to the unsubscribing address. If you wish to unsubscribe an address
- other than the address you sent this request from, you may specify
- `address=<address>' (no brackets around the email address, and no
- quotes!)
-"""
-
-from email.Utils import parseaddr
-
-from mailman import Errors
-from mailman.i18n import _
-
-STOP = 1
-
-
-
-def gethelp(mlist):
- return _(__doc__)
-
-
-
-def process(res, args):
- mlist = res.mlist
- password = None
- address = None
- argnum = 0
- for arg in args:
- if arg.startswith('address='):
- address = arg[8:]
- elif argnum == 0:
- password = arg
- else:
- res.results.append(_('Usage:'))
- res.results.append(gethelp(mlist))
- return STOP
- argnum += 1
- # Fill in empty defaults
- if address is None:
- realname, address = parseaddr(res.msg['from'])
- if not mlist.isMember(address):
- listname = mlist.real_name
- res.results.append(
- _('%(address)s is not a member of the %(listname)s mailing list'))
- return STOP
- # If we're doing admin-approved unsubs, don't worry about the password
- if mlist.unsubscribe_policy:
- try:
- mlist.DeleteMember(address, 'mailcmd')
- except Errors.MMNeedApproval:
- res.results.append(_("""\
-Your unsubscription request has been forwarded to the list administrator for
-approval."""))
- elif password is None:
- # No password was given, so we need to do a mailback confirmation
- # instead of unsubscribing them here.
- cpaddr = mlist.getMemberCPAddress(address)
- mlist.ConfirmUnsubscription(cpaddr)
- # We don't also need to send a confirmation to this command
- res.respond = 0
- else:
- # No admin approval is necessary, so we can just delete them if the
- # passwords match.
- oldpw = mlist.getMemberPassword(address)
- if oldpw <> password:
- res.results.append(_('You gave the wrong password'))
- return STOP
- mlist.ApprovedDeleteMember(address, 'mailcmd')
- res.results.append(_('Unsubscription request succeeded.'))
diff --git a/src/attic/cmd_who.py b/src/attic/cmd_who.py
deleted file mode 100644
index 6c66610b3..000000000
--- a/src/attic/cmd_who.py
+++ /dev/null
@@ -1,152 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-from email.Utils import parseaddr
-
-from mailman import i18n
-from mailman.config import config
-
-STOP = 1
-
-def _(s): return s
-
-PUBLICHELP = _("""
- who
- See the non-hidden members of this mailing list.
- who password
- See everyone who is on this mailing list. The password is the
- list's admin or moderator password.
-""")
-
-MEMBERSONLYHELP = _("""
- who password [address=<address>]
- See the non-hidden members of this mailing list. The roster is
- limited to list members only, and you must supply your membership
- password to retrieve it. If you're posting from an address other
- than your membership address, specify your membership address with
- `address=<address>' (no brackets around the email address, and no
- quotes!). If you provide the list's admin or moderator password,
- hidden members will be included.
-""")
-
-ADMINONLYHELP = _("""
- who password
- See everyone who is on this mailing list. The roster is limited to
- list administrators and moderators only; you must supply the list
- admin or moderator password to retrieve the roster.
-""")
-
-_ = i18n._
-
-
-
-def gethelp(mlist):
- if mlist.private_roster == 0:
- return _(PUBLICHELP)
- elif mlist.private_roster == 1:
- return _(MEMBERSONLYHELP)
- elif mlist.private_roster == 2:
- return _(ADMINONLYHELP)
-
-
-def usage(res):
- res.results.append(_('Usage:'))
- res.results.append(gethelp(res.mlist))
-
-
-
-def process(res, args):
- mlist = res.mlist
- address = None
- password = None
- ok = False
- full = False
- if mlist.private_roster == 0:
- # Public rosters
- if args:
- if len(args) == 1:
- if mlist.Authenticate((config.AuthListModerator,
- config.AuthListAdmin),
- args[0]):
- full = True
- else:
- usage(res)
- return STOP
- else:
- usage(res)
- return STOP
- ok = True
- elif mlist.private_roster == 1:
- # List members only
- if len(args) == 1:
- password = args[0]
- realname, address = parseaddr(res.msg['from'])
- elif len(args) == 2 and args[1].startswith('address='):
- password = args[0]
- address = args[1][8:]
- else:
- usage(res)
- return STOP
- if mlist.isMember(address) and mlist.Authenticate(
- (config.AuthUser,
- config.AuthListModerator,
- config.AuthListAdmin),
- password, address):
- # Then
- ok = True
- if mlist.Authenticate(
- (config.AuthListModerator,
- config.AuthListAdmin),
- password):
- # Then
- ok = full = True
- else:
- # Admin only
- if len(args) <> 1:
- usage(res)
- return STOP
- if mlist.Authenticate((config.AuthListModerator,
- config.AuthListAdmin),
- args[0]):
- ok = full = True
- if not ok:
- res.results.append(
- _('You are not allowed to retrieve the list membership.'))
- return STOP
- # It's okay for this person to see the list membership
- dmembers = mlist.getDigestMemberKeys()
- rmembers = mlist.getRegularMemberKeys()
- if not dmembers and not rmembers:
- res.results.append(_('This list has no members.'))
- return
- # Convenience function
- def addmembers(members):
- for member in members:
- if not full and mlist.getMemberOption(member,
- config.ConcealSubscription):
- continue
- realname = mlist.getMemberName(member)
- if realname:
- res.results.append(' %s (%s)' % (member, realname))
- else:
- res.results.append(' %s' % member)
- if rmembers:
- res.results.append(_('Non-digest (regular) members:'))
- addmembers(rmembers)
- if dmembers:
- res.results.append(_('Digest members:'))
- addmembers(dmembers)
diff --git a/src/attic/digests.py b/src/attic/digests.py
deleted file mode 100644
index 812ae1649..000000000
--- a/src/attic/digests.py
+++ /dev/null
@@ -1,426 +0,0 @@
-# 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/>.
-
-"""Add the message to the list's current digest and possibly send it."""
-
-# Messages are accumulated to a Unix mailbox compatible file containing all
-# the messages destined for the digest. This file must be parsable by the
-# mailbox.UnixMailbox class (i.e. it must be ^From_ quoted).
-#
-# When the file reaches the size threshold, it is moved to the qfiles/digest
-# directory and the DigestRunner will craft the MIME, rfc1153, and
-# (eventually) URL-subject linked digests from the mbox.
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'ToDigest',
- ]
-
-
-import os
-import re
-import copy
-import time
-import logging
-
-from StringIO import StringIO # cStringIO can't handle unicode.
-from email.charset import Charset
-from email.generator import Generator
-from email.header import decode_header, make_header, Header
-from email.mime.base import MIMEBase
-from email.mime.message import MIMEMessage
-from email.mime.text import MIMEText
-from email.parser import Parser
-from email.utils import formatdate, getaddresses, make_msgid
-from zope.interface import implements
-
-from mailman import Message
-from mailman import Utils
-from mailman import i18n
-from mailman.Mailbox import Mailbox
-from mailman.Mailbox import Mailbox
-from mailman.config import config
-from mailman.core import errors
-from mailman.interfaces.handler import IHandler
-from mailman.interfaces.member import DeliveryMode, DeliveryStatus
-from mailman.pipeline.decorate import decorate
-from mailman.pipeline.scrubber import process as scrubber
-
-
-_ = i18n._
-
-UEMPTYSTRING = ''
-EMPTYSTRING = ''
-
-log = logging.getLogger('mailman.error')
-
-
-
-def process(mlist, msg, msgdata):
- # Short circuit non-digestable lists.
- if not mlist.digestable or msgdata.get('isdigest'):
- return
- mboxfile = os.path.join(mlist.data_path, 'digest.mbox')
- mboxfp = open(mboxfile, 'a+')
- mbox = Mailbox(mboxfp)
- mbox.AppendMessage(msg)
- # Calculate the current size of the accumulation file. This will not tell
- # us exactly how big the MIME, rfc1153, or any other generated digest
- # message will be, but it's the most easily available metric to decide
- # whether the size threshold has been reached.
- mboxfp.flush()
- size = os.path.getsize(mboxfile)
- if size / 1024.0 >= mlist.digest_size_threshold:
- # This is a bit of a kludge to get the mbox file moved to the digest
- # queue directory.
- try:
- # Enclose in try/except here because a error in send_digest() can
- # silently stop regular delivery. Unsuccessful digest delivery
- # should be tried again by cron and the site administrator will be
- # notified of any error explicitly by the cron error message.
- mboxfp.seek(0)
- send_digests(mlist, mboxfp)
- os.unlink(mboxfile)
- except Exception, errmsg:
- # Bare except is generally prohibited in Mailman, but we can't
- # forecast what exceptions can occur here.
- log.exception('send_digests() failed: %s', errmsg)
- mboxfp.close()
-
-
-
-def send_digests(mlist, mboxfp):
- # Set the digest volume and time
- if mlist.digest_last_sent_at:
- bump = False
- # See if we should bump the digest volume number
- timetup = time.localtime(mlist.digest_last_sent_at)
- now = time.localtime(time.time())
- freq = mlist.digest_volume_frequency
- if freq == 0 and timetup[0] < now[0]:
- # Yearly
- bump = True
- elif freq == 1 and timetup[1] <> now[1]:
- # Monthly, but we take a cheap way to calculate this. We assume
- # that the clock isn't going to be reset backwards.
- bump = True
- elif freq == 2 and (timetup[1] % 4 <> now[1] % 4):
- # Quarterly, same caveat
- bump = True
- elif freq == 3:
- # Once again, take a cheap way of calculating this
- weeknum_last = int(time.strftime('%W', timetup))
- weeknum_now = int(time.strftime('%W', now))
- if weeknum_now > weeknum_last or timetup[0] > now[0]:
- bump = True
- elif freq == 4 and timetup[7] <> now[7]:
- # Daily
- bump = True
- if bump:
- mlist.bump_digest_volume()
- mlist.digest_last_sent_at = time.time()
- # Wrapper around actually digest crafter to set up the language context
- # properly. All digests are translated to the list's preferred language.
- with i18n.using_language(mlist.preferred_language):
- send_i18n_digests(mlist, mboxfp)
-
-
-
-def send_i18n_digests(mlist, mboxfp):
- mbox = Mailbox(mboxfp)
- # Prepare common information (first lang/charset)
- lang = mlist.preferred_language
- lcset = Utils.GetCharSet(lang)
- lcset_out = Charset(lcset).output_charset or lcset
- # Common Information (contd)
- realname = mlist.real_name
- volume = mlist.volume
- issue = mlist.next_digest_number
- digestid = _('$realname Digest, Vol $volume, Issue $issue')
- digestsubj = Header(digestid, lcset, header_name='Subject')
- # Set things up for the MIME digest. Only headers not added by
- # CookHeaders need be added here.
- # Date/Message-ID should be added here also.
- mimemsg = Message.Message()
- mimemsg['Content-Type'] = 'multipart/mixed'
- mimemsg['MIME-Version'] = '1.0'
- mimemsg['From'] = mlist.request_address
- mimemsg['Subject'] = digestsubj
- mimemsg['To'] = mlist.posting_address
- mimemsg['Reply-To'] = mlist.posting_address
- mimemsg['Date'] = formatdate(localtime=1)
- mimemsg['Message-ID'] = make_msgid()
- # Set things up for the rfc1153 digest
- plainmsg = StringIO()
- rfc1153msg = Message.Message()
- rfc1153msg['From'] = mlist.request_address
- rfc1153msg['Subject'] = digestsubj
- rfc1153msg['To'] = mlist.posting_address
- rfc1153msg['Reply-To'] = mlist.posting_address
- rfc1153msg['Date'] = formatdate(localtime=1)
- rfc1153msg['Message-ID'] = make_msgid()
- separator70 = '-' * 70
- separator30 = '-' * 30
- # In the rfc1153 digest, the masthead contains the digest boilerplate plus
- # any digest header. In the MIME digests, the masthead and digest header
- # are separate MIME subobjects. In either case, it's the first thing in
- # the digest, and we can calculate it now, so go ahead and add it now.
- mastheadtxt = Utils.maketext(
- 'masthead.txt',
- {'real_name' : mlist.real_name,
- 'got_list_email': mlist.posting_address,
- 'got_listinfo_url': mlist.script_url('listinfo'),
- 'got_request_email': mlist.request_address,
- 'got_owner_email': mlist.owner_address,
- }, mlist=mlist)
- # MIME
- masthead = MIMEText(mastheadtxt.encode(lcset), _charset=lcset)
- masthead['Content-Description'] = digestid
- mimemsg.attach(masthead)
- # RFC 1153
- print >> plainmsg, mastheadtxt
- print >> plainmsg
- # Now add the optional digest header
- if mlist.digest_header:
- headertxt = decorate(mlist, mlist.digest_header, _('digest header'))
- # MIME
- header = MIMEText(headertxt.encode(lcset), _charset=lcset)
- header['Content-Description'] = _('Digest Header')
- mimemsg.attach(header)
- # RFC 1153
- print >> plainmsg, headertxt
- print >> plainmsg
- # Now we have to cruise through all the messages accumulated in the
- # mailbox file. We can't add these messages to the plainmsg and mimemsg
- # yet, because we first have to calculate the table of contents
- # (i.e. grok out all the Subjects). Store the messages in a list until
- # we're ready for them.
- #
- # Meanwhile prepare things for the table of contents
- toc = StringIO()
- print >> toc, _("Today's Topics:\n")
- # Now cruise through all the messages in the mailbox of digest messages,
- # building the MIME payload and core of the RFC 1153 digest. We'll also
- # accumulate Subject: headers and authors for the table-of-contents.
- messages = []
- msgcount = 0
- msg = mbox.next()
- while msg is not None:
- if msg == '':
- # It was an unparseable message
- msg = mbox.next()
- continue
- msgcount += 1
- messages.append(msg)
- # Get the Subject header
- msgsubj = msg.get('subject', _('(no subject)'))
- subject = Utils.oneline(msgsubj, in_unicode=True)
- # Don't include the redundant subject prefix in the toc
- mo = re.match('(re:? *)?(%s)' % re.escape(mlist.subject_prefix),
- subject, re.IGNORECASE)
- if mo:
- subject = subject[:mo.start(2)] + subject[mo.end(2):]
- username = ''
- addresses = getaddresses([Utils.oneline(msg.get('from', ''),
- in_unicode=True)])
- # Take only the first author we find
- if isinstance(addresses, list) and addresses:
- username = addresses[0][0]
- if not username:
- username = addresses[0][1]
- if username:
- username = ' ({0})'.format(username)
- # Put count and Wrap the toc subject line
- wrapped = Utils.wrap('{0:2}. {1}'.format(msgcount, subject), 65)
- slines = wrapped.split('\n')
- # See if the user's name can fit on the last line
- if len(slines[-1]) + len(username) > 70:
- slines.append(username)
- else:
- slines[-1] += username
- # Add this subject to the accumulating topics
- first = True
- for line in slines:
- if first:
- print >> toc, ' ', line
- first = False
- else:
- print >> toc, ' ', line.lstrip()
- # We do not want all the headers of the original message to leak
- # through in the digest messages. For this phase, we'll leave the
- # same set of headers in both digests, i.e. those required in RFC 1153
- # plus a couple of other useful ones. We also need to reorder the
- # headers according to RFC 1153. Later, we'll strip out headers for
- # for the specific MIME or plain digests.
- keeper = {}
- all_keepers = set(
- header for header in
- config.digests.mime_digest_keep_headers.split() +
- config.digests.plain_digest_keep_headers.split())
- for keep in all_keepers:
- keeper[keep] = msg.get_all(keep, [])
- # Now remove all unkempt headers :)
- for header in msg.keys():
- del msg[header]
- # And add back the kept header in the RFC 1153 designated order
- for keep in all_keepers:
- for field in keeper[keep]:
- msg[keep] = field
- # And a bit of extra stuff
- msg['Message'] = repr(msgcount)
- # Get the next message in the digest mailbox
- msg = mbox.next()
- # Now we're finished with all the messages in the digest. First do some
- # sanity checking and then on to adding the toc.
- if msgcount == 0:
- # Why did we even get here?
- return
- toctext = toc.getvalue()
- # MIME
- try:
- tocpart = MIMEText(toctext.encode(lcset), _charset=lcset)
- except UnicodeError:
- tocpart = MIMEText(toctext.encode('utf-8'), _charset='utf-8')
- tocpart['Content-Description']= _("Today's Topics ($msgcount messages)")
- mimemsg.attach(tocpart)
- # RFC 1153
- print >> plainmsg, toctext
- print >> plainmsg
- # For RFC 1153 digests, we now need the standard separator
- print >> plainmsg, separator70
- print >> plainmsg
- # Now go through and add each message
- mimedigest = MIMEBase('multipart', 'digest')
- mimemsg.attach(mimedigest)
- first = True
- for msg in messages:
- # MIME. Make a copy of the message object since the rfc1153
- # processing scrubs out attachments.
- mimedigest.attach(MIMEMessage(copy.deepcopy(msg)))
- # rfc1153
- if first:
- first = False
- else:
- print >> plainmsg, separator30
- print >> plainmsg
- # Use Mailman.pipeline.scrubber.process() to get plain text
- try:
- msg = scrubber(mlist, msg)
- except errors.DiscardMessage:
- print >> plainmsg, _('[Message discarded by content filter]')
- continue
- # Honor the default setting
- for h in config.digests.plain_digest_keep_headers.split():
- if msg[h]:
- uh = Utils.wrap('{0}: {1}'.format(
- h, Utils.oneline(msg[h], in_unicode=True)))
- uh = '\n\t'.join(uh.split('\n'))
- print >> plainmsg, uh
- print >> plainmsg
- # If decoded payload is empty, this may be multipart message.
- # -- just stringfy it.
- payload = msg.get_payload(decode=True) \
- or msg.as_string().split('\n\n',1)[1]
- mcset = msg.get_content_charset('us-ascii')
- try:
- payload = unicode(payload, mcset, 'replace')
- except (LookupError, TypeError):
- # unknown or empty charset
- payload = unicode(payload, 'us-ascii', 'replace')
- print >> plainmsg, payload
- if not payload.endswith('\n'):
- print >> plainmsg
- # Now add the footer
- if mlist.digest_footer:
- footertxt = decorate(mlist, mlist.digest_footer)
- # MIME
- footer = MIMEText(footertxt.encode(lcset), _charset=lcset)
- footer['Content-Description'] = _('Digest Footer')
- mimemsg.attach(footer)
- # RFC 1153
- # BAW: This is not strictly conformant RFC 1153. The trailer is only
- # supposed to contain two lines, i.e. the "End of ... Digest" line and
- # the row of asterisks. If this screws up MUAs, the solution is to
- # add the footer as the last message in the RFC 1153 digest. I just
- # hate the way that VM does that and I think it's confusing to users,
- # so don't do it unless there's a clamor.
- print >> plainmsg, separator30
- print >> plainmsg
- print >> plainmsg, footertxt
- print >> plainmsg
- # Do the last bit of stuff for each digest type
- signoff = _('End of ') + digestid
- # MIME
- # BAW: This stuff is outside the normal MIME goo, and it's what the old
- # MIME digester did. No one seemed to complain, probably because you
- # won't see it in an MUA that can't display the raw message. We've never
- # got complaints before, but if we do, just wax this. It's primarily
- # included for (marginally useful) backwards compatibility.
- mimemsg.postamble = signoff
- # rfc1153
- print >> plainmsg, signoff
- print >> plainmsg, '*' * len(signoff)
- # Do our final bit of housekeeping, and then send each message to the
- # outgoing queue for delivery.
- mlist.next_digest_number += 1
- virginq = config.switchboards['virgin']
- # Calculate the recipients lists
- plainrecips = set()
- mimerecips = set()
- # When someone turns off digest delivery, they will get one last digest to
- # ensure that there will be no gaps in the messages they receive.
- # Currently, this dictionary contains the email addresses of those folks
- # who should get one last digest. We need to find the corresponding
- # IMember records.
- digest_members = set(mlist.digest_members.members)
- for address in mlist.one_last_digest:
- member = mlist.digest_members.get_member(address)
- if member:
- digest_members.add(member)
- for member in digest_members:
- if member.delivery_status <> DeliveryStatus.enabled:
- continue
- # Send the digest to the case-preserved address of the digest members.
- email_address = member.address.original_address
- if member.delivery_mode == DeliveryMode.plaintext_digests:
- plainrecips.add(email_address)
- elif member.delivery_mode == DeliveryMode.mime_digests:
- mimerecips.add(email_address)
- else:
- raise AssertionError(
- 'Digest member "{0}" unexpected delivery mode: {1}'.format(
- email_address, member.delivery_mode))
- # Zap this since we're now delivering the last digest to these folks.
- mlist.one_last_digest.clear()
- # MIME
- virginq.enqueue(mimemsg,
- recips=mimerecips,
- listname=mlist.fqdn_listname,
- isdigest=True)
- # RFC 1153
- # If the entire digest message can't be encoded by list charset, fall
- # back to 'utf-8'.
- try:
- rfc1153msg.set_payload(plainmsg.getvalue().encode(lcset), lcset)
- except UnicodeError:
- rfc1153msg.set_payload(plainmsg.getvalue().encode('utf-8'), 'utf-8')
- virginq.enqueue(rfc1153msg,
- recips=plainrecips,
- listname=mlist.fqdn_listname,
- isdigest=True)
diff --git a/src/attic/docs/OLD-NEWS.txt b/src/attic/docs/OLD-NEWS.txt
deleted file mode 100644
index c87635640..000000000
--- a/src/attic/docs/OLD-NEWS.txt
+++ /dev/null
@@ -1,2835 +0,0 @@
-Mailman - The GNU Mailing List Management System
-Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-
-Here is a history of user visible changes to Mailman.
-
-2.1.10b4 (13-Mar-2008)
-
- Security
-
- - The 2.1.9 fixes for CVE-2006-3636 were not complete. In particular,
- some potential cross-site scripting attacks were not detected in
- editing templates and updating the list's info attribute via the web
- admin interface. This has been assigned CVE-2008-0564 and has been
- fixed. Thanks again to Moritz Naumann for assistance with this.
-
- New Features
-
- - Changed cmd_who.py to list all members if authorization is with the
- list's admin or moderator password and to accept the password if the
- roster is public. Also changed the web roster to show hidden members
- when authorization is by site or list's admin or moderator password
- (1587651).
-
- - Added the ability to put a list name in accept_these_nonmembers
- to accept posts from members of that list (1220144).
-
- - Added a new 'sibling list' feature to exclude members of another list
- from receiving a post from this list if the other list is in the To: or
- Cc: of the post or to include members of the other list if that list is
- not in the To: or Cc: of the post (Patch ID 1347962).
-
- - Added the admin_member_chunksize attribute to the admin General Options
- interface (Bug 1072002, Partial RFE 782436).
-
-Internationalization
-
- - Added the Hebrew translation from Dov Zamir. This includes addition of
- a direction ('ltr', 'rtl') to the LC_DESCRIPTIONS table. The
- add_language() function defaults direction to 'ltr' to not break
- existing mm_cfg.py files.
-
- - Added the Slovak translation from Martin Matuska.
-
- - Added the Galician translation from Frco. Javier Rial Rodríguez.
-
- Bug fixes and other patches
-
- - Added bounce recognition for several additional bounce formats.
-
- - Fixed CommandRunner.py to decode a quoted-printable or base64 encoded
- message part (1829061).
-
- - Fixed Scrubber.py to avoid loss of an implicit text/plain message part
- with no Content-* headers in a MIME multipart message (759841). Fixed
- several other minor scrubber issues (1242450).
-
- - Added Date and Message-ID headers to the confirm reply message that
- Mailman adds to the admin notification (1471318).
-
- - Fixed Cgi/options.py to not present the "empty" topic to user.
-
- - Fixed Handlers/CalcRecips.py to not process topics if topics are
- disabled for the list. This caused users who had previously subscribed
- to topics and elected to not receive non-matching posts to receive no
- messages after topics were disabled for the list.
-
- - Fixed MaildirRunner.py to handle hyphenated list names.
-
- - Fixed a bug in MimeDel.py (content filtering) which caused
- *_filename_extensions to not match if the extension in the message was
- not all lower case.
-
- - Fixed versions.py to not call a non-existant method when converting held
- posts from Mailman 1.0.x lists.
-
- - Added a test to configure to detect a missing python-devel package on
- some RedHat systems.
-
- - Fixed bin/dumpdb to once again be able to dump marshals (broken since
- 2.1.5) (963137).
-
- - Worked around a bug in the Python email library that could cause Mailman
- to not get the correct value for the sender of a message from an RFC
- 2231 encoded header causing spurious held messages.
-
- - Fixed bin/check_perms to detect certain missing permissions on the
- archives/private/ and archives/private/<list>/database/ directories.
-
- - Improved exception handling in cron/senddigests.
-
- - Changed the admindb page to not show the "Discard all messages marked
- Defer" checkbox when there are only (un)subscribes and no held messages.
- Also added a separator and heading for "Held Messages" like the ones for
- "Subscribe Requests" and "Unsubscribe Requests". Suppressed the
- "Database Updated" message when coming from the login page. Also
- removed the "Discard all messages marked Defer" checkbox from the
- details page where it didn't work (1562922, 1000699).
-
- - Fixed admin.py so null VARHELP category is handled (1573393).
-
- - Fixed OldStyleMemberships.py to preserve delivery statuses BYADMIN
- and BYUSER on a straight change of address (1642388). Also fixed a
- bug that could result in a member key with uppercase in the domain.
-
- - Fixed bin/withlist so that -r can take a full package path to a
- callable.
-
- - Removal of DomainKey/DKIM signatures is now controlled by Defaults.py
- mm_cfg.py variable REMOVE_DKIM_HEADERS (default = No). Also, if
- REMOVE_DKIM_HEADERS = Yes, an Authentication-Results: header will be
- removed if present.
-
- - The DeprecationWarning issued by Python 2.5 regarding string exceptions
- is supressed.
-
- - format=flowed and delsp=yes are now preserved for message bodies when
- message headers/footers are added and attachments are scrubbed
- (1495122).
-
- - Queue runner processing is improved to log and preserve for analysis in
- the shunt queue certain bad queue entries that were previously logged
- but lost. Also, entries are preserved when an attempt to shunt throws
- an exception (1656289).
-
- - The admin Membership List pages have been changed in that the email
- address which forms a part of the various CGI data keys is now
- urllib.quote()ed. This allows changing options for and unsubbing an
- address which contains a double-quote character, but it may require
- changes to scripts that screen-scrape the web admin interface to
- produce a membership list so they will report an unquoted address.
-
- - The fix for bug 1181161 in 2.1.7 was incomplete. The Approve(d): line
- wasn't always found in quoted-printable encoded parts and was never
- found in base64 encoded parts. This is now fixed.
-
- - Fixed a mail loop if a list owner puts the list's -bounces or -admin
- address in the list's owner attribute (1834569).
-
- - Fixed the mailto: link in archived messages to prefix the subject with
- Re: and to put the correct message-id in In-Reply-To (1621278, 1834281).
-
- - Coerced list name arguments to lower case in the change_pw, inject,
- list_admins and list_owners command line tools (patch 1842412).
-
- - Fixed cron/disabled to test if bounce info is stale before disabling
- a member when the threshold has been reduced.
-
- - It wasn't noted here, but in 2.1.9, queue runner processing was made
- more robust by making backups of queue entries when they were dequeued
- so they could be recovered in the event of a system failure. This
- opened the possibility that if a message itself caused a runner to
- crash, a loop could result that would endlessly reprocess the message.
- This has now been fixed by adding a dequeue count to the entry and
- moving the entry aside and logging the fact after the third dequeue of
- the same entry.
-
- - Fixed the command line scripts add_members, sync_members and
- clone_member to properly handle banned addresses (1904737).
-
- - Fixed bin/newlist to add the list's preferred language to the list's
- available_languages if it is other than the server's default language
- (1906368).
-
- - Changed the first URL in the RFC 2369 List-Unsubscribe: header to go
- to the options login page instead of the listinfo page.
-
- - Changed the options login page to not issue the "No address given" error
- when coming from the List-Unsubscribe and other direct links. Also
- changed to remember the user's language selection when redisplaying the
- page following an error.
-
- - Changed cmd_subscribe.py to properly accept (no)digest without a
- password and to recognize (no)digest and address= case insensitively.
-
- Miscellaneous
-
- - Brad Knowles' mailman daily status report script updated to 0.0.17.
-
-2.1.9 (12-Sep-2006)
-
- Security
-
- - A malicious user could visit a specially crafted URI and inject an
- apparent log message into Mailman's error log which might induce an
- unsuspecting administrator to visit a phishing site. This has been
- blocked. Thanks to Moritz Naumann for its discovery.
-
- - Fixed denial of service attack which can be caused by some
- standards-breaking RFC 2231 formatted headers. CVE-2006-2941.
-
- - Several cross-site scripting issues have been fixed. Thanks to Moritz
- Naumann for their discovery. CVE-2006-3636
-
- - Fixed an unexploitable format string vulnerability. Discovery and fix
- by Karl Chen. Analysis of non-exploitability by Martin 'Joey' Schulze.
- Also thanks go to Lionel Elie Mamane. CVE-2006-2191.
-
- Internationalization
-
- - New languages: Arabic, Vietnamese.
-
- Bug fixes and other patches
-
- - Fixed Decorate.py so that characters in message header/footer which
- are not in the character set of the list's language are ignored rather
- than causing shunted messages (1507248).
-
- - Switchboard.py - Closed very tiny holes at the upper ends of queue
- slices that could result in unprocessable queue entries. Improved FIFO
- processing when two queue entries have the same timestamp.
-
-2.1.8 (15-Apr-2006)
-
- Security
-
- - A cross-site scripting hole in the private archive script of 2.1.7
- has been closed. Thanks to Moritz Naumann for its discovery.
-
- Bug fixes and other patches
-
- - Bouncers support added: 'unknown user', Microsoft SMTPSVC, Prodigy.net
- and several others.
-
- - Updated email library to 2.5.7 which will encode payload into qp/base64
- upon setting. This enabled backing out the scrubber related patches
- including 'X-Mailman-Scrubbed' header in 2.1.7.
-
- - Fix SpamDetect.py potential hold/reject loop problem.
-
- - A warning message from email package to the stderr can cause error
- in Logging because stderr may be detached from the process during
- the qrunner run. We chose not to output errors to stderr but to
- the logs/error if the process is running under mailmanctl subprocess.
-
- - DKIM header cleansing was separated from Cleanse.py and added to
- -owner messages too.
-
- - Fixes: Lose Topics when go directly to topics URL (1194419).
- UnicodeError running bin/arch (1395683). edithtml.py missing import
- (1400128). Bad escape in cleanarch. Wrong timezone in list archive
- index pages (1433673). bin/arch fails with TypeError (1430236).
- Subscription fails with some Language combinations (1435722).
- Postfix delayed notification not recognized (863989). 2.1.7 (VERP)
- mistakes delay notice for bounce (1421285). show_qfiles: 'str'
- object has no attribute 'as_string' (1444447). Utils.get_domain()
- wrong if VIRTUAL_HOST_OVERVIEW off (1275856).
-
- Miscellaneous
-
- - Brad Knowles' mailman daily status report script updated to 0.0.16.
-
-2.1.7 (31-Dec-2005)
-
- Security
-
- - The fix for CAN-2005-0202 has been enhanced to issue an appropriate
- message instead of just quietly dropping ./ and ../ from URLs.
-
- - A note on CVE-2005-3573: Although the RFC2231 bug example in the CVE has
- been solved in Mailman 2.1.6, there may be more cases where
- ToDigest.send_digests() can block regular delivery. We put the
- send_digests() calling part in a try/except clause and leave a message
- in the error log if something happened in send_digests(). Daily call of
- cron/senddigests will provide more detail to the site administrator.
-
- - List administrators can no longer change the user's option/subscription
- globally. Site admin can change these only if
- mm_cfg.ALLOW_SITE_ADMIN_COOKIES is set to Yes.
-
- - <script> tags are HTML-escaped in the edithtml CGI script.
-
- - Since the probe message for disabled users may reach unintended
- recipients, the password is excluded from sendProbe() and probe.txt.
- Note that the default value of VERP_PROBE has been set to `No' from
- 2.1.6., thus this change doesn't affect the default behavior.
-
- New Features
-
- - Always remove DomainKey (and similar) headers from messages sent to the
- list. (1287546)
-
- - List owners can control the content filter behavior when collapsing
- multipart/alternative parts to its first subpart. This allows the
- option of letting the HTML part pass through after other content
- filtering is done.
-
- Internationalization
-
- - New language: Interlingua.
-
- Bug fixes and other patches
-
- - Defaults.py.in: SCRUBBER_DONT_USE_ATTACHMENT_FILENAME is set to True for
- safer operation.
-
- - Fixed the bug where Scrubber.py munges quoted-printable by introducing
- the 'X-Mailman-Scrubbed' header which marks that the payload is
- scrubber-munged. The flag is referenced in ToDigest.py, ToArchive.py,
- Decorate.py and Archiver. A similar problem in ToDigest.py where the
- plain digest is generated is also fixed.
-
- - Fixed Syslog.py to write quopri encoded messages when it fail to write
- 8-bit characters.
-
- - Fixed MTA/Postfix.py to check aliases group permission in check_perms
- and fixed mailman-install document on this matter (1378270).
-
- - Fixed private.py to go to the original URL after authorization
- (1080943).
-
- - Fixed bounce log score messages to be more consistent.
-
- - Fixed bin/remove_members to accept no arguments when both --fromall and
- --file= options are specified.
-
- - Changed cgi-bin and mail wrapper "group not found" error message to be
- more descriptive of the actual problem.
-
- - The list's ban_list now applies to address changes, admin mass
- subscribes and invites, and to confirmations/approvals of address
- changes, subscriptions and invitations.
-
- - quoted-printable and base64 encoded parts are decoded before passing to
- HTML_TO_PLAIN_TEXT_COMMAND (1367783).
-
- - Approve: header is removed from posts, and treated the same as the
- Approved: header. (1355707)
-
- - Fixed the removal of the line following Approve[d]: line in body of
- post. (1318883)
-
- - The Approve[d]: <password> header is removed from all text/* parts in
- addition the initial text/plain part. It must still be the first
- non-blank line in the first text/plain part or it won't be found or
- removed at all. (1181161)
-
- - Posts are now logged in post log file with the true sender, not
- listname-bounces. (1287921)
-
- - Correctly initialize and remember the list's default_member_moderation
- attribute in the web list creation page. (1263213)
-
- - PEP263 charset is added to the config_list output. (1343100)
-
- - Fixed header_filter_rules getting lost if accessed directly and
- authentication was needed by login page. (1230865)
-
- - Obscure email when the poster doesn't set full name in 'From:' header.
-
- - Preambles and epilogues are taken into account when calculating message
- sizes for holding purposes. (Mark Sapiro)
-
- - Logging/Logger.py unicode transform option. (1235567)
-
- - bin/update crashes with bogus files. (949117)
-
- - Bugs and patches: 1212066/1301983 (Date header in create/remove notice)
-
-2.1.6 (30-May-2005)
-
- Security
-
- - Critical security patch for path traversal vulnerability in private
- archive script (CAN-2005-0202).
-
- - Added the ability for Mailman generated passwords (both member and list
- admin) to be more cryptographically secure. See new configuration
- variables USER_FRIENDLY_PASSWORDS, MEMBER_PASSWORD_LENGTH, and
- ADMIN_PASSWORD_LENGTH. Also added a new bin/withlist script called
- reset_pw.py which can be used to reset all member passwords. Passwords
- generated by Mailman are now 8 characters by default for members, and 10
- characters for list administrators.
-
- - A potential cross-site scripting hole in the driver script has been
- closed. Thanks to Florian Weimer for its discovery. Also, turn
- STEALTH_MODE on by default.
-
- Internationalization
-
- - Chinese languages are now supported. They have been moved from 'big5'
- and 'gb' to 'zh_TW' and 'zh_CN' respectively for compliance to the IANA
- spec. Note, however, that the character sets were changed from 'Big5'
- or 'GB2312' to 'UTF-8' to cope with the insufficient codecs support in
- Python 2.3 and earlier. You may have to install Chinese capable codecs
- (like CJKCodecs) separately to handle the incoming messages which are in
- local charsets, or upgrade your Python to 2.4 or newer.
-
- Behavior or defaults changes
-
- - VERP_PROBES is disabled by default.
-
- - bin/withlist can be run without a list name, but only if -i is given.
- Also, withlist puts the directory it's found in at the end of sys.path,
- making it easier to run withlist scripts that live in $prefix/bin.
-
- - bin/newlist grew two new options: -u/--urlhost and -e/--emailhost which
- lets the user provide the web and email hostnames for the new mailing
- list. This is a better way to specify the domain for the list, rather
- than the old 'mylist@hostname' syntax (which is still supported for
- backward compatibility, but deprecated).
-
- Compatibility
-
- - Python 2.4 compatibility issue: time.strftime() became strict about the
- 'day of year' range. (1078482)
-
- New Features
-
- - New feature: automatic discards of held messages. List owners can now
- set how many days to hold the messages in the moderator request queue.
- cron/checkdb will automatically discard old messages. See the
- max_days_to_hold variable in the General Options and
- DEFAULT_MAX_DAYS_TO_HOLD in Defaults.py. This defaults to 0
- (i.e. disabled). (790494)
-
- - New feature: subject_prefix can be configured to include a sequence
- number which is taken from the post_id variable. Also, the prefix is
- always put at the start of the subject, i.e. "[list-name] Re: original
- subject", if mm_cfg.OLD_STYLE_PREFIXING is set No. The default style
- is "Re: [list-name]" if numbering is not set, for backward compatibility.
- If the list owner is using numbering feature by "%d" directive, the new
- style, "[list-name 123] Re:", is always used.
-
- - List owners can now cusomize the non-member rejection notice from
- admin/<listname>/privacy/sender page. (1107169)
-
- - Allow editing of the welcome message from the admin page (1085501).
-
- - List owners can now use Scrubber to get the attachments scrubbed (held
- in the web archive), if the site admin permits it in mm_cfg.py. New
- variables introduced are SCRUBBER_DONT_USE_ATTACHMENT_FILENAME and
- SCRUBBER_USE_ATTACHMENT_FILENAME_EXTENSION in Defaults.py for scrubber
- behavior. (904850)
-
- Documentation
-
- - Most of the installation instructions have been moved to a latex
- document. See doc/mailman-install/index.html for details.
-
- Bug fixes and other patches
-
- - Mail-to-news gateway now strips subject prefix off from a response
- by a mail user if news_prefix_subject_too is not set.
-
- - Date and Message-Id headers are added for digests. (1116952)
-
- - Improved mail address sanity check. (1030228)
-
- - SpamDetect.py now checks attachment header. (1026977)
-
- - Filter attachments by filename extensions. (1027882)
-
- - Bugs and patches: 955381 (older Python compatibility), 1020102/1013079/
- 1020013 (fix spam filter removed), 665569 (newer Postfix bounce
- detection), 970383 (moderator -1 admin requests pending), 873035
- (subject handling in -request mail), 799166/946554 (makefile
- compatibility), 872068 (add header/footer via unicode), 1032434
- (KNOWN_SPAMMERS check for multi-header), 1025372 (empty Cc:), 789015
- (fix pipermail URL), 948152 (Out of date link on Docs), 1099138
- (Scrubber.py breaks on None part), 1099840/1099840 (deprecated %
- insertion), 880073/933762 (List-ID RFC compliance), 1090439 (passwd
- reminder shunted), 1112349 (case insensitivity in acceptable_aliases),
- 1117618 (Don't Cc for personalized anonymous list), 1190404 (wrong
- permission after editing html)
-
-2.1.5 (15-May-2004)
-
- - The admindb page has a checkbox that allows you to discard all held
- messages that are marked Defer. On heavy lists with lots of spam holds,
- this makes clearing them much faster.
-
- - The qrunner system has changed to use only one file per message.
- However the configuration variable METADATA_FORMAT has been removed, and
- support for SAVE_MSGS_AS_PICKLES has been changed. The latter no longer
- writes messages as plain text. Instead, they are stored as pickles of
- plain strings, using the text pickle format. This still makes them
- non-binary files readable and editable by humans.
-
- bin/dumpdb also works differently. It will print out the entire pickle
- file (with more verbosity) and if used with 'python -i', it binds msg to
- a list of all objects found in the pickle file.
-
- Removed from Defaults.py: PENDINGDB_LOCK_TIMEOUT,
- PENDINGDB_LOCK_ATTEMPTS, METAFMT_MARSHAL, METAFMT_BSDDB_NATIVE,
- METAFMT_ASCII, METADATA_FORMAT
-
- - The bounce processor has been redesigned so that now when an address's
- bounce score reaches the threshold, that address will be sent a probe
- message. Only if the probe bounces will the address be disabled. The
- score is reset to zero when the probe is sent. Also, bounce events are
- now kept in an event file instead of in memory. This should help
- contain the bloat of the BounceRunner.
-
- New supporting variables in Defaults.py: VERP_PROBE_FORMAT,
- VERP_PROBE_REGEXP
-
- REGISTER_BOUNCES_EVERY is promoted to a Defaults.py variable.
-
- - The pending database has been changed from a global pickle file, to a
- unique pickle file per mailing list.
-
- - The 'request' database file has changed from a marshal, to the more
- secure pickle format.
-
- - Disallow multiple password retrievals.
-
- - SF patch #810675 which adds a "Discard all messages marked Defer" button
- for faster admindb maintenance.
-
- - The email package is updated to version 2.5.5.
-
- - New language: Turkish.
-
- - Bugs and patches: 869644, 869647 (NotAMemberError for old cookie data),
- 878087 (bug in Slovenian catalog), 899263 (ignore duplicate pending
- ids), 810675 (discard all defers button)
-
-2.1.4 (31-Dec-2003)
-
- - Close some cross-site scripting vulnerabilities in the admin pages
- (CAN-2003-0965).
-
- - New languages: Catalan, Croatian, Romanian, Slovenian.
-
- - New mm_cfg.py/Defaults.py variable PUBLIC_MBOX which allows the site
- administrator to disable public access to all the raw list mbox files
- (this is not a per-list configuration).
-
- - Expanded header filter rules under Privacy -> Spam Filters. Now you can
- specify regular expression matches against any header, with specific
- actions tied to those matches.
-
- - Rework the SMTP error handling in SMTPDirect.py to avoid scoring bounces
- for all recipients when a permanent error code is returned by the mail
- server (e.g. because of content restrictions).
-
- - Promoted SYNC_AFTER_WRITE to a Default.py/mm_cfg.py variable and
- make it control syncing on the config.pck file. Also, we always flush
- and sync message files.
-
- - Reduce archive bloat by not storing the HTML body of Article objects in
- the Pipermail database. A new script bin/rb-archfix was added to clean
- up older archives.
-
- - Proper RFC quoting for List-ID descriptions.
-
- - PKGDIR can be passed to the make command in order to specify a different
- directory to unpack the distutils packages in misc. (SF bug 784700).
-
- - Improved logging of the origin of subscription requests.
-
- - Bugs and patches: 832748 (unsubscribe_policy ignored for unsub button on
- member login page), 846681 (bounce disabled cookie was always out of
- date), 835870 (check VIRTUAL_HOST_OVERVIEW on through the web list
- creation), 835036 (global address change when the new address is already
- a member of one of the lists), 833384 (incorrect admin password on a
- hold message confirmation attachment would discard the message), 835012
- (fix permission on empty archive index), 816410 (confirmation page
- consistency), 834486 (catch empty charsets in the scrubber), 777444 (set
- the process's supplemental groups if possible), 860135 (ignore
- DiscardMessage exceptions during digest scrubbing), 828811 (reduce
- process size for list and admin overviews), 864674/864676 (problems
- accessing private archives and rosters with admin password), 865661
- (Tokio Kikuchi's i18n patches), 862906 (unicode prefix leak in admindb),
- 841445 (setting new_member_options via config_list), n/a (fixed email
- command 'set delivery')
-
-2.1.3 (28-Sep-2003)
-
- Performance, Reliability, Security
-
- - Closed a cross-site scripting exploit in the create cgi script.
-
- - Improvements in the performance of the bounce processor.
- Now, instead of processing each bounce immediately (which
- can cause severe lock contention), bounce events are queued.
- Every 15 minutes by default, the queued bounce events are
- processed en masse, on a list-per-list basis, so that each
- list only needs to be locked once.
-
- - When some or all of a message's recipients have temporary
- delivery failures, the message is moved to a "retry" queue.
- This queue wakes up occasionally and moves the file back to
- the outgoing queue for attempted redelivery. This should
- fix most observed OutgoingRunner 100% cpu consumption,
- especially for bounces to local recipients when using the
- Postfix MTA.
-
- - Optional support for fsync()'ing qfile data after writing.
- Under some catastrophic system failures (e.g. power lose),
- it would be possible to lose messages because the data
- wasn't sync'd to disk. By setting SYNC_AFTER_WRITE to True
- in Mailman/Queue/Switchboard.py, you can force Mailman to
- fsync() queue files after flushing them. The benefits are
- debatable for most operating environments, and you must
- ensure that your Python has the os.fsync() function defined
- before enabling this feature (it isn't, even on all
- Unix-like operating systems).
-
- Internationalization
-
- - New languages Ukrainian, Serbian, Danish, Euskara/Basque.
-
- - Fixes to template lookup. Lists with local overriding
- templates would find the wrong template.
-
- - .mo files (for internationalization) are now generated at
- build time instead of coming as part of the source
- distribution.
-
- Documentation
-
- - A first draft of member documentation by Terri Oda. There
- is also a Japanese translation of this manual by Ikeda Soji.
-
- Archiver / Pipermail
-
- - In the configuration variables PUBLIC_EXTERNAL_ARCHIVER, and
- PRIVATE_EXTERNAL_ARCHIVER, %(hostname)s has been added to
- the list of allowable substitution variables.
-
- - The timezone is now taken into account when figuring the
- posting date for an article.
-
- Scripts / Cron
-
- - Fixes to cron/disabled for NotAMemberError crashes.
-
- - New script bin/show_qfiles which prints the contents of .pck
- message files. New script bin/discard which can be used to
- mass discard held messages.
-
- - Fixes to cron/mailpasswds to account for old password-less
- subscriptions.
-
- - bin/list_members has grown two new options: --invalid/-i
- prints only the addresses in the member database that are
- invalid (which could have snuck in via old releases);
- --unicode/-u prints addresses which are stored as Unicode
- objects instead of as normal strings.
-
- Miscellaneous
-
- - Fixes to problems in some configurations where Python wouldn't
- be able to find its standard library.
-
- - Fixes to the digest which could cause MIME-losing missing
- newlines when parts are scrubbed via the content filters.
-
- - In the News/Mail gateway admin page, the configuration variable
- nntp_host can now be a name:port pair.
-
- - When messages are pulled from NNTP, the member moderation checks
- are short-circuited.
-
- - email 2.5.4 is included. This fixes an RFC 2231 bug, among
- possibly others.
-
- - Fixed some extra spaces that could appear in the List-ID header.
-
- - Fixes to ensure that invalid email addresses can't be invited.
-
- - WEB_LINK_COLOR in Defaults.py/mm_cfg.py should now work.
-
- - Fixes so that shunted message file names actually match
- those logged in log/errors.
-
- - An improved pending action cookie generation algorithm has
- been added.
-
- - Fixes to the DSN bounce detector.
-
- - The usual additional u/i, internationalization, unicode, and
- other miscellaneous fixes.
-
-2.1.2 (22-Apr-2003)
-
- - New languages Portuguese (Portugal) and Polish.
-
- - Many convenient constants have been added to the Defaults.py
- module to (hopefully) make it more readable.
-
- - Email addresses which contain 8-bit characters in them are now
- rejected and won't be subscribed. This is not the same as 8-bit
- characters in the realname, which is still allowed.
-
- - The X-Originating-Email header is removed for anonymous lists.
- Hotmail apparently adds this header.
-
- - When running make to build Mailman, you can specify $DESTDIR to
- the install target to specify an alternative location for
- installation, without influencing the paths stored in
- e.g. Defaults.py. This is useful to package managers.
-
- - New Defaults.py variable DELIVERY_RETRY_WAIT which controls how
- long the outgoing qrunner will wait before it retries a
- tempfailure delivery.
-
- - The semantics for the extend.py hook to MailList objects has
- changed slightly. The hook is now called before attempting to
- lock and load the database.
-
- - Mailman now uses the email package version 2.5.1
-
- - bin/transcheck now checks for double-%'s
-
- - bin/genaliases grew a -q / --quiet flag
-
- - cron/checkdbs grew a -h / --help option.
-
- - The -c / --change-msg option has been removed from bin/add_members
-
- - bin/msgfmt.py has been added, taken from Python 2.3's Tools/i18n
- directory. The various .mo files are now no longer distributed
- with Mailman. They are generated at build time instead.
-
- - A new file misc/sitelist.cfg which can be used with
- bin/config_list provides a small number of recommended settings
- for your site list. Be sure to read it over before applying!
- sitelist.cfg is installed into the data directory.
-
- - Many bug fixes, including these SourceForge bugs closed and
- patches applied: 677668, 690448, 700538, 700537, 673294, 683906,
- 671294, 522080, 521124, 534297, 699900, 697321, 695526, 703941,
- 658261, 710678, 707608, 671303, 717096, 694912, 707624, 716755,
- 661138, 716754, 716702, 667167, 725369, 726415
-
-
-2.1.1 (08-Feb-2003)
-
- Lots of bug fixes and language updates. Also:
-
- - Closed a cross-site scripting vulnerability in the user options page.
-
- - Restore the ability to control which headers show up in messages
- included in plaintext and MIME digests. See the variables
- PLAIN_DIGEST_KEEP_HEADERS and MIME_DIGEST_KEEP_HEADERS in
- Defaults.py.
-
- - Messages included in the plaintext digests are now sent through
- the scrubber to remove (and archive) attachments. Otherwise,
- attachments would screw up plaintext digests. MIME digests
- include the attachments inline.
-
-2.1 final (30-Dec-2002)
-
- Last minute bug fixes and language updates.
-
-2.1 rc 1 (24-Dec-2002)
-
- Bug fixes and language updates. Also,
-
- - Lithuanian support has been added.
-
- - bin/remove_members grew --nouserack and --noadminack switches
-
- - configure now honors --srcdir
-
-2.1 beta 6 (09-Dec-2002)
-
- Lots and lots of bug fixes, and translation updates. Also,
-
- - ARCHIVER_OBSCURES_EMAILADDRS is now set to true by default.
-
- - QRUNNER_SAVE_BAD_MESSAGES is now set to true by default.
-
- - Bounce messages which were recognized, but in which no member
- addresses were found are no longer forwarded to the list
- administrator.
-
- - bin/arch grew a --wipe option which first removes the entire old
- archive before regenerating the new one.
-
- - bin/mailmanctl -u now prints a warning that permission problems
- could appear, such as when trying to delete a list through the
- web that has some archives in it.
-
- - bin/remove_members grew --nouserack/-n and -noadminack/-N options.
-
- - A new script bin/list_owners has been added for printing out
- list owners and moderators.
-
- - Dates in the web version of archived messages are now relative
- to the local timezone, and include the timezone names, when
- available.
-
-2.1 beta 5 (19-Nov-2002)
-
- As is typical for a late beta release, this one includes the usual
- bug fixes, tweaks, and massive new features (just kidding).
-
- IMPORTANT: If you are using Pipermail, and you have any archives
- that were created or added to in 2.1b4, you will need to run
- bin/b4b5-archfix, followed by bin/check_perms to fix some serious
- performance problems. From you install directory, run
- "bin/b4b5-archfix --help" for details.
-
- - The personalization options have been tweaked to provide more
- control over mail header and decoration personalizations. In
- 2.1b4, when personalization was enabled, the To and Cc headers
- were always overwritten. But that's usually not appropriate for
- anything but announce lists, so now these headers aren't changed
- unless "Full personalization" is enabled.
-
- - You now need to go to the General category to enable emergency
- moderation.
-
- - The order of the hold modules in the GLOBAL_PIPELINE has
- changed, again. Now Moderate comes before Hold.
-
- - Estonian language support has been added.
-
- - All posted messages should now get decorated with headers and
- footers in a MIME-safe way. Previously, some MIME type messages
- didn't get decorated at all.
-
- - bin/arch grew a -q/--quiet option
-
- - bin/list_lists grew a -b/--bare option
-
-2.1 beta 4 (26-Oct-2002)
-
- The usual assortment of bug fixes and language updates, some u/i
- tweaks, as well as the following:
-
- - Configuring / building / installing
- o Tightened up some configure checks; it will now bark loudly
- if you don't have the Python distutils package available
- (some Linux distros only include distutils in their "devel"
- packages).
-
- o Mailman's username/group security assertions are now done by
- symbolic name instead of numeric id. This provides a level
- of indirection that makes it much easier to move or package
- Mailman. --with-mail-gid and --with-cgi-gid are retained,
- but they control the group names used instead.
-
- - Command line scripts
- o A new script, bin/transcheck that language teams can use to
- check their .po files.
-
- o bin/list_members grew a --fullnames/-f option to print the
- full names along with the addresses.
-
- o cron/senddigests grew --help/-h and --listname/-l options.
-
- o bin/fix_url.py grew some command line options to support moving
- a list to a specific virtual domain.
-
- - Pipermail / archiving
- o Reworked the directory layout for archive attachments to be
- less susceptible to inode overload. Attachments are now
- placed in
-
- archives/private/<listname>/attachments/<YYYYMMDD>/<msgidhash>
-
- o Internationalization support in the archiver has been improved.
-
- - Internationalization
- o New languages: Swedish.
-
- - Mail handling
- o Content filtering now has a pass_mime_type variable, which
- is a whitelist of MIME types to allow in postings. See the
- details of the variable in the Content Filtering category
- for more information.
-
- o If a member has enabled their DontReceiveDuplicates option,
- we'll also strip their addresses from the Cc headers in the
- copy of the message sent to the list. This helps keep the
- Cc lines from growing astronomically.
-
- o Bounce messages are now forwarded to the list administrators
- both if they are unrecognized, and if no list member's
- address could be extracted.
-
- o Content filtering now has a filter_action variable which
- controls what happens when a message matches the content
- filter rules. The default is still to discard the message.
-
- o When searching for an Approve/Approved header, the first
- non-whitespace line of the body of the message is also
- checked, if the body has a MIME type of text/plain.
-
- o If a list is personalized, and the list's posting address is
- not included in a Reply-To header, the posting address is
- copied into a Cc header, otherwise there was no (easy) way a
- recipient could reply back to the list.
-
- o Added a MS Exchange bounce recognizer.
-
- o New configuration variable news_moderation which allows the
- mail->news gateway to properly post to moderated newsgroups.
-
- o Messages sent to a list's owners now comes from the site
- list to prevent mail loops when list owners or moderators
- having bouncing addresses.
-
- - Miscellaneous
- o mailanctl prevents runaway restarts by imposing a maximum
- restart value (defaulting to 10) for restarting the
- qrunners. If you hit this limit, do "mailmanctl stop"
- followed by "mailmanctl start".
-
- o The Membership Management page's search feature now includes
- searching on members real names.
-
- o The start of a manual for list administrators is given in
- Python HOWTO format (LaTeX). It's in doc/mailman-admin.tex
- but it still needs lots of fleshing out.
-
- o More protections against creating a list with an invalid name.
-
-2.1 beta 3 (09-Aug-2002)
-
- The usual assortment of bug fixes and language updates.
-
- - New languages: Dutch, Portuguese (Brazil)
-
- - New configure script options: --with-mailhost, --with-urlhost,
- --without-permcheck. See ./configure --help for details.
-
- - The encoding of Subject: prefixes is controlled by a new list
- option encode_ascii_prefixes. This is useful for languages with
- character sets other than us-ascii. See the Languages admin
- page for details.
-
- - A new list option news_prefix_subject_too controls whether
- postings gated from mail to news should have the subject prefix
- added to their Subject: header.
-
- - The algorithm for upgrading the moderation controls for a
- Mailman 2.0.x list has changed. The change should be
- transparent, but you'll want to double check the moderation
- controls after upgrading from MM2.0.x. This should have no
- effect for upgrades from a previous MM2.1 beta.
-
- See the UPGRADING file for details.
-
- - On the Mass Subscribe admin page, a text box has been added so
- that the admin can add a custom message to be prepended to the
- welcome/invite notification.
-
- - On the admindb page, a link is included to more easily reload
- the page.
-
- - The Sendmail.py delivery module is sabotaged so that it can't be
- used naively. You need to read the comments in the file and
- edit the code to use this unsafe module.
-
- - When a member sends a `help' command to the request address,
- the url to their options page is included in the response.
-
- - Autoresponses, -request command responses, and posting hold
- notifications are inhibited for any message that has a
- Precedence: {bulk|list|junk} header. This is to avoid mail
- loops between email 'bots. If the original message has an
- X-Ack: yes header, the response is sent.
-
- Responses are also limited to a maximum number per day, as
- defined in the site variable MAX_AUTORESPONSES_PER_DAY. This is
- another guard against 'bot loops, and it defaults to 10.
-
- - When a Reply-To: header is munged to include both the original
- and the list address, the list address is always added last.
-
- - The cron/mailpasswds script has grown a -l/--listname option.
-
- - The cron/disabled script has grown options to send out
- notifications for reasons other than bounce-disabled. It has
- also grown a -f/--force option. See cron/disabled --help for
- details.
-
- - The bin/dumpdb script has grown a -n/--noprint option.
-
- - An experimental new mechanism for processing incoming messages
- has been added. If you can configure your MTA to do qmail-style
- Maildir delivery, Mailman now has a MaildirRunner qrunner. This
- may turn out to be much more efficient and scalable, but for
- MM2.1, it will not be officially supported. See Defaults.py.in
- and Mailman/Queue/MaildirRunner.py for details.
-
-2.1 beta 2 (05-May-2002)
-
- Lots of bug fixing, and the following new features and changes:
-
- - A "de-mime" content filter feature has been added. This
- oft-requested feature allows you to specify MIME types that
- Mailman should strip off of any messages before they're posted
- to the list. You can also optionally convert text/html to
- text/plain (by default, through lynx if it's available).
-
- - Changes to the way the RFC 2919 and 2369 headers (i.e. the
- List-*: headers) are added:
- o List-Id: is always added
- o List-Post:, List-Help:, List-Subscribe:,
- List-Unsubscribe:, and List-Archive: are only added to
- posting messages.
- o X-List-Administrivia: is only added to messages Mailman
- creates and sends out of its own accord.
-
- Also, if the site administrator allows it, list owners can
- suppress the addition of all the List-*: headers. List owners
- can also separately suppress the List-Post: header for
- announce-only lists.
-
- - A new framework for email commands has been added. This allows
- you to easily add, delete, or change the email commands that
- Mailman understands, on a per-site, per-list, or even per-user
- basis.
-
- - Users can now change their digest delivery type from MIME to
- plain text globally, for all lists they are subscribed to.
-
- - No language select pulldowns are shown if the list only supports
- one language.
-
- - More mylist-admin eradication.
-
- - Several performance improvements in the bounce qrunner, one of
- which is to make it run only once per minute instead of once per
- second.
-
- - Korean language support as been added.
-
- - Gatewaying from news -> mail uses its connections to the nntpd
- more efficiently.
-
- - In bin/add_members, -n/--non-digest-members-file command line
- switch is deprecated in favor of -r/--regular-members-file.
-
- - bin/sync_members grew a -g/--goodbye-msg switch.
-
-2.1 beta 1 (16-Mar-2002)
-
- In addition to the usual bug fixes, performance improvements, and
- GUI changes, here are the highlights:
-
- - MIME and other message handling
- o More robustness against badly MIME encapsulated messages: if
- a MessageParseError is raised during the initial parse, the
- message can either be discarded or saved in qfiles/bad,
- depending on the value of the new configuration variable
- QRUNNER_SAVE_BAD_MESSAGES.
-
- o There is a new per-user option that can be used to avoid
- receipt of extra copies, when a member of the list is also
- explicitly CC'd.
-
- o Always add an RFC 2822 Date: header if missing, since not
- all MTAs insert one automatically.
-
- o The Sender: and Errors-To: headers are no longer added to
- outgoing messages.
-
- o Headers and footers are always added by concatenation, if
- the message is not MIME and if the list's charset is a
- superset of us-ascii.
-
- - List administration
- o An `invitation' feature has been added. This is selectable
- as a radio button on the mass subscribe page. When
- selected, users are invited to join instead of immediately
- joined, i.e. they get a confirmation message.
-
- o You can now enable and disable list owner notifications for
- disabled-due-to-bouncing and removal-due-to-bouncing
- actions. The site config variables
- DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE and
- DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL control the default
- behavior.
-
- o List owners can now decide whether they receive unrecognized
- bounce messages or not (i.e. messages that the bounce
- processor doesn't recognize). Site admins can set the
- default value for this flag with the config variable
- DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER.
-
- o The admindb summary page gives the option of clearing the
- moderation flag of members who are on quarantined.
-
- o The action to take when a moderated member posts to a list
- is now configurable. The message can either be held,
- rejected (bounced), or discarded. If the message is
- rejected, a rejection notice string can be given.
-
- o In the General admin page, you can now set the default value
- for five per-user flags: concealing the user's email
- address, acknowledging posts sent by the user, copy
- suppression, not-me-too selection, and the default digest
- type. Site admins can set the default bit field with the
- new DEFAULT_NEW_MEMBER_OPTIONS variable.
-
- o A new "Emergency brake" feature for turning on moderation of
- all list postings. This is useful for when flamewars break
- out, and the list needs a cooling off period. Messages
- containing an Approved: header with the list owner password
- are still allowed through, as are messages approved through
- the admindb interface.
-
- o When a moderated message is approved for the list, add an
- X-Mailman-Approved-At: header which contains the timestamp
- of the approval action (changed from X-Moderated: with a
- different format).
-
- o Lists can now be converted to using a less error prone
- mechanism for variable substitution syntax in headers and
- footers. Instead of %(var)s strings, you'd use $var
- strings. You must use "bin/withlist -r convert" to enable
- this.
-
- o When moderating held messages, the header text box and the
- message excerpt text box are now both read-only.
-
- o You can't delete the site list through the web.
-
- o When creating new lists through the web, you have the option
- of setting the "default member moderation" flag.
-
- - Security and privacy
- o New feature: banned subscription addresses. Privacy
- options/subscription rules now have an additional list box
- which can contain addresses or regular expressions.
- Subscription requests from any matching address are
- automatically rejected.
-
- o Membership tests which compare message headers against list
- rosters are now more robust. They now check, by default
- these header in order: From:, unixfrom, Reply-To:, Sender:.
- If any match, then the membership test succeeds.
-
- o ALLOW_SITE_ADMIN_COOKIES is a new configuration variable
- which says whether to allow AuthSiteAdmin cookies or not.
- Normally, when a list administrator logs into a list with
- the site password, they are issued a cookie that only allows
- them to do administration for this one list. By setting
- ALLOW_SITE_ADMIN_COOKIES to 1, the user only needs to
- authenticate to one list with the site password, and they
- can administer any mailing list.
-
- I'm not sure this feature is wise, so the default value for
- ALLOW_SITE_ADMIN_COOKIES is 0.
-
- o Marc MERLIN's new recipes for secure Linuxes have been
- updated.
-
- o DEFAULT_PRIVATE_ROSTER now defaults to 1.
-
- o Passwords are no longer included in the confirmation pages.
-
- - Internationalization
- o With the approval of Tamito KAJIYAMA, the Japanese codecs
- for Python are now included automatically, so you don't need
- to download and install these separate. It is installed in
- a Mailman-specific place so it won't affect your larger
- Python installation.
-
- o The configure script will produce a warning if the Chinese
- codes are not installed. This is not a fatal error.
-
- o Russian templates and catalogs have been added.
-
- o Finnish templates and catalogs have been added.
-
- - Scripts and utilities
- o New program bin/unshunt to safely move shunted messages back
- into the appropriate processing queue.
-
- o New program bin/inject for sending a plaintext message into
- the incoming queue from the command line.
-
- o New cron script cron/disabled for periodically culling the
- disabled membership.
-
- o bin/list_members has grown some new command line switches
- for filtering on different criteria (digest mode, disable
- mode, etc.)
-
- o bin/remove_members has grown the --fromall switch.
-
- o You can now do a bin/rmlist -a to remove an archive even
- after the list has been deleted.
-
- o bin/update removes the $prefix/Mailman/pythonlib directory.
-
- o bin/withlist grows a --all/-a flag so the --run/-r option
- can be applied to all the mailing lists. Also, interactive
- mode is now the default if -r isn't used. You don't need to
- run this script as "python -i bin/withlist" anymore.
-
- o There is a new script contrib/majordomo2mailman.pl which
- should ease the transition from Majordomo to Mailman.
-
- - MTA integration
- o Postfix integration has been made much more robust, but now
- you have to set POSTFIX_ALIAS_CMD and POSTFIX_MAP_CMD to
- point to the postalias and postmap commands respectively.
-
- o VERP-ish delivery has been made much more efficient by
- eliminating extra disk copies of messages for each recipient
- of a VERP delivery. It has also been made more robust in
- the face of failures during chunk delivery. This required a
- rewrite of SMTPDirect.py and one casualty of that rewrite
- was the experimental threaded delivery. It is no longer
- supported (but /might/ be resurrected if there's enough
- demand -- or a contributed patch :).
-
- o A new site config variable SMTP_MAX_SESSIONS_PER_CONNECTION
- specifies how many consecutive SMTP sessions will be
- conducted down the same socket connection. Some MTAs have a
- limit on this.
-
- o Support for VERP-ing confirmation messages. These are less
- error prone since the Subject: header doesn't need to be
- retained, and they allow a more user friendly (and i18n'd)
- Subject: header. VERP_CONFIRM_FORMAT, VERP_CONFIRM_REGEXP,
- and VERP_CONFIRMATIONS control this feature (only supported
- for invitation confirmations currently, but will be expanded
- to the other confirmations).
-
- o Several new list-centric addresses have been added:
- -subscribe and -unsubscribe are synonyms for -join and
- -leave, respectively. Also -confirm has been added to
- support VERP'd confirmations.
-
- - Archiver
- o There's now a default page for the Pipermail archive link
- for when no messages have yet been posted to the list.
-
- o Just the mere presence of an X-No-Archive: is enough to
- inhibit archiving for this message; the value of the header
- is now ignored.
-
- - Configuring, building, installing
- o Mailman now has a new favicon, donated by Terry Oda. Not
- all web pages are linked to the favicon yet though.
-
- o The add-on email package is now distributed and installed
- automatically, so you don't need to do this. It is
- installed in a Mailman-specific place so it won't affect
- your larger Python installation.
-
- o The default value of VERP_REGEXP has changed.
-
- o New site configuration variables BADQUEUE_DIR and
- QRUNNER_SAVE_BAD_MESSAGES which describe where to save
- messages which are not properly MIME encoded.
-
- o configure should be more POSIX-ly conformant.
-
- o The Mailman/pythonlib directory has been removed, but a new
- $prefix/pythonlib directory has been added.
-
- o Regression tests are now installed.
-
- o The second argument to add_virtual() calls in mm_cfg.py are
- now optional.
-
- o DEFAULT_FIRST_STRIP_REPLY_TO now defaults to 0.
-
- o Site administrators can edit the Mailman/Site.py file to
- customize some filesystem layout policies.
-
-
-2.1 alpha 4 (31-Dec-2001)
-
- - The administrative requests database page (admindb) has been
- redesigned for better usability when there are lots of held
- postings. Changes include:
- o A summary page which groups held messages by sender email
- address. On this page you can dispose of all the sender's
- messages in one action. You can also view the details of
- all the sender's messages, or the details of a single
- message. You can also add the sender to one of the list's
- sender filters.
-
- o A details page where you can view all messages, just those
- for a particular sender, or just a single held message.
- This details page is laid out the same as the old admindb
- page.
-
- o The instructions have been shorted on the summary and
- details page, with links to more detailed explanations.
-
- - Bounce processing
- o Mailman now keeps track of the reason a member's delivery
- has been disabled: explicitly by the administrator,
- explicitly by the user, by the system due to excessive
- bounces, or for (legacy) unknown reasons.
-
- o A new bounce processing algorithm has been implemented (we
- might actually understand this one ;). When an address
- starts bouncing, the member gets a "bounce score". Hard
- (fatal) bounces score 1.0, while soft (transient) bounces
- score 0.5.
-
- List administrators can specify a bounce threshold above
- which a member gets disabled. They can also specify a time
- interval after which, if no bounces are received from the
- member, the member's bounce score is considered stale and is
- thrown away.
-
- o A new cron script, cron/disabled, periodically sends
- notifications to members who are bounce disabled. After a
- certain number of warnings the member is deleted from the
- list. List administrators can control both the number of
- notifications and the amount of time between notifications.
-
- Notifications include a confirmation cookie that the member
- can use to re-enable their subscription, via email or web.
-
- o New configuration variables to support the bounce processing
- are DEFAULT_BOUNCE_SCORE_THRESHOLD,
- DEFAULT_BOUNCE_INFO_STALE_AFTER,
- DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS,
- DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL.
-
- - Privacy and security
- o Sender filters can now be regular expressions. If a line
- starts with ^ it is taken as a (raw string) regular
- expression, otherwise it is a literal email address.
-
- o Fixes in 2.0.8 ported forward: prevent cross-site scripting
- exploits.
-
- - Mail delivery
- o Aliases have all been changed so that there's more
- consistency between the alias a message gets delivered to,
- and the script & queue runner that handles the message.
-
- I've also renamed the mail wrapper script to `mailman' from
- `wrapper' to avoid collisions with other MLM's. You /will/
- need to regenerate your alias files with bin/genaliases, and
- you may need to update your smrsh (Sendmail) configs.a
-
- Bounces always go to listname-bounces now, since
- administration has been separated from bounce processing.
- listname-admin is obsolete.
-
- o VERP support! This greatly improves the accuracy of bounce
- detection. Configuration variables which control this feature
- include VERP_DELIVERY_INTERVAL, VERP_PERSONALIZED_DELIVERIES,
- VERP_PASSWORD_REMINDERS, VERP_REGEXP, and VERP_FORMAT. The
- latter two must be tuned to your MTA.
-
- o A new alias mailman-loop@dom.ain is added which directs all
- output to the file $prefix/data/owner-bounces.mbox. This is
- used when sending messages to the site list owners, as the
- final fallback for bouncing messages.
-
- o New configuration variable POSTFIX_STYLE_VIRTUAL_DOMAINS
- which should be set if you are using the Postfix MTA and
- want Mailman to play nice with Postfix-style virtual
- domains.
-
- - Miscellaneous
- o Better interoperability with Python 2.2.
-
- o MailList objects now record the date (in seconds since
- epoch) that they were created. This is in a hidden
- attribute `created_at'.
-
- o bin/qrunner grows a -s/--subproc switch which is usually
- used only when it's started from mailmanctl.
-
- o bin/newlist grows a -l/--language option so that the list's
- preferred language can be set from the command line.
-
- o cron changes: admin reminders go out at 8am local time instead
- of 10pm local time.
-
- - Pipermail archiver
- o MIME attachments are scrubbed out into separate files which
- can be viewed by following a link in the original article.
- Article contains an indication of the size of the
- attachment, its type, and other useful information.
-
- o New script bin/cleanarch which can be used to `clean' an
- .mbox archive file by fixing unescaped embedded Unix From_
- lines.
-
- o New configuration variable ARCHIVE_SCRUBBER in
- Defaults.py.in which names the module that Pipermail should
- use to scrub articles of MIME attachments.
-
- o New configuration variable ARCHIVE_HTML_SANITIZER which
- describes how the scrubber should handle text/html
- attachments.
-
- o PUBLIC_ARCHIVE_URL has change its semantics. It is now an
- absolute url, with the hostname and listname parts
- interpolated into it on a per-list basis.
-
- o Pipermail should now provide the proper character set in the
- Content-Type: header for archived articles.
-
- - Internationalization
- o Czech translations by Dan Ohnesorg.
-
- o The Hungarian charset has be fixed to be iso-8859-2.
-
- o The member options login page now has a language selection
- widget.
-
- - Building, configuration
- o email-0.96 package is required (see the misc directory).
-
- o New recipes for integrating Mailman and Sendmail,
- contributed by David Champion.
-
-
-2.1 alpha 3 (22-Oct-2001)
-
- - Realname support
- o Mailman now tracks a member's Real Name in addition to their
- email address.
-
- o List members can now supply their Real Names when
- subscribing via the web. Their Real Names are parsed from
- any thru-email subscriptions.
-
- o Members can change their Real Names on their options page,
- and admins can change members' Real Names on the membership
- pages. Mass subscribing accepts "email@dom.ain (Real Name)"
- and "Real Name <email@dom.ain>" entries, for both
- in-text-box and file-upload mass subscriptions.
-
- - Filtering and Privacy
- o Reply-To: munging has been enhanced to allow a wider range
- of list policies. You can now pre-strip any Reply-To:
- headers before adding list-specific ones (i.e. you can
- override or extend existing Reply-To: headers). If
- stripping, the old headers are no longer saved on
- X-Reply-To:
-
- o New sender moderation rules. The old `posters',
- `member_only_posting', `moderated' and `forbidden_posters'
- options have been removed in favor of a new moderation
- scheme. Each member has a personal moderation bit, and
- non-member postings can be automatically accepted, held for
- approval, rejected (bounced) or discarded.
-
- o When membership rosters are private, responses to
- subscription (and other) requests are made more generic so
- that these processes can't be covertly mined for hidden
- addresses. If a subscription request comes in for a user
- who is already subscribed, the user is notified of potential
- membership mining.
-
- o When a held message is approved via the admindb page, an
- X-Moderated: header is added to the message.
-
- o List admins can now set an unsubscribe policy which requires
- them to approve of member unsubscriptions.
-
- - Web U/I
- o All web confirmations now require a two-click procedure,
- where the first click gives them a page that allows them to
- confirm or cancel their subscription. It is bad form for an
- email click (HTTP GET) to have side effects.
-
- o Lots of improvements for clarity.
-
- o The Privacy category has grown three subcategories.
-
- o The General options page as a number of subsection headers.
-
- o The Passwords and Languages categories are now on separate
- admin pages.
-
- o The admin subcategories are now formated as two columns in
- the top and bottom legends.
-
- o When creating a list through the web, you can now specify
- the initial list of supported languages.
-
- o The U/I for unsubscribing a member on the admin's membership
- page should be more intuitive now.
-
- o There is now a separate configuration option for whether the
- goodbye_msg is sent when a member is unsubscribed.
-
- - Performance
- o misc/mailman is a Unix init script, appropriate for
- /etc/init.d, and containing chkconfig hooks for systems that
- support it.
-
- o bin/mailmanctl has been rewritten; the `restart' command
- actually works now. It now also accepts -s, -q, and -u
- options.
-
- o bin/qrunner has been rewritten too; it can serve the role of
- the old cron/qrunner script for those who want classic
- cron-invoked mail delivery.
-
- o Internally, messages are now stored in the qfiles directory
- primarily as pickles. List configuration databases are now
- stored as pickles too (i.e. config.pck). bin/dumpdb knows
- how to display both pickles and marshals.
-
- - Mail delivery
- o If a user's message is held for approval, they are sent a
- notification message containing a confirmation cookie. They
- can use this confirmation cookie to cancel their own
- postings (if they haven't already been approved).
-
- o When held messages are forwarded to an explicit address
- using the admindb page, it is done so in a message/rfc822
- encapsulation.
-
- o When a message is first held for approval, the notification
- sent to the list admin is a 3-part multipart/mixed. The
- first part holds the notification message, the second part
- hold the original message, and the third part hold a cookie
- confirmation message, to which the admin can respond to
- approve or discard the message via email.
-
- o In the mail->news gateway, you can define mail headers that
- must be modified or deleted before the message can be posted
- to the nntp server.
-
- o The list admin can send an immediate urgent message to the
- entire list membership, bypassing digest delivery. This is
- done by adding an Urgent: header with the list password.
- Urgent messages with an invalid password are rejected.
-
- o Lists can now optionally personalize email messages, if the
- site admin allows it. Personalized messages mean that the
- To: header includes the recipient's address instead of the
- list's address, and header and footer messages can contain
- user-specific information. Note that only regular
- deliveries can currently be personalized.
-
- o Message that come from Usenet but that have broken MIME
- boundaries are ignored.
-
- o If the site administrator agrees, list owners have the
- ability to disable RFC 2369 List-* headers.
-
- o There is now an API for an external process to post a
- message to a list. This posting process can also specify an
- explicit list of recipients, in effect turning the mailing
- list into a "virtual list" with a fluid membership. See
- Mailman/Post.py for details.
-
- - Building/testing/configuration
- o mimelib is no longer required, but you must install the
- email package (see the tarball in the misc directory).
-
- o An (as yet) incomplete test suite has been added. Don't try
- running it in a production environment!
-
- o Better virtual host support by adding a mapping from the
- host name given in cgi's HTTP_HOST/SERVER_NAME variable to
- the email host used in list addresses. (E.g. www.python.org
- maps to @python.org).
-
- o Specifying urls to external public archivers is more
- flexible.
-
- o The filters/ subdirectory has been removed.
-
- o There is now a `site list' which is a mailing list that must
- be created first, and from which all password reminders
- appear to come from. It is recommended that this list be
- called "mailman@your.site".
-
- o bin/move_list is no longer necessary (see the FAQ for
- detailed instructions on renaming a list).
-
- o A new script bin/fix_url.py can be used with bin/withlist to
- change a list's web_page_url configuration variable (since
- it is no longer modifiable through the web).
-
- - Internationalization
- o Support for German, Hungarian, Italian, Japanese, and
- Norwegian have been added.
-
- - Miscellaneous
- o Lots of new bounce detectors. Bounce detectors can now
- discard temporary bounce messages by returning a special
- Stop value.
-
- o bin/withlist now sports a -q/--quiet flag.
-
- o bin/add_members has a new -a/--admin-notify flag which can
- be used to inhibit list owner notification for each
- subscription.
-
- - Membership Adaptors
- o Internally, mailing list memberships are accessed through a
- MemberAdaptor interface. This would allow for integrating
- membership databases with external sources (e.g. Zope or
- LDAP), although the only MemberAdaptor currently implemented
- is a "classic" adaptor which stores the membership
- information on the MailList object.
-
- o There's a new pipeline handler module called FileRecips.py
- which could be used to get all regular delivery mailing list
- recipients from a Sendmail-style :include: file (see List
- Extensibility bullet below).
-
- This work was sponsored by Control.com
-
- - List Extensibility
- o A framework has been added which can be used to specialize
- and extend specific mailing lists. If there is a file
- called lists/<yourlist>/extend.py, it is execfile()'d after
- the MailList object is instantiated. The file should
- contain a function extend() which will be called with the
- MailList instance. This function can do all sorts of deep
- things, like modify the handler pipeline just for this list,
- or even strip out particular admin GUI elements (see below).
-
- o All the admin page GUI elements are now separate
- components. This provides greater flexibility for list
- customization. Also, each GUI element will be given an
- opportunity to handle admin CGI form data.
-
- This work was sponsored by Control.com
-
- - Topic Filters
- o A new feature has been added called "Topic Filters". A list
- administrator can create topics, which are essentially
- regular expression matches against Subject: and Keyword:
- headers (including such pseudo-headers if they appear in the
- first few lines of the body of a message).
-
- List members can then `subscribe' to various topics, which
- allows them to filter out any messages that don't match a
- topic, or to filter out any message that does match a
- topic. This can be useful for high volume lists where not
- everyone will be interested in every message.
-
- This work was sponsored by Control.com
-
-2.1 alpha 2 (11-Jul-2001)
-
- - Building
- o mimelib 0.4 is now required. Get it from
- http://mimelib.sf.net. If you've installed an earlier
- version of mimelib, you must upgrade.
-
- o /usr/local/mailman is now the default installation
- directory. Use configure's --prefix switch to change it
- back to the default (/home/mailman) or any other
- installation directory of your choice.
-
- - Security
- o Better definition of authentication domains. The following
- roles have been defined: user, list-admin, list-moderator,
- creator, site-admin.
-
- o There is now a separate role of "list moderator", which has
- access to the pending requests (admindb) page, but not the
- list configuration pages.
-
- o Subscription confirmations can now be performed via email or
- via URL. When a subscription is received, a unique (sha)
- confirm URL is generated in the confirmation message.
- Simply visiting this URL completes the subscription process.
-
- o In a similar manner, removal requests (via web or email
- command) no longer require the password. If the correct
- password is given, the removal is performed immediately. If
- no password is given, then a confirmation message is
- generated.
-
- - Internationalization
- o More I18N patches. The basic infrastructure should now be
- working correctly. Spanish templates and catalogs are
- included, and English, French, Hungarian, and Big5 templates
- are included.
-
- o Cascading specializations and internationalization of
- templates. Templates are now search for in the following
- order: list-specific location, domain-specific location,
- site-wide location, global defaults. Each search location
- is further qualified by the language being displayed. This
- means that you only need to change the templates that are
- different from the global defaults.
-
- Templates renamed: admlogin.txt => admlogin.html
- Templates added: private.html
-
- - Web UI
- o Redesigned the user options page. It now sits behind an
- authentication so user options cannot be viewed without the
- proper password. The other advantage is that the user's
- password need not be entered on the options page to
- unsubscribe or change option values. The login screen also
- provides for password mail-back, and unsubscription w/
- confirmation.
-
- Other new features accessible from the user options page
- include: ability to change email address (with confirmation)
- both per-list and globally for all list on virtual domain;
- global membership password changing; global mail delivery
- disable/enable; ability to suppress password reminders both
- per-list and globally; logout button.
-
- [Note: the handle_opts cgi has gone away]
-
- o Color schemes for non-template based web pages can be defined
- via mm_cfg.
-
- o Redesign of the membership management page. The page is now
- split into three subcategories (Membership List, Mass
- Subscription, and Mass Removal). The Membership List
- subcategory now supports searching for member addresses by
- regular expression, and if necessary, it groups member
- addresses first alphabetically, and then by chunks.
-
- Mass Subscription and Mass Removal now support file upload,
- with one address per line.
-
- o Hyperlinks from the logos in the footers have been removed.
- The sponsors got too much "unsubscribe me!" spam from
- desperate user of Mailman at other sites.
-
- o New buttons on the digest admin page to send a digest
- immediately (if it's non-empty), to start a new digest
- volume with the next digest, and to select the interval with
- which to automatically start a new digest volume (yearly,
- monthly, quarterly, weekly, daily).
-
- DEFAULT_DIGEST_VOLUME_FREQUENCY is a new configuration
- variable, initially set to give a new digest volume monthly.
-
- o Through-the-web list creation and removal, using a separate
- site-wide authentication role called the "list creator and
- destroyer" or simply "list creator". If the configuration
- variable OWNERS_CAN_DELETE_THEIR_OWN_LISTS is set to 1 (by
- default, it's 0), then list admins can delete their own
- lists.
-
- This feature requires an adaptor for the particular MTA
- you're using. An adaptor for Postfix is included, as is a
- dumb adaptor that just emails mailman@yoursite with the
- necessary Sendmail style /etc/alias file changes. Some MTAs
- like Exim can be configured to automatically recognize new
- lists. The adaptor is selected via the MTA option in
- mm_cfg.py
-
- - Email UI
- o In email commands, "join" is a synonym for
- "subscribe". "remove" and "leave" are synonyms for
- "unsubscribe". New robot addresses are support to make
- subscribing and unsubscribing much easier:
-
- mylist-join@mysite
- mylist-leave@mysite
-
- o Confirmation messages have a shortened Subject: header,
- containing just the word "confirm" and the confirmation
- cookie. This should help for MUAs that like to wrap long
- Subject: lines, messing up confirmation.
-
- o Mailman now recognizes an Urgent: header, which, if it
- contains the list moderator or list administrator password,
- forces the message to be delivered immediately to all
- members (i.e. both regular and digest members). The message
- is also placed in the digest. If the password is incorrect,
- the message will be bounced back to the sender.
-
- - Performance
- o Refinements to the new qrunner subsystem which preserves
- FIFO order of messages.
-
- o The qrunner is no longer started from cron. It is started
- by a Un*x init-style script called bin/mailmanctl (see
- below). cron/qrunner has been removed.
-
- - Command line scripts
- o bin/mailmanctl script added, which is used to start, stop,
- and restart the qrunner daemon.
-
- o bin/qrunner script added which allows a single sub-qrunner
- to run once through its processing loop.
-
- o bin/change_pw script added (eases mass changing of list
- passwords).
-
- o bin/update grows a -f switch to force an update.
-
- o bin/newlang renamed to bin/addlang; bin/rmlang removed.
-
- o bin/mmsitepass has grown a -c option to set the list
- creator's password. The site-wide `create' web page is
- linked to from the admin overview page.
-
- o bin/newlist's -o option is removed. This script also grows
- a way of spelling the creation of a list in a specific
- virtual domain.
-
- o The `auto' script has been removed.
-
- o bin/dumpdb has grown -m/--marshal and -p/--pickle options.
-
- o bin/list_admins can be used to print the owners of a mailing list.
-
- o bin/genaliases regenerates from scratch the aliases and
- aliases.db file for the Postfix MTA.
-
- - Archiver
- o New archiver date clobbering option, which allows dates to
- only be clobber if they are outrageously out-of-date
- (default setting is 15 days on either side of received
- timestamp). New configuration variables:
-
- ARCHIVER_CLOBBER_DATE_POLICY
- ARCHIVER_ALLOWABLE_SANE_DATE_SKEW
-
- The archived copy of messages grows an X-List-Received-Date:
- header indicating the time the message was received by
- Mailman.
-
- o PRIVATE_ARCHIVE_URL configuration variable is removed (this
- can be calculated on the fly, and removing it actually makes
- site configuration easier).
-
- - Miscellaneous
- o Several new README's have been added.
-
- o Most syslog entries for the qrunner have been redirected to
- logs/error.
-
- o On SIGHUP, qrunner will re-open all its log files and
- restart all child processes. See "bin/mailmanctl restart".
-
- - Patches and bug fixes
- o SF patches and bug fixes applied: 420396, 424389, 227694,
- 426002, 401372 (partial), 401452.
-
- o Fixes in 2.0.5 ported forward:
- Fix a lock stagnation problem that can result when the
- user hits the `stop' button on their browser during a
- write operation that can take a long time (e.g. hitting
- the membership management admin page).
-
- o Fixes in 2.0.4 ported forward:
- Python 2.1 compatibility release. There were a few
- questionable constructs and uses of deprecated modules
- that caused annoying warnings when used with Python 2.1.
- This release quiets those warnings.
-
- o Fixes in 2.0.3 ported forward:
- Bug fix release. There was a small typo in 2.0.2 in
- ListAdmin.py for approving an already subscribed member
- (thanks Thomas!). Also, an update to the OpenWall
- security workaround (contrib/securelinux_fix.py) was
- included. Thanks to Marc Merlin.
-
-2.1 alpha 1 (04-Mar-2001)
-
- - Python 2.0 or newer required. Also required is `mimelib' a new
- library for handling MIME documents. This will be bundled in
- future releases, but for now, you must download and install it
- (using Python's distutils) from
-
- http://barry.wooz.org/software/Code/mimelib-0.2.tar.gz
-
- You need mimelib 0.2 or better.
-
- - Redesigned qrunner subsystem. Now there are multiple message
- queues, and considerable flexibility in file formats for
- integration with external systems. The current crop of queues
- include:
-
- archive -- for posting messages to an archiver
- commands -- for incoming email commands and bounces
- in -- for list-destined incoming email
- news -- for messages outgoing to a nntp server
- out -- for messages outgoing to a smtp server
- shunt -- for messages that trigger unexpected exceptions in Mailman
- virgin -- for messages that are generated by Mailman
-
- cron/qrunner is now a long running script that forks off
- sub-runners for each of the above queues. qrunner still plays
- nice with cron, but it is expected to be started by init at some
- point in the future. Some support exists for parallel
- processing of messages in the queues.
-
- - Support for internationalization support merged in. Original
- work done by Juan Carlos Rey Anaya and Victoriano Giralt. I've
- tested about 90% of the web side, 50% of the email, and 50% of
- the command line / cron scripts.
-
- New scripts: bin/newlang, bin/rmlang
-
- - New delivery script `auto' for automatic integration with the
- Postfix MTA.
-
- - A bunch of new bounce detectors.
-
- Changes ported from Mailman 2.0.2 and 2.0.1:
-
- - A fix for a potential privacy exploit where a clever list
- administrator could gain access to user passwords. This doesn't
- allow them to do much more harm to the user then they normally
- could, but they still shouldn't have access to the passwords.
-
- - In the admindb page, don't complain when approving a
- subscription of someone who's already on the list (SF bug
- #222409 - Thomas Wouters).
-
- Also, quote for HTML the Subject: text printed for held
- messages, otherwise messages with e.g. "Subject: </table>" could
- royally screw page formatting.
-
- - Docstring fix bin/newlist to remove mention of "immediate"
- argument (Thomas Wouters).
-
- - Fix for bin/update when PREFIX != VAR_PREFIX (SF bug #229794 --
- Thomas Wouters).
-
- - Bug fix release, namely fixes a buglet in bin/withlist affecting
- the -l and -r flags; also a problem that can cause qrunner to
- stop processing mail after disk-full events (SourceForge bug
- 127199).
-
-2.0 final (21-Nov-2000)
-
- No changes from rc3.
-
-2.0 release candidate 3 (16-Nov-2000)
-
- - By popular demand, Reply-To: munging policy is now to always
- override any Reply-To: header in the original message, if
- reply_goes_to_list is set to "This list" or "Explicit Address"
-
- - bin/newlist given -q/--quiet flag instead of the <immediate>
- positional argument
-
- - Hopefully last fix to DEFAULT_URL not ending in a slash
- sensitivity
-
- - 2.0rc2 buglets fixed:
- o newlist argument parsing
- o updating with unlocked lists
- o HyperArch.py traceback when there's no
- Content-Transfer-Encoding: header
-
- - SourceForge bugs fixed:
- 122358 (qmail-to-mailman.py listname case folding)
-
- - SourceForge patches applied:
- 102373 (qmail-to-mailman.py listname case folding)
-
-2.0 release candidate 2 (10-Nov-2000)
-
- - Documentation updates: start in the doc/ directory.
-
- - bin/withlist accepts additional command line arguments when used
- with the --run flag; bin/mmsitepass and bin/newlist accept
- -h/--help flags
-
- - bin/newlist has a -o/--output flag to append /etc/aliases
- suggestions to a specified file
-
- - SourceForge bugs fixed:
- 116615 (README.BSD update), 117015 (duplicate messages on
- moderated posts), 117548 (exception in HyperArch.py), 117682
- (typos), 121185 (vsnprintf signature), 121591 and 122017
- (bogus link after web unsubscribe), 121811 (`subscribe' in
- Subject: doesn't get archived)
-
- - SourceForge patches applied:
- 101812 (securelinux_fix.py contrib), 102097 (fix for bug
- 117548), 102211 (additional args for withlist), 102268 (case
- insensitive Content-Transfer-Encoding:)
-
-2.0 release candidate 1 (23-Oct-2000)
-
- - Bug fixes and security patches.
-
- - Better html rendition of articles in non us-ascii charsets
- (Jeremy Hylton). See VERBATIM_ENCODING variable in
- Defaults.py.in for customization.
-
-2.0 beta 6 (22-Sep-2000)
-
- - Building
- o Tested with Python 1.5.2, Python 1.6, and Python 2.0 beta 1.
- Conducted on RH Linux 6.1 only, but should work
- cross-platform.
-
- o Configure now accepts --with-username, --with-groupname,
- --with-var-prefix flags. See `configure --help' or the
- INSTALL file for details.
-
- o Setting the CFLAGS environment variable before invoking
- configure now works.
-
- o The icons are now copied into $prefix/icons at install time.
- Patch by David Champion.
-
- - Standards
- o Compliance with RFC 2369 (List-*: headers). Patch by
- Darrell Fuhriman. List-ID: header is kept for historical
- reasons.
-
- o Fixes by Jeremy Hylton to Pipermail in support of non-ASCII
- charsets, based on the Content-Type: and encoded-words in
- the original message. Mail headers are now decoded as per
- RFC 2047.
-
- o Many more bounce formats are detected: Microsoft's SMTPSVC,
- Compuserve, GroupWise, SMTP32, and the more generic
- SimpleMatch (which catches lots of similar but slightly
- different formats).
-
- - Defaults
- o Email addresses can now be obscured in Pipermail archives by
- setting mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS to 1 (obscuring
- is turned off by default). Patch provided by Chris Snell.
-
- o The default NNTP host can now be set by editing
- mm_cfg.DEFAULT_NNTP_HOST. Patch by David Champion.
-
- o The default archiving mode (public/private) can now be set
- by editing mm_cfg.DEFAULT_ARCHIVE. Patch by Ted Cabeen.
-
- - Web UI
- o The variable details pages in the administrators interface
- is now `live', i.e. there's a submit button on the details
- page.
-
- o A link to the administrative interface is placed in the
- footer of the general user pages (authentication still
- required, of course!)
-
- o The user options change results page has a link back to the
- user's main page.
-
- o In the admindb page (for dealing with held postings), the
- default forward address is now listname-owner instead of
- listname-admin. This avoids bounce detection on the
- forwarded message.
-
- - Miscellaneous
- o Fixed config.db corruption problem when disk-full errors are
- encountered.
-
- o Command line scripts accept list names case-insensitively.
-
- o bin/remove_members takes a -a flag to remove all members of
- a list in one fell swoop.
-
- o List admin passwords must be non-empty.
-
- o Mailman generated passwords are slightly more mnemonic, and
- shouldn't have confusing character selections (i.e. `i'
- only, but no `1' or `l').
-
- o Crossposting to two gated mailing lists should be fixed.
-
- o Many other bug fixes and minor web UI improvements.
-
-2.0 beta 5 (01-Aug-2000)
-
- - Bug fix release. This includes a fix for a small security hole
- which could be exploited to gain mailman group access by a local
- user (not a mail or web user).
-
- - As part of the fix for the "cookie reauthorization" bug, only
- session cookies are used now. This means that administrative
- and private archive cookies expire only when the browser session
- is quit, however an explicit "Logout" button has been added.
-
-2.0 beta 4 (06-Jul-2000)
-
- - Bug fix release.
-
-2.0 beta 3 (29-Jun-2000)
-
- - Delivery mechanism (qrunner) refined to support immediate
- queuing, queuing directly from MTA, and queuing on any error
- along the delivery pipeline. This means 1) that huge lists
- can't time out the MTA's program delivery channel; 2) it is much
- harder to completely lose messages; 3) eventually, qrunner will
- be elaborated to meter delivery to the MTA so as not to swamp
- it. The tradeoff is in more disk I/O since every message coming
- into the system (and most that are generated by the system) live
- on disk for some part of their journey through Mailman.
-
- For now, see the Default.py variables QRUNNER_PROCESS_LIFETIME
- and QRUNNER_MAX_MESSAGES for primitive resource management.
-
- The API to the pipeline handler modules has changed. See
- Mailman/Handlers/HandlerAPI.py for details.
-
- - Revamped admindb web page: held messages are split into headers
- and bodies so they are easier to vette; admins can now also
- preserve a held message (for spam evidence gathering) or forward
- the message to a specified email address; disposition of held
- messages can be deferred; held messages have a more context
- meaningful default rejection message.
-
- - Change to the semantics for `acceptable_aliases' list
- configuration variable, based on suggestions by Harald Meland.
-
- - New mm_cfg.py variables NNTP_USERNAME and NNTP_PASSWORD can be
- set on a site-wide basis if connection to your nntpd requires
- authentication.
-
- - The list attribute `num_spawns' has been removed. The mm_cfg.py
- variables MAX_SPAWNS, and DEFAULT_NUM_SPAWNS removed too.
-
- - LIST_LOCK_LIFETIME cranked to 5 hours and LIST_LOCK_TIMEOUT
- shortened to 10 seconds. QRUNNER_LOCK_LIFETIME cranked up to 10
- hours. This should decrease the changes for bogus and harmful
- lock breaking.
-
- - Resent-to: is now one of the headers checked for explicit
- destinations.
-
- - Tons more bounce formats are recognized. The API to the bounce
- modules has changed.
-
- - A rewritten LockFile module which should fix most (hopefully all)
- bugs in the locking machinery. Many improvements suggested by
- Thomas Wouters and Harald Meland.
-
- - Experimental support (disabled by default) for delivering SMTP
- chunks to the MTA via multiple threads. Your Python executable
- must have been compiled with thread support enabled, and you
- must set MAX_DELIVERY_THREADS in mm_cfg.py. Note that this may
- not improve your overall system performance.
-
- - Some changes and additions to scripts: bin/find_member now
- supports a -w/--owner flag to match regexps against mailing list
- owners; bin/find_member now supports multiple regexps;
- cron/gate_news command line option changes; new script
- bin/dumbdb for debugging purposes; bin/clone_member can now also
- remove the old address and change change the list owner
- addresses.
-
- - The News/Mail gateway admin page has a button that lets you do
- an explicit catchup of the newsgroup.
-
- - The CVS repository has been moved out to SourceForge. For more
- information, see the project summary at
-
- http://sourceforge.net/project/?group_id=103
-
- - Lots 'o bug fixes and some performance improvements.
-
-2.0 beta 2 (07-Apr-2000)
-
- - Rewritten gate_news cron script which should be more efficient
- and avoid race and locking problems. Each list now maintains
- its own watermark, and when you use the admin CGI script to turn
- on gating from Usenet->mail, an automatic mass catch up is done
- to avoid flooding the mailing list. cron/gate_news's command
- line interface has also changed. See its docstring for
- details.
-
- - A new cron script called qrunner has been added to retry message
- deliveries that fail because of temporary smtpd problems.
-
- - New command line script called bin/list_lists which does exactly
- that: lists all the mailing lists on the system (much like the
- listinfo CGI does).
-
- - bin/withlist is now directly executable, however if you want to
- use python -i, you must still explicitly invoke it.
- bin/withlist also now cleans up after itself by unlocking any
- locked lists. It does NOT save any dirty lists though - you
- must do this explicitly.
-
- - $prefix permissions (and all subdirs) must now be 02775.
- bin/check_perms has been updated to fix all the subdir
- permissions.
-
- - "make update" (a.k.a. bin/update) is run automatically when you
- do a "make install"
-
- - The CGI driver script now puts information about the Python
- environment into the logs/error file (but not the diagnostic web
- page).
-
- - Bug fixes and some performance improvements
-
-2.0 beta 1 (19-Mar-2000)
-
- - Python 1.5.2 (or newer) is now required.
-
- - A new bundled auto-responder has been added. You can now
- configure an autoresponse text for each list's primary
- addresses:
-
- listname@yourhost.com -- the general posting address
- listname-request@... -- the automated "request bot" address
- listname-admin@... -- the human administrator address
-
- - The standard UI now includes three logos at the bottom of the
- page: Dragon's Mailman logo, the Python Powered logo, and the
- GNU logo. All point to their respective home pages.
-
- - It is now possible to set the Reply-To: field on lists to an
- arbitrary address. NOTE: Reply-To: munging is generally
- considered harmful! However for some read-only lists, it is
- useful to direct replies to a parallel discussion list.
-
- - There is a new message delivery architecture which uses a
- pipeline processor for incoming and internally generated
- messages. Mailman no longer contains a bundled bulk-mailer;
- instead message delivery is handled completely by the MTA. Most
- MTAs give a high enough priority to connections from the
- localhost that mail will not be lost because of system load, but
- this is not guaranteed (or handled) by Mailman currently. Be
- careful also if your smtpd is on a different host than the
- Mailman host. In practice, mail lossage has not be observed.
-
- For this reason cron/run_queue is no longer needed (see the
- UPGRADING file for details).
-
- Also, you can choose whether you want direct smtp delivery, or
- delivery via the command line to a sendmail-compatible daemon.
- You can also easily add your own delivery module. See
- Mailman/Defaults.py for details.
-
- - A similar pipeline architecture for the parsing of bounce
- messages has been added. Most common bounce formats are now
- handled, including Qmail, Postfix, and DSN. It is now much
- easier to add new bounce detectors.
-
- - The approval pending architecture has also been revamped.
- Subscription requests and message posts waiting for admin
- approval are no longer kept in the config.db file, but in a
- separate requests.db file instead.
-
- - Finally made consistent the use of Sender:/From:/From_ in the
- matching of headers for such things as member-post-only. Now,
- if USE_ENVELOPE_SENDER is true, Sender: will always be chosen
- over From:, however the default has been changed to
- USE_ENVELOPE_SENDER false so that From: is always chosen over
- Sender:. In both cases, if no header is found, From_ (i.e. the
- envelope sender is used). Note that the variable is now
- misnamed! Most people want From: matching anyway and any are
- easily spoofable.
-
- - New scripts bin/move_list, bin/config_list
-
- - cron/upvolumes_yearly, cron/upvolumes_monthly, cron/archive,
- cron/run_queue all removed. Edit your crontab if you used these
- scripts. Other scripts removed: contact_transport, deliver,
- dumb_deliver.
-
- - Several web UI improvements, especially in the admin page.
-
- - Remove X-pmrqc: headers to prevent return reciepts for Pegasus
- mail users.
-
- - Security patch when using external archivers.
-
- - Honor "X-Archive: No" header by not putting this message in the
- archive.
-
- - Changes to the log file format.
-
- - The usual bug fixes.
-
-1.1 (05-Nov-1999)
-
- - All GIFs removed. See http://www.gnu.org/philosophy/gif.html
- for the reason why.
-
- - Improvements to the Pipermail archiver which make things faster.
- Primary change is that the .txt files are not gzipped on every
- posted message. Instead, use the new cron script `nightly_gzip'
- to gzip the .txt file in batches (this means that the .txt file
- will lag behind the on-line archives a little).
-
- - From the C drivers programs, Python is invoked with the -S
- option. This tells Python to avoid importing the site module,
- which can improve start up time of the Python process
- considerably. Note that the command line script invocation has
- not been changed.
-
- - New configuration variables PUBLIC_EXTERNAL_ARCHIVER and
- PRIVATE_EXTERNAL_ARCHIVER which can contain a shell command
- string for os.popen(). This can be used to invoke an external
- archiver instead of the bundled Pipermail archiver. See
- Defaults.py for details.
-
- - new script `bin/find_member' which can be used to search for a
- member by regular expression.
-
- - More child processes are reaped, which should eliminate most
- occurrences of zombie processes.
-
- - A few small miscellaneous bug fixes (including PR#99, PR#107)
- and improvements to the file locking algorithms.
-
-1.0 (30-Jul-1999)
-
- - Configure script now allows $PREFIX (by default /home/mailman)
- to be permissions 02755. Also, configure now tests for
- vsnprintf()
-
- - Workaround, taken from GNU screen, for systems missing
- vsnprintf()
-
- - Return-Receipt-To: and Disposition-Notification-To: headers are
- always removed from posted messages (they can be used to troll
- for list membership).
-
- - Workaround for MSIE4.01 (and possibly other versions) bug in the
- handling of cookies.
-
- - A small collection of other bug fixes.
-
-1.0rc3 (10-Jul-1999)
-
- - new script bin/check_perms which checks (and optionally fixes)
- the permissions and group ownerships of the files in your
- Mailman installation.
-
- - Removed a bottleneck in the archiving code that was causing
- performance problems on highly loaded servers.
-
- - The code that saves a list's state and configuration database
- has been made more robust.
-
- - Additional exception handlers have been added in several places
- to alleviate problems with Mailman bombing out when it really
- would be better to print/log a helpful message.
-
- - The "password" mail command will now mail back the sender's
- subscription password when given with no arguments.
-
- - The embarrassing subject-prefixing bug present in rc2 has been
- fixed.
-
- - A small (but nice :) collection of other squashed bugs.
-
-1.0rc2 (14-Jun-1999)
-
- - A security flaw in the CGI cookie mechanisms was discovered --
- the Mailman-issued cookies were easily spoofable, implying that
- e.g. admin access to all Mailman lists via the web interface
- could be compromised. This flaw has now been fixed.
-
- - Handling of SMTP errors has been improved.
-
- - Both "Mass Subscription" via web admin interface and
- bin/add_members have been greatly sped up.
-
- - autoconf check for syslog has been revamped, and is now verified
- to work on SCO OpenServer 5. If syslog can't be found, the C
- wrappers will compile, but without any syslog calls.
-
- - Various other bug fixes.
-
-1.0rc1 (04-May-1999)
-
- - There is a new Mailman logo, contributed by The Dragon De
- Monsyne. Please read the INSTALL file for information about
- installing the logo in a place your Web server can find it.
-
- - USE_ENVELOPE_SENDER is now set to 0 by default. Turning this on
- caused problems for too many users; lists restricted to
- member-only posts were not matching the addresses correctly.
-
- - A revamped bin/withlist to be a little more useful.
-
- - A revamped cron/mailpasswds which groups users by virtual hosts.
-
- - The usual assortment of bug fixes.
-
-1.0b11 (03-Apr-1999)
-
- - Bug fixes and improvements for case preservation of subscribed
- addresses. The DATA_FILE_VERSION has been bumped to 14.
-
- - New script bin/withlist, useful for interactive debugging.
-
-1.0b10 (26-Mar-1999)
-
- - New script bin/sync_members which can be used to synchronize a
- list's membership against a flat (e.g. sendmail :include: style)
- file.
-
- - bin/add_members and bin/remove_members now accept addresses on
- the command line with `-' as the value for the -d and -n
- options.
-
- - Added variable USE_ENVELOPE_SENDER to Defaults.py for site-wide
- configuration of address matching scheme. With this variable
- set to true, the envelope sender (e.g. Unix "From_" header) is
- used to match addresses, otherwise the From: header is used.
- Envelope sender matching seems not to work on many systems.
- This variable is currently defaulted to 1, but may change to 0
- for the final release.
-
- - Reorganization of the membership management admin page. Also
- member addresses are linked to their options page. Only the
- `General' category has the admin password change form.
-
- - Major reorganization of email command handling and responses.
- `notmetoo' is the preferred email command instead of `norcv',
- although the latter is still accepted as an argument. If more
- than 5 errors are found in the message, command processing is
- halted.
-
- - User options page now shows the user their case-preserved
- subscribed address as well.
-
- - The usual assortment of bug fixes.
-
-1.0b9 (01-Mar-1999)
-
- - New bin scripts: clone_member, list_members, add_members (a
- consolidation of convertlist and populate_new_list which have
- been removed).
-
- - Two new readmes have been added: README.LINUX and README.QMAIL
-
- - New configure option --with-cgi-ext which can be used if your
- Web server requires extensions on CGI scripts. The extension
- must include a dot (e.g. --with-cgi-ext=".cgi").
-
- - Many bug fixes, including the setgid problem that was causing
- mail to be lost on some versions of Linux.
-
-1.0b8 (14-Jan-1999)
-
- - Bug fixes and workarounds for certain Linuxes.
-
- - Illegal addresses are no longer allowed to be subscribed, from
- any interface.
-
-1.0b7 (31-Dec-1998)
-
- - Many, many bug fixes. Some performance improvements for large
- lists. Some improvements in the Web interfaces. Some security
- improvements. Improved compatibility with Python 1.5.
-
- - bin/convert_list and bin/populate_new_list have been replaced
- by bin/add_members.
-
- - Admins can now get notification on subscriptions and
- unsubscriptions. Posts are now logged.
-
- - The username portion of email addresses are now case-preserved
- for delivery purposes. All other address comparisions are
- case-insensitive.
-
- - New default SMTP_MAX_RCPTS that limits the number of "RCPT TO"
- SMTP commands that can be given for a single message. Most
- MTAs have some hard limit.
-
- - "Precedence: bulk" header and "List-id:" header are now added
- to all outgoing messages. The latter is not added if the
- message already has a "List-id:" header. See RFC 2046 and
- draft-chandhok-listid-02 for details.
-
- - The standard (as of Python 1.5.2) smtplib.py is now used.
-
- - The install process now compiles all the .py files in the
- installation.
-
- - Versions of the Mailman papers given at IPC7 and LISA-98 are
- now included.
-
-1.0b6 (07-Nov-1998)
-
- - Archiving is (finally) back in.
-
- - Administrivia filter added.
-
- - Mail queue mechanism revamped with better concurrency control.
-
- - For recipients that have estmp MTAs, set delivery notification
- status so that only delivery failure notices are sent out,
- inhibiting 4 hour and N day warning notices.
-
- - Now expire old unconfirmed subscription requests, rather than
- keeping them forever.
-
- - Added proposed standard List-Id: header, and our own
- X-MailmanVersion header.
-
- - Prevent havoc from attempts to subscribe a list to itself. (!)
-
- - Refine mail command processing to prevent loops.
-
- - Pending subscription DB redone with better locking and cleaner
- interface.
-
- - posters functionality expanded.
-
- - Subscription policy more flexible, sensible, and
- site-configurable.
-
- - Various and sundry bug fixes.
-
-1.0b5 (27-Jul-1998)
-
- - New file locking that should be portable and work w/ NFS.
-
- - Better use of packages.
-
- - Better error logging and reporting.
-
- - Less startup overhead.
-
- - Various and sundry bug fixes.
-
-
-1.0b4 (03-Jun-1998)
-
- - A configure script for easy installation (Barry Warsaw)
-
- - The ability to install Mailman to locations other than
- /home/mailman (Barry Warsaw)
-
- - Use cookies on the admin pages (also hides admin pages from
- others) (Scott Cotton)
-
- - Subscription requests send a request for confirmation, which may
- be done by simply replying to the message (Scott Cotton)
-
- - Facilities for gating mail to a newsgroup, and for gating a
- newsgroup to a mailing list (John Viega)
-
- - Contact the SMTP port instead of calling sendmail (primarily for
- portability) (John Viega)
-
- - Changed all links on web pages to relative links where appropriate.
- (John Viega)
-
- - Use MD5 if crypt is not available (John Viega)
-
- - Lots of fixing up of bounce handling (Ken Manheimer)
-
- - General UI polishing (Ken Manheimer)
-
- - mm_html: Make it prominent when the user's delivery is disabled
- on his option page. (Ken Manheimer)
-
- - mallist:DeleteMember() Delete the option setings if any. (Ken
- Manheimer)
-
-1.0b3 (03-May-1998)
-
- - mm_message:Deliverer.DeliverToList() added missing newline
- between the headers and message body. Without it, any sequence
- of initial body lines that _looked_ like headers ("Sir: Please
- excuse my impertinence, but") got treated like headers.
-
- - Fixed typo which broke subscription acknowledgement message
- (thanks to janne sinkonen for pointing this out promptly after
- release). (Anyone who applied my intermediate patch will
- probably see this one trigger patch'es reversed-patch
- detector...)
-
- - Fixed cgi-wrapper.c so it doesn't segfault when invoked with
- improper uid or gid, and generally wrappers are cleaned up a
- bit.
-
- - Prevented delivery-failure notices for misdirected subscribe-
- confirmation requests from bouncing back to the -request addr,
- and then being treated as failing requests.
-
- Implemented two measures. Set the reply-to for the
- confirmation- request to the -request addr, and the sender to be
- the list admin. This way, bounces go to list admin instead of
- to -request addr. (Using the errors-to header wasn't
- sufficient. Thanks, barry, for pointing out the use of sender
- here.) Second, ignore any mailcommands coming from postmaster
- or non-login system type accounts (mailer-daemon, daemon,
- postoffice, etc.)
-
- - Reenabled admin setting of web_page_url - crucial for having
- lists use alternate names of a host that occupies multiple
- addresses.
-
- - Fixed and refined admin-options help mechanism. Top-level visit
- to general-category (where the "general" isn't in the URL) was
- broken. New help presentation shows the same row that shows on
- the actual options page.
-
- - cron/crontab.in crontab template had wrong name for senddigests.
-
- - Default digest format setting, as distributed, is now non-MIME,
- on urging of reasoned voices asserting that there are still
- enough bad MIME implementations in the world to be a nuisance to
- too many users if MIME is the default. Sigh.
-
- - MIME digests now preserve the structure of MIME postings,
- keeping attachments as attachments, etc. They also are more
- structured in general.
-
- - Added README instructions explaining how to determine the right
- UID and GID settings for the wrapper executables, and improved
- some of the explanations about exploratory interaction
- w/mailman.
-
- - Removed the constraint that subscribers have their domain
- included in a static list in the code. We might want to
- eventually reincorporate the check for the sake of a warning
- message, to give a heads up to the subscriber, but try delivery
- anyway...
-
- - Added missing titles to error docs.
-
- - Improved several help details, including particularly explaining
- better how real_name setting is used.
-
- - Strengthened admonition against setting reply_goes_to_list.
-
- - Added X-BeenThere header to postings for the sake of prevention
- of external mail loops.
-
- - Improved handling of bounced messages to better recognize
- members address, and prevent duplicate attempts to react (which
- could cause superfluous notices to administrator).
-
- - Added __delitem__ method to mm_message.OutgoingMessage, to fix
- the intermediate patch posted just before this one.
-
- - Using keyword substitution format for more message text (ie,
- "substituting %(such)s into text" % {'such': "something"}) to
- make the substitutions less fragile and, presumably, easier to
- debug.
-
- - Removed hardwired (and failure-prone) /tmp file logging from
- answer.majordomo_mail, and generally spiffed up following janne
- sinkkonen's lead.
-
-1.0b2 (13-Apr-1998)
-1.0b1 (09-Apr-1998)
-
- Web pages much more polished
- - Better organized, text more finely crafted
- - Easier, more refined layout
- - List info and admin interface overviews, enumerate all public lists
- (via, e.g., http://www.python.org/mailman/listinfo - sans the
- specific list)
- - Admin interface broken into sections, with help elaboration for
- complicated configuration options
-
- Mailing List Archives
- - Integrated with a newer, *much* improved, external pipermail - to be
- found at http://starship.skyport.net/crew/amk/maintained/pipermail.html
- - Private archives protected with mailing list members passwords,
- cookie-fied.
-
- Spam prevention
- - New spam prevention measures catch most if not all spam without
- operator intervention or general constraints on who can post to
- list:
- require_explicit_destination option imposes hold of any postings
- that do not have the list name in any of the to or cc header
- destination addresses. This catches the vast majority of random
- spam.
- Other options (forbidden_posters, bounce_matching_headers) provide
- for filtering of known transgressors.
- - Option obscure_addresses (default on) causes mailing list subscriber
- lists on the web to be slightly mangled so they're not directly
- recognizable as email address by web spiders, which might be
- seeking targets for spammers.
-
- Site configuration arrangement organized - in mailman/mailman/modules:
- - When installing, create a mailman/modules/mm_cfg.py (if there's not
- one already there), using mm_cfg.py.dist as a template.
- mm_default.py contains the distributed defaults, including
- descriptions of the values. mm_cfg.py does a 'from mm_defaults.py
- import *' to get the distributed defaults. Include settings in
- mm_cfg.py for any values in mm_defaults.py that need to be
- customized for your site, after the 'from .. import *'.
- See mm_cfg.py.dist for more details.
-
- Logging
- - Major operations (subscription, admin approval, bounce,
- digestification, cgi script failure tracebacks) logged in files
- using a reliable mechanism
- - Wrapper executables log authentication complaints via syslog
-
- Wrappers
- - All cgi-script wrapper executables combined in a single source,
- easier to configure. (Mail and aliases wrappers separate.)
-
- List structure version migration
- - Provision for automatic update of list structures when moving to a
- new version of the system. See modules/versions.py.
-
- Code cleaning
- - Many more module docstrings, __version__ settings, more function
- docstrings.
- - Most unqualified exception catches have been replaced with more
- finely targeted catches, to avoid concealing bugs.
- - Lotsa long lines wrapped (pet peeve:).
-
- Random details (not complete, sorry):
- - make archival frequency a list option
- - Option for daily digest dispatch, in addition to size threshhold
- - make sure users only get one periodic password notifcation message for
- all the lists they're on (repaired 1.0b1.1 varying-case mistake)
- - Fix rmlist sans-argument bug causing deletion of all lists!
- - doubled generated random passwords to four letters
- - Cleaned lots and lots of notices
- - Lots and lots of html page cleanup, including table-of-contents, etc
- - Admin options sections - don't do the "if so" if the ensuing list
- is empty
- - Prevent list subject-prefix cascade
- - Sources under CVS
- - Various spam filters - implicit-destination, header-field
- - Adjusted permissions for group access
- - Prevent redundant subscription from redundant vetted requests
- - Instituted centralize, robustish logging
- - Wrapper sources use syslog for logging (john viega)
- - Sorting of users done on presentation, not in list.
- - Edit options - give an error for non-existent users, not an options page.
- - Bounce handling - offer 'disable' option, instead of remove, and
- never remove without notifying admin
- - Moved subscribers off of listinfo (and made private lists visible
- modulo authentication)
- - Parameterize default digest headers and footers and create some
- - Put titles on cgi result pages that do not get titles (all?)
- - Option for immediate admin notifcation via email of pending
- requests, as well as periodic
- - Admin options web-page help
- - Enabled grouped and cascading lists despite implicit-name constraint
- - Changed subscribers list so it has its own script (roster)
- - Welcome pages: http://www.python.org/mailman/{admin,listinfo}/
-
-0.95 (25-Jan-1997)
- - Fixed a bug in sending out digests added when adding disable mime option.
- - Added an option to not notify about bounced posts.
- - Added hook for pre-posting filters. These could be used to
- auto-strip signatures. I'm using the feature to auto-strip footers
- that are auto-generated by mail received from another mailing list.
-
-0.94 (22-Jan-1997)
- - Made admin password work ubiquitously in place of a user password.
- - Added an interface for getting / setting user options.
- - Added user option to disable mime digests (digested people only)
- - Added user option to not receive your own posts (nondigested people only)
- - Added user option to ack posts
- - Added user option to disable list delivery to their box.
- - Added web interface to user options
- - Config number of sendmail spawns on a per-list basis
- - Fixed extra space at beginning of each message in digests...
- - Handled comma separated emails in bounce messages...
- - Added a FindUser() function to MailList. Used it where appropriate.
- - Added mail interface to setting list options.
- - Added name links to the templates options page
- - Added an option so people can hide their names from the subscription list.
- - Added an answer_majordomo_mail script for people switching...
-
-0.93 (18/20-Jan-1997)
- - When delivering to list, don't call sendmail directly. Write to a file,
- and then run the new deliver script, which forks and exits in the parent
- immediately to avoid hanging when delivering mail for large lists, so that
- large lists don't spend a lot of time locked.
- - GetSender() no longer assumes that you don't have an owner-xxx address.
- - Fixed unsubscribing via mail.
- - Made subscribe via mail generate a password if you don't supply one.
- - Added an option to clobber the date in the archives to the date the list
- resent the post, so that the archive doesn't get mail from people sending
- bad dates clumped up at the beginning or end.
- - Added automatic error message processing as an option. Currently
- logging to /tmp/bounce.log
- - Changed archive to take a list as an argument, (the old way was broken)
- - Remove (ignore) spaces in email addresses
- - Allow user passwords to be case insensitive.
- - Removed the cleanup script since it was now redundant.
- - Fixed archives if there were no archives.
- - Added a Lock() call to Load() and Create(). This fixes the
- problem of loading then locking.
- - Removed all occurances of Lock() except for the ones in mailing
- list since creating a list
- now implicitly locks it.
- - Quote single periods in message text.
- - Made bounce system handle digest users fairly.
-
-0.92 (13/16-Jan-1997)
- - Added Lock and Unlock methods to list to ensure each operation is atomic
- - Added a cmd that rms all files of a mailing list (but not the aliases)
- - Fixed subscribing an unknown user@localhost (confirm this)
- - Changed the sender to list-admin@... to ensure we avoid mail loops.
- - check to make sure there are msgs to archive before calling pipermail.
- - started using this w/ real mailing lists.
- - Added a cron script that scours the maillog for User/Host unknown errs
- - Sort membership lists
- - Always display digest_is_default option
- - Don't slam the TO list unless you're sending a digest.
- - When making digest summaries, if missing sender name, use their email.
- - Hacked in some protection against crappy dates in pipermail.py
- - Made it so archive/digest volumes can go up monthly for large large lists.
- - Number digest messages
- - Add headers/footers to each message in digest for braindead mailers
- - I removed some forgotten debug statements that caused server errors
- when a CGI script sent mail.
- - Removed loose_matches flag, since everything used it.
- - Fixed a problem in pipermail if there was no From line.
- - In upvolume_ scripts, remove INDEX files as we leave a volume.
- - Threw a couple of scripts in bin for generating archives from majordomo's
- digest-archives. I wouldn't recommend them for the layman, though, they
- were meant to do a job quickly, not to be usable.
-
-0.91 (23-Dec-1996)
- - broke code into mixins for managability
- - tag parsing instead of lots of gsubs
- - tweaked pipermail (see comments on pipermail header)
- - templates are now on a per-list basis as intended.
- - request over web that your password be emailed to you.
- - option so that web subscriptions require email confirmation.
- - wrote a first pass at an admin interface to configurable variables.
- - made digests mime-compliant.
- - added a FakeFile class that simulates enough of a file object on a
- string of text to fool rfc822.Message in non-seek mode.
- - changed OutgoingMessage not to require its args in constructor.
- - added an admin request DB interface.
- - clearly separated the internal name from the real name.
- - replaced lots of ugly, redundant code w/ nice code.
- (added Get...Email() interfaces, GetScriptURL, etc...)
- - Wrote a lot of pretty html formatting functions / classes.
- - Fleshed out the newlist command a lot. It now mails the new list
- admin, and auto-updates the aliases file.
- - Made multiple owners acceptable.
- - Non-advertised lists, closed lists, max header length, max msg length
- - Allowed editing templates from list admin pages.
- - You can get to your info page from the web even if the list is closed.
-
-
-Local Variables:
-mode: indented-text
-indent-tabs-mode: nil
-End:
diff --git a/src/attic/docs/man/add_members.1 b/src/attic/docs/man/add_members.1
deleted file mode 100644
index d442a2b66..000000000
--- a/src/attic/docs/man/add_members.1
+++ /dev/null
@@ -1,60 +0,0 @@
-.\"
-.\" GNU Mailman Manual
-.\"
-.\" add_members
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 12, 2004
-.\" Last Updated: September 12, 2004
-.\"
-.TH add_members 1 "September 12, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-add_members \- Remove members from a Mailman mailing list.
-.\"=====================================================================
-.SH SYNOPSIS
-.B add_members
-[-r \fIfile\fP]
-[-d \fIfile\fP]
-[-w <\fIy|n\fP>]
-[-a <\fIy|n\fP>]
-[-h]
-\fIlistname\fP
-.\"=====================================================================
-.SH DESCRIPTION
-.B add_members
-adds members to a Mailman mailing list from the command line.
-.PP
-You must supply at least one of -r and -d options. At most one of the
-files can be `-'.
-.\"=====================================================================
-.SH OPTIONS
-.IP "--regular-members-file=\fIfile\fP, -r \fIfile\fP"
-A file containing addresses of the members to be added, one
-address per line. This list of people become non-digest
-members. If file is `-', read addresses from stdin. Note that
--n/--non-digest-members-file are deprecated synonyms for this option.
-.IP "--digest-members-file=\fIfile\fP, -d \fIfile\fP"
-Similar to above, but these people become digest members.
-.IP "--welome-msg=<\fIy|n\fP>, -w <\fIy|n\fP>"
-Set whether or not to send the list members a welcome message,
-overriding whatever the list's `send_welcome_msg' setting is.
-.IP "--admin-notify=<\fIy|n\fP>, -a <\fIy|n\fP>"
-Set whether or not to send the list administrators a notification on
-the success/failure of these subscriptions, overriding whatever the
-list's `admin_notify_mchanges' setting is.
-.IP \fIlistname\fP
-The name of the list to which you wish to add members.
-.\"=====================================================================
-.SH SEE ALSO
-.BR clone_member (1),
-.BR find_member (1),
-.BR list_members (1),
-.BR remove_members (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/check_db.1 b/src/attic/docs/man/check_db.1
deleted file mode 100644
index 28a3b8149..000000000
--- a/src/attic/docs/man/check_db.1
+++ /dev/null
@@ -1,60 +0,0 @@
-.\"
-.\" GNU Mailman Manual
-.\"
-.\" check_db
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 14, 2004
-.\" Last Updated: September 14, 2004
-.\"
-.TH check_db 1 "September 14, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-check_db \- Check a Mailman mailing list's config database file for integrity.
-.\"=====================================================================
-.SH SYNOPSIS
-.B check_db
-[-a]
-[-v]
-[-h]
-[\fIlistname\fP [\fIlistname\fP ...]]
-.\"=====================================================================
-.SH DESCRIPTION
-.B check_db
-checks a list's config database file for integrity.
-.PP
-All of the following files are checked:
-.RS
- config.pck
- config.pck.last
- config.db
- config.db.last
- config.safety
-.RE
-.PP
-It's okay if any of these are missing. config.pck and config.pck.last are
-pickled versions of the config database file for 2.1a3 and beyond. config.db
-and config.db.last are used in all earlier versions, and these are Python
-marshals. config.safety is a pickle written by 2.1a3 and beyond when the
-primary config.pck file could not be read.
-.\"=====================================================================
-.SH OPTIONS
-.IP "--all, -a"
-Check the databases for all lists. Otherwise only the lists named on
-the command line are checked.
-.IP "--verbose, -v"
-Verbose output. The state of every tested file is printed.
-Otherwise only corrupt files are displayed.
-.IP "--help, -h"
-Print help text and exit.
-.\"=====================================================================
-.SH SEE ALSO
-.BR check_perms (1),
-.BR transcheck (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/check_perms.1 b/src/attic/docs/man/check_perms.1
deleted file mode 100644
index 76966a87c..000000000
--- a/src/attic/docs/man/check_perms.1
+++ /dev/null
@@ -1,46 +0,0 @@
-.\"
-.\" GNU Mailman Manual
-.\"
-.\" check_perms
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 14, 2004
-.\" Last Updated: September 14, 2004
-.\"
-.TH check_perms 1 "September 14, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-check_perms \- Check the permissions for the Mailman installation.
-.\"=====================================================================
-.SH SYNOPSIS
-.B check_perms
-[-f]
-[-v]
-[-h]
-.\"=====================================================================
-.SH DESCRIPTION
-.B check_perms
-checks the permissions for the Mailman installation.
-.PP
-With no arguments, just check and report all the files that have bogus
-permissions or group ownership. With -f (and run as root), fix all the
-permission problems found. With -v be verbose.
-.\"=====================================================================
-.SH OPTIONS
-.IP "-f"
-Run as root and fix all the permission problems found.
-.IP "-v"
-Verbose output.
-.IP "--help, -h"
-Print help message and exit.
-.\"=====================================================================
-.SH SEE ALSO
-.BR check_db (1),
-.BR transcheck (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/clone_member.1 b/src/attic/docs/man/clone_member.1
deleted file mode 100644
index 35148f6af..000000000
--- a/src/attic/docs/man/clone_member.1
+++ /dev/null
@@ -1,71 +0,0 @@
-.\"
-.\" GNU Mailman Manual
-.\"
-.\" clone_member
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 14, 2004
-.\" Last Updated: September 14, 2004
-.\"
-.TH clone_member 1 "September 14, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-clone_member \- Clone a Mailman mailing list member address.
-.\"=====================================================================
-.SH SYNOPSIS
-.B clone_member
-[-l \fIlistname\fP]
-[-r]
-[-a]
-[-q]
-[-n]
-[-h]
-\fIfromoldaddr\fP \fItonewaddr\fP
-.\"=====================================================================
-.SH DESCRIPTION
-.B clone_member
-clones a member address.
-.PP
-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.
-.PP
-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.
-.\"=====================================================================
-.SH OPTIONS
-.IP "--listname=\fIlistname\fP, -l \fIlistname\fP"
-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.
-.IP "--remove, -r"
-Remove the old address from the mailing list after it's been cloned.
-.IP "--admin, -a"
-Scan the list admin addresses for the old address, and clone or change
-them too.
-.IP "--quiet, -q"
-Do the modifications quietly.
-.IP "--nomodify, -n"
-Print what would be done, but don't actually do it. Inhibits the
---quiet flag.
-.IP "--help, -h"
-Print help message and exit.
-.IP \fIfromoldaddr\fP
-(`from old address') is the old address of the user.
-.IP \fItonewaddr\fP
-(`to new address') is the new address of the user.
-.\"=====================================================================
-.SH SEE ALSO
-.BR add_member (1),
-.BR find_member (1),
-.BR list_members (1),
-.BR remove_members (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/find_member.1 b/src/attic/docs/man/find_member.1
deleted file mode 100644
index 9d76bf5db..000000000
--- a/src/attic/docs/man/find_member.1
+++ /dev/null
@@ -1,64 +0,0 @@
-add.\"
-.\" GNU Mailman Manual
-.\"
-.\" find_member
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 13, 2004
-.\" Last Updated: September 13, 2004
-.\"
-.TH find_member 1 "September 13, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-find_member \- Find all Mailman mailing lists to which a given address is
-subscribed.
-.\"=====================================================================
-.SH SYNOPSIS
-.B find_member
-[-l \fIlistname\fP]
-[-x \fIlistname\fP]
-[-w]
-[-h]
-\fIregex\fP
-.\"=====================================================================
-.SH DESCRIPTION
-.B find_member
-finds all Mailman mailing lists to which a member's address is subscribed.
-.PP
-The interaction between -l and -x is as follows. If any -l option is given
-then only the named list will be included in the search. If any -x option is
-given but no -l option is given, then all lists will be search except those
-specifically excluded.
-.PP
-Regular expression syntax is Perl5-like, using the Python re module. Complete
-specifications are at:
-.PP
-http://www.python.org/doc/current/lib/module-re.html
-.PP
-Address matches are case-insensitive, but case-preserved addresses are
-displayed.
-.\"=====================================================================
-.SH OPTIONS
-.IP "--listname=\fIlistname\fP, -l \fIlistname\fP"
-Include only the named list in the search.
-.IP "--exclude=\fIlistname\fP, -x \fIlistname\fP"
-Exclude the named list from the search.
-.IP "--owners, -w"
-Search list owners as well as members.
-.IP "--help, -h"
-Print help message and exit.
-.IP \fIregex\fP
-A Python regular expression to match.
-.\"=====================================================================
-.SH SEE ALSO
-.BR add_members (1),
-.BR clone_member (1),
-.BR list_members (1),
-.BR remove_members (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/list_members.1 b/src/attic/docs/man/list_members.1
deleted file mode 100644
index cbf338a40..000000000
--- a/src/attic/docs/man/list_members.1
+++ /dev/null
@@ -1,78 +0,0 @@
-add.\"
-.\" GNU Mailman Manual
-.\"
-.\" list_members
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 13, 2004
-.\" Last Updated: September 13, 2004
-.\"
-.TH list_member 1 "September 13, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-list_member \- List all the members of a Mailman mailing list.
-.\"=====================================================================
-.SH SYNOPSIS
-.B list_member
-[-o \fIfile\fP]
-[-r]
-[-d [\fIkind\fP]]
-[-n [\fIwhy\fI]]
-[-f]
-[-p]
-[-i]
-[-u]
-[-h]
-\fIlistname\fP
-.\"=====================================================================
-.SH DESCRIPTION
-.B list_member
-lists all members of a mailing list.
-.PP
-Note that if neither -r or -d is supplied, both regular members are printed
-first, followed by digest members, but no indication is given as to address
-status.
-.\"=====================================================================
-.SH OPTIONS
-.IP "--output \fIfile\fP, -o \fIfile\fP"
-Write output to specified file instead of standard out.
-.IP "--regular, -r"
-Print just the regular (non-digest) members.
-.IP "--digest[=\fIkind\fP], -d [\fIkind\fP]"
-Print just the digest members. Optional argument can be "mime" or
-"plain" which prints just the digest members receiving that kind of
-digest.
-.IP "--nomail[=\fIwhy\fP], -n[\fIwhy\fP]"
-Print the members that have delivery disabled. Optional argument can
-be "byadmin", "byuser", "bybounce", or "unknown" which prints just the
-users who have delivery disabled for that reason. It can also be
-"enabled" which prints just those member for whom delivery is
-enabled.
-.IP "--fullnames, -f"
-Include the full names in the output.
-.IP "--preserve, -p"
-Output member addresses case preserved the way they were added to the
-list. Otherwise, addresses are printed in all lowercase.
-.IP "--invalid, -i"
-Print only the addresses in the membership list that are invalid.
-Ignores -r, -d, -n.
-.IP "--unicode, -u"
-Print addresses which are stored as Unicode objects instead of normal
-string objects. Ignores -r, -d, -n.
-.IP "--help, -h"
-Print help message and exit.
-.IP "\fIlistname\fP"
-The name of the mailing list to use.
-.\"=====================================================================
-.SH SEE ALSO
-.BR add_members (1),
-.BR clone_member (1),
-.BR find_member (1),
-.BR remove_members (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/remove_members.1 b/src/attic/docs/man/remove_members.1
deleted file mode 100644
index 69ed545d6..000000000
--- a/src/attic/docs/man/remove_members.1
+++ /dev/null
@@ -1,63 +0,0 @@
-add.\"
-.\" GNU Mailman Manual
-.\"
-.\" remove_members
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 13, 2004
-.\" Last Updated: September 13, 2004
-.\"
-.TH remove_members 1 "September 13, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-remove_members \- Remove members from a Mailman mailing list.
-.\"=====================================================================
-.SH SYNOPSIS
-.B remove_members
-[-f \fIfile\fP]
-[-a]
-[--fromall]
-[-n]
-[-N]
-[-h]
-[\fIlistname\fP]
-[\fIaddr1\fP ...]
-.\"=====================================================================
-.SH DESCRIPTION
-.B remove_members
-removes members from a Mailman mailing list from the command line.
-.\"=====================================================================
-.SH OPTIONS
-.IP "---file=\fIfile\fP, -f \fIfile\fP"
-Remove member addresses found in the given file. If file is
-`-', read stdin.
-.IP "--all, -a"
-Remove all members of the mailing list.
-(mutually exclusive with --fromall)
-.IP "--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.
-.IP "--nouserack, -n"
-Don't send the admin acknowledgements. If not specified, the list
-default value is used.
-.IP "--help, -h"
-Print help message and exit.
-.IP \fPlistname\fI
-The name of the mailing list to use.
-.IP \fPaddr1\fI ...
-Additional addresses to remove.
-.\"=====================================================================
-.SH SEE ALSO
-.BR add_members (1),
-.BR clone_member (1),
-.BR find_member (1),
-.BR list_members (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/sync_members.1 b/src/attic/docs/man/sync_members.1
deleted file mode 100644
index b185ae3c1..000000000
--- a/src/attic/docs/man/sync_members.1
+++ /dev/null
@@ -1,81 +0,0 @@
-add.\"
-.\" GNU Mailman Manual
-.\"
-.\" list_members
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 13, 2004
-.\" Last Updated: September 13, 2004
-.\"
-.TH sync_members 1 "September 13, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-sync_members \- Synchronize a Mailman mailing list's membership with a flat file.
-.\"=====================================================================
-.SH SYNOPSIS
-.B sync_members
-[-n]
-[-w <\fIyes|no\fP>]
-[-g <\fIyes|no\fP>]
-[-d <\fIyes|no\fP>]
-[-a <\fIyes|no\fP>]
-[-h]
--f \fIfilename\fP
-\fIlistname\fP
-.\"=====================================================================
-.SH DESCRIPTION
-.B sync_members
-synchronizes a Mailman mailing list's membership with a flat file.
-.PP
-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.
-.\"=====================================================================
-.SH OPTIONS
-.IP "--no-change -n"
-Don't actually make the changes. Instead, print out what would be
-done to the list.
-.IP "-welcome-msg[=<\fIyes|no\fP>], -w[=<\fIyes|no\fP>]"
-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.
-.IP "--goodbye-msg[=<\fIyes|no\fP>], -g[=<\fIyes|no\fP>]"
-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.
-.IP "--digest[=<\fIyes|no\fP>], -d[=<\fIyes|no\fP>]"
-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.
-.IP "--notifyadmin[=<\fIyes|no\fP>], -a[=<\fIyes|no\fP>]"
-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.
-.IP "--file <\fIfilename | -\fp>, -f <\fIfilename | -\fP>"
-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.
-.IP "--help, -h"
-Print help message.
-.IP \fIlistname\fP
-Required. This specifies the list to synchronize.
-.\"=====================================================================
-.SH SEE ALSO
-.BR add_members (1),
-.BR clone_member (1),
-.BR list_members (1),
-.BR remove_members (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/man/transcheck.1 b/src/attic/docs/man/transcheck.1
deleted file mode 100644
index 6e16a10f8..000000000
--- a/src/attic/docs/man/transcheck.1
+++ /dev/null
@@ -1,41 +0,0 @@
-.\"
-.\" GNU Mailman Manual
-.\"
-.\" transcheck
-.\"
-.\" Documenter: Terri Oda
-.\" terri (at) zone12.com
-.\" Created: September 18, 2004
-.\" Last Updated: September 18, 2004
-.\"
-.TH transcheck 1 "September 18, 2004" "Mailman 2.1" "GNU Mailman Manual"
-.\"=====================================================================
-.SH NAME
-transcheck \- Check a given Mailman translation
-.\"=====================================================================
-.SH SYNOPSIS
-.B transcheck
-[-q]
-\fIlang\fP
-.\"=====================================================================
-.SH DESCRIPTION
-.B transcheck
-checks 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.
-.\"=====================================================================
-.SH OPTIONS
-.IP "-q"
-Asks for a brief summary.
-.IP "\fIlang\fP"
-Your country code. (e.g. 'it' for Italy)
-.\"=====================================================================
-.SH SEE ALSO
-.BR check_perms (1),
-.BR check_db (1)
-.PP
-The Mailman website: http://www.list.org
-.\"=====================================================================
-.SH AUTHOR
-This man page was created by Terri Oda <terri (at) zone12.com>.
-Use <mailman-developers@python.org> to contact the developers.
diff --git a/src/attic/docs/posting-flow-chart.ps b/src/attic/docs/posting-flow-chart.ps
deleted file mode 100644
index e8d47e27c..000000000
--- a/src/attic/docs/posting-flow-chart.ps
+++ /dev/null
@@ -1,735 +0,0 @@
-%!PS-Adobe-2.0
-%%Title: posting-flow-chart
-%%Creator: Dia v0.86
-%%CreationDate: Mon Oct 15 13:46:55 2001
-%%For: a user
-%%DocumentPaperSizes: A4
-%%Orientation: Landscape
-%%BeginSetup
-%%EndSetup
-%%EndComments
-%%BeginProlog
-[ /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
-/.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
-/.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
-/.notdef /.notdef /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright
-/parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one
-/two /three /four /five /six /seven /eight /nine /colon /semicolon
-/less /equal /greater /question /at /A /B /C /D /E
-/F /G /H /I /J /K /L /M /N /O
-/P /Q /R /S /T /U /V /W /X /Y
-/Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c
-/d /e /f /g /h /i /j /k /l /m
-/n /o /p /q /r /s /t /u /v /w
-/x /y /z /braceleft /bar /braceright /asciitilde /.notdef /.notdef /.notdef
-/.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
-/.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
-/.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
-/space /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright
-/ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior
-/acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf
-/threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla
-/Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde
-/Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex
-/Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring
-/ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis
-/eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave
-/uacute /ucircumflex /udieresis /yacute /thorn /ydieresis] /isolatin1encoding exch def
-/Times-Roman-latin1
- /Times-Roman findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Times-Italic-latin1
- /Times-Italic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Times-Bold-latin1
- /Times-Bold findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Times-BoldItalic-latin1
- /Times-BoldItalic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/AvantGarde-Book-latin1
- /AvantGarde-Book findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/AvantGarde-BookOblique-latin1
- /AvantGarde-BookOblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/AvantGarde-Demi-latin1
- /AvantGarde-Demi findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/AvantGarde-DemiOblique-latin1
- /AvantGarde-DemiOblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Bookman-Light-latin1
- /Bookman-Light findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Bookman-LightItalic-latin1
- /Bookman-LightItalic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Bookman-Demi-latin1
- /Bookman-Demi findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Bookman-DemiItalic-latin1
- /Bookman-DemiItalic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Courier-latin1
- /Courier findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Courier-Oblique-latin1
- /Courier-Oblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Courier-Bold-latin1
- /Courier-Bold findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Courier-BoldOblique-latin1
- /Courier-BoldOblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-latin1
- /Helvetica findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-Oblique-latin1
- /Helvetica-Oblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-Bold-latin1
- /Helvetica-Bold findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-BoldOblique-latin1
- /Helvetica-BoldOblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-Narrow-latin1
- /Helvetica-Narrow findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-Narrow-Oblique-latin1
- /Helvetica-Narrow-Oblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-Narrow-Bold-latin1
- /Helvetica-Narrow-Bold findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Helvetica-Narrow-BoldOblique-latin1
- /Helvetica-Narrow-BoldOblique findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/NewCenturySchoolbook-Roman-latin1
- /NewCenturySchoolbook-Roman findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/NewCenturySchoolbook-Italic-latin1
- /NewCenturySchoolbook-Italic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/NewCenturySchoolbook-Bold-latin1
- /NewCenturySchoolbook-Bold findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/NewCenturySchoolbook-BoldItalic-latin1
- /NewCenturySchoolbook-BoldItalic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Palatino-Roman-latin1
- /Palatino-Roman findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Palatino-Italic-latin1
- /Palatino-Italic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Palatino-Bold-latin1
- /Palatino-Bold findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Palatino-BoldItalic-latin1
- /Palatino-BoldItalic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/Symbol-latin1
- /Symbol findfont
-definefont pop
-/ZapfChancery-MediumItalic-latin1
- /ZapfChancery-MediumItalic findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/ZapfDingbats-latin1
- /ZapfDingbats findfont
- dup length dict begin
- {1 index /FID ne {def} {pop pop} ifelse} forall
- /Encoding isolatin1encoding def
- currentdict end
-definefont pop
-/cp {closepath} bind def
-/c {curveto} bind def
-/f {fill} bind def
-/a {arc} bind def
-/ef {eofill} bind def
-/ex {exch} bind def
-/gr {grestore} bind def
-/gs {gsave} bind def
-/sa {save} bind def
-/rs {restore} bind def
-/l {lineto} bind def
-/m {moveto} bind def
-/rm {rmoveto} bind def
-/n {newpath} bind def
-/s {stroke} bind def
-/sh {show} bind def
-/slc {setlinecap} bind def
-/slj {setlinejoin} bind def
-/slw {setlinewidth} bind def
-/srgb {setrgbcolor} bind def
-/rot {rotate} bind def
-/sc {scale} bind def
-/sd {setdash} bind def
-/ff {findfont} bind def
-/sf {setfont} bind def
-/scf {scalefont} bind def
-/sw {stringwidth pop} bind def
-/tr {translate} bind def
-
-/ellipsedict 8 dict def
-ellipsedict /mtrx matrix put
-/ellipse
-{ ellipsedict begin
- /endangle exch def
- /startangle exch def
- /yrad exch def
- /xrad exch def
- /y exch def
- /x exch def /savematrix mtrx currentmatrix def
- x y tr xrad yrad sc
- 0 0 1 startangle endangle arc
- savematrix setmatrix
- end
-} def
-
-/mergeprocs {
-dup length
-3 -1 roll
-dup
-length
-dup
-5 1 roll
-3 -1 roll
-add
-array cvx
-dup
-3 -1 roll
-0 exch
-putinterval
-dup
-4 2 roll
-putinterval
-} bind def
-%%EndProlog
-
-
-%%Page: 1 1
-gs
-90 rotate
-10.504277 -10.504277 scale
--7.390052 13.596868 translate
-n 15.000000 -5.986920 m 79.927437 -5.986920 l 79.927437 35.463012 l 15.000000 35.463012 l 15.000000 -5.986920 l clip
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0 slj
-0 slc
-0 slj
-[] 0 sd
-1.000000 1.000000 1.000000 srgb
-n 28.288577 -5.936920 m 34.143683 -5.936920 l 34.952105 -5.936920 35.607460 -5.489205 35.607460 -4.936920 c 35.607460 -4.384635 34.952105 -3.936920 34.143683 -3.936920 c 28.288577 -3.936920 l 27.480155 -3.936920 26.824800 -4.384635 26.824800 -4.936920 c 26.824800 -5.489205 27.480155 -5.936920 28.288577 -5.936920 c f
-0.000000 0.000000 0.000000 srgb
-n 28.288577 -5.936920 m 34.143683 -5.936920 l 34.952105 -5.936920 35.607460 -5.489205 35.607460 -4.936920 c 35.607460 -4.384635 34.952105 -3.936920 34.143683 -3.936920 c 28.288577 -3.936920 l 27.480155 -3.936920 26.824800 -4.384635 26.824800 -4.936920 c 26.824800 -5.489205 27.480155 -5.936920 28.288577 -5.936920 c s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(post a msg) dup sw 2 div 31.216130 ex sub -4.742230 m gs 1 -1 sc sh gr
-1.000000 1.000000 1.000000 srgb
-n 31.228705 0.448229 m 34.632611 3.852135 l 31.228705 7.256041 l 27.824799 3.852135 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 31.228705 0.448229 m 34.632611 3.852135 l 31.228705 7.256041 l 27.824799 3.852135 l cp s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(is a) dup sw 2 div 31.228705 ex sub 3.646825 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(member?) dup sw 2 div 31.228705 ex sub 4.446825 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 31.228700 0.448231 m 31.216100 -3.936920 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 30.826403 -0.350616 m 31.228700 0.448231 l 31.626400 -0.352915 l f
-1.000000 1.000000 1.000000 srgb
-n 22.333205 8.807639 m 25.737111 12.211545 l 22.333205 15.615451 l 18.929299 12.211545 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 22.333205 8.807639 m 25.737111 12.211545 l 22.333205 15.615451 l 18.929299 12.211545 l cp s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(mod bit) dup sw 2 div 22.333205 ex sub 12.006235 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(set?) dup sw 2 div 22.333205 ex sub 12.806235 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 22.333200 8.807640 m 27.824800 3.852140 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 22.659157 7.974722 m 22.333200 8.807640 l 23.195108 8.568655 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-() dup sw 2 div 15.000000 ex sub 5.000000 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(yes) dup sw 2 div 28.675700 ex sub 3.001160 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(yes) dup sw 2 div 19.780300 ex sub 11.360600 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(no) dup sw 2 div 23.184200 ex sub 14.764500 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 34.632600 3.852140 m 40.261205 8.139600 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 39.382424 7.973037 m 40.261205 8.139600 l 39.867187 7.336637 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(no) dup sw 2 div 33.781600 ex sub 3.001160 m gs 1 -1 sc sh gr
-1.000000 1.000000 1.000000 srgb
-n 40.261205 8.139600 m 43.907510 11.959285 l 40.261205 15.778970 l 36.614900 11.959285 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 40.261205 8.139600 m 43.907510 11.959285 l 40.261205 15.778970 l 36.614900 11.959285 l cp s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(explicit) dup sw 2 div 40.261205 ex sub 11.753975 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(accept?) dup sw 2 div 40.261205 ex sub 12.553975 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(yes) dup sw 2 div 39.349629 ex sub 14.824049 m gs 1 -1 sc sh gr
-1.000000 1.000000 1.000000 srgb
-n 59.913105 8.346249 m 63.559411 11.992555 l 59.913105 15.638861 l 56.266799 11.992555 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 59.913105 8.346249 m 63.559411 11.992555 l 59.913105 15.638861 l 56.266799 11.992555 l cp s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(explicit) dup sw 2 div 59.913105 ex sub 11.787245 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(reject?) dup sw 2 div 59.913105 ex sub 12.587245 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(yes) dup sw 2 div 59.001528 ex sub 14.727284 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 54.324011 11.922575 m 56.266799 11.992555 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 55.452919 12.363498 m 56.266799 11.992555 l 55.481716 11.564017 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(no) dup sw 2 div 42.995934 ex sub 11.004364 m gs 1 -1 sc sh gr
-1.000000 1.000000 1.000000 srgb
-n 71.066405 8.389389 m 74.712711 12.035695 l 71.066405 15.682001 l 67.420099 12.035695 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 71.066405 8.389389 m 74.712711 12.035695 l 71.066405 15.682001 l 67.420099 12.035695 l cp s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(explicit) dup sw 2 div 71.066405 ex sub 11.830385 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(discard?) dup sw 2 div 71.066405 ex sub 12.630385 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 63.559411 11.992555 m 67.420100 12.035700 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 66.615680 12.426735 m 67.420100 12.035700 l 66.624620 11.626785 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(no) dup sw 2 div 62.647834 ex sub 11.080979 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 71.066400 15.682000 m 71.055000 19.434500 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 70.657432 18.633289 m 71.055000 19.434500 l 71.457429 18.635719 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(yes) dup sw 2 div 70.154800 ex sub 14.770400 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(no) dup sw 2 div 73.801100 ex sub 11.124100 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 59.913105 15.638861 m 59.907500 19.455400 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 59.508675 18.654813 m 59.907500 19.455400 l 60.308674 18.655988 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 22.333200 15.615400 m 22.362400 21.044900 l 27.166900 21.015055 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 26.369400 21.420017 m 27.166900 21.015055 l 26.364431 20.620032 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0 slj
-0 slc
-0 slj
-[] 0 sd
-1.000000 1.000000 1.000000 srgb
-n 28.954977 29.425200 m 34.810083 29.425200 l 35.618505 29.425200 36.273860 29.964050 36.273860 30.628755 c 36.273860 31.293460 35.618505 31.832310 34.810083 31.832310 c 28.954977 31.832310 l 28.146555 31.832310 27.491200 31.293460 27.491200 30.628755 c 27.491200 29.964050 28.146555 29.425200 28.954977 29.425200 c f
-0.000000 0.000000 0.000000 srgb
-n 28.954977 29.425200 m 34.810083 29.425200 l 35.618505 29.425200 36.273860 29.964050 36.273860 30.628755 c 36.273860 31.293460 35.618505 31.832310 34.810083 31.832310 c 28.954977 31.832310 l 28.146555 31.832310 27.491200 31.293460 27.491200 30.628755 c 27.491200 29.964050 28.146555 29.425200 28.954977 29.425200 c s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(hold for) dup sw 2 div 31.882530 ex sub 30.423445 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(moderation) dup sw 2 div 31.882530 ex sub 31.223445 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0 slj
-0 slc
-0 slj
-[] 0 sd
-1.000000 1.000000 1.000000 srgb
-n 28.666900 19.811500 m 34.666900 19.811500 l 35.495328 19.811500 36.166900 20.350350 36.166900 21.015055 c 36.166900 21.679760 35.495328 22.218610 34.666900 22.218610 c 28.666900 22.218610 l 27.838472 22.218610 27.166900 21.679760 27.166900 21.015055 c 27.166900 20.350350 27.838472 19.811500 28.666900 19.811500 c f
-0.000000 0.000000 0.000000 srgb
-n 28.666900 19.811500 m 34.666900 19.811500 l 35.495328 19.811500 36.166900 20.350350 36.166900 21.015055 c 36.166900 21.679760 35.495328 22.218610 34.666900 22.218610 c 28.666900 22.218610 l 27.838472 22.218610 27.166900 21.679760 27.166900 21.015055 c 27.166900 20.350350 27.838472 19.811500 28.666900 19.811500 c s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(pass thru) dup sw 2 div 31.666900 ex sub 21.209745 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0 slj
-0 slc
-0 slj
-[] 0 sd
-1.000000 1.000000 1.000000 srgb
-n 57.222377 19.455400 m 62.592683 19.455400 l 63.334168 19.455400 63.935260 19.903115 63.935260 20.455400 c 63.935260 21.007685 63.334168 21.455400 62.592683 21.455400 c 57.222377 21.455400 l 56.480892 21.455400 55.879800 21.007685 55.879800 20.455400 c 55.879800 19.903115 56.480892 19.455400 57.222377 19.455400 c f
-0.000000 0.000000 0.000000 srgb
-n 57.222377 19.455400 m 62.592683 19.455400 l 63.334168 19.455400 63.935260 19.903115 63.935260 20.455400 c 63.935260 21.007685 63.334168 21.455400 62.592683 21.455400 c 57.222377 21.455400 l 56.480892 21.455400 55.879800 21.007685 55.879800 20.455400 c 55.879800 19.903115 56.480892 19.455400 57.222377 19.455400 c s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(bounce it) dup sw 2 div 59.907530 ex sub 20.650090 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0 slj
-0 slc
-0 slj
-[] 0 sd
-1.000000 1.000000 1.000000 srgb
-n 67.885077 19.434500 m 74.224983 19.434500 l 75.100342 19.434500 75.809960 19.882215 75.809960 20.434500 c 75.809960 20.986785 75.100342 21.434500 74.224983 21.434500 c 67.885077 21.434500 l 67.009718 21.434500 66.300100 20.986785 66.300100 20.434500 c 66.300100 19.882215 67.009718 19.434500 67.885077 19.434500 c f
-0.000000 0.000000 0.000000 srgb
-n 67.885077 19.434500 m 74.224983 19.434500 l 75.100342 19.434500 75.809960 19.882215 75.809960 20.434500 c 75.809960 20.986785 75.100342 21.434500 74.224983 21.434500 c 67.885077 21.434500 l 67.009718 21.434500 66.300100 20.986785 66.300100 20.434500 c 66.300100 19.882215 67.009718 19.434500 67.885077 19.434500 c s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(discard it) dup sw 2 div 71.055030 ex sub 20.629190 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 40.261205 15.778970 m 40.184200 21.044900 l 36.166900 21.015055 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 36.969849 20.621009 m 36.166900 21.015055 l 36.963906 21.420987 l f
-1.000000 1.000000 1.000000 srgb
-n 65.017305 25.865999 m 69.790811 30.639505 l 65.017305 35.413011 l 60.243799 30.639505 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 65.017305 25.865999 m 69.790811 30.639505 l 65.017305 35.413011 l 60.243799 30.639505 l cp s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(generic) dup sw 2 div 65.017305 ex sub 30.034195 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(non-member) dup sw 2 div 65.017305 ex sub 30.834195 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(disposition) dup sw 2 div 65.017305 ex sub 31.634195 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 74.712700 12.035700 m 77.310000 12.068400 l 77.310000 30.668100 l 69.790800 30.639500 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 70.592316 30.242546 m 69.790800 30.639500 l 70.589273 31.042540 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(discard) dup sw 2 div 68.632800 ex sub 24.246900 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 66.210600 27.059400 m 71.055000 21.434500 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 70.836024 22.301708 m 71.055000 21.434500 l 70.229848 21.779644 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 63.823900 27.059400 m 59.907500 21.455400 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 60.693636 21.882004 m 59.907500 21.455400 l 60.037899 22.340271 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(reject) dup sw 2 div 61.865700 ex sub 24.257400 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 60.243800 30.639500 m 36.273860 30.628755 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 37.074039 30.229114 m 36.273860 30.628755 l 37.073681 31.029114 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(hold) dup sw 2 div 48.258830 ex sub 30.634128 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 18.929299 12.211545 m 17.707519 12.274592 l 17.707519 30.574592 l 27.491200 30.628755 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 26.688998 31.024320 m 27.491200 30.628755 l 26.693427 30.224332 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 61.437100 29.446100 m 35.727550 21.866089 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 36.608013 21.708655 m 35.727550 21.866089 l 36.381775 22.475998 l f
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(accept) dup sw 2 div 48.582325 ex sub 25.656094 m gs 1 -1 sc sh gr
-1.000000 1.000000 1.000000 srgb
-n 50.677705 8.276269 m 54.324011 11.922575 l 50.677705 15.568881 l 47.031399 11.922575 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 50.677705 8.276269 m 54.324011 11.922575 l 50.677705 15.568881 l 47.031399 11.922575 l cp s
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(explicit) dup sw 2 div 50.677705 ex sub 11.717265 m gs 1 -1 sc sh gr
-0.000000 0.000000 0.000000 srgb
-(hold?) dup sw 2 div 50.677705 ex sub 12.517265 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(yes) dup sw 2 div 49.766128 ex sub 14.657304 m gs 1 -1 sc sh gr
-/Courier-latin1 ff 0.800000 scf sf
-0.000000 0.000000 0.000000 srgb
-(no) dup sw 2 div 53.412434 ex sub 11.010998 m gs 1 -1 sc sh gr
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slj
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 50.677705 15.568881 m 50.723859 19.674592 l 35.845120 29.777721 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 36.282254 28.997392 m 35.845120 29.777721 l 36.731664 29.659231 l f
-0.100000 slw
-[] 0 sd
-[] 0 sd
-0 slc
-0.000000 0.000000 0.000000 srgb
-n 43.907510 11.959285 m 47.031399 11.922575 l s
-0 slj
-0.000000 0.000000 0.000000 srgb
-n 46.236154 12.331948 m 47.031399 11.922575 l 46.226754 11.532003 l f
-/Courier-latin1 ff 2.000000 scf sf
-0.000000 0.000000 0.000000 srgb
-(Sender Moderation Flowchart) dup sw 2 div 57.443403 ex sub 1.624592 m gs 1 -1 sc sh gr
-gr
-showpage
-
diff --git a/src/web/Cgi/Auth.py b/src/web/Cgi/Auth.py
deleted file mode 100644
index 825d972f4..000000000
--- a/src/web/Cgi/Auth.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# 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/>.
-
-"""Common routines for logging in and logging out of the list administrator
-and list moderator interface.
-"""
-
-from Mailman import Utils
-from Mailman.htmlformat import FontAttr
-from Mailman.i18n import _
-
-
-
-class NotLoggedInError(Exception):
- """Exception raised when no matching admin cookie was found."""
- def __init__(self, message):
- Exception.__init__(self, message)
- self.message = message
-
-
-
-def loginpage(mlist, scriptname, msg='', frontpage=False):
- url = mlist.GetScriptURL(scriptname)
- if frontpage:
- actionurl = url
- else:
- request = Utils.GetRequestURI(url).lstrip('/')
- up = '../' * request.count('/')
- actionurl = up + request
- if msg:
- msg = FontAttr(msg, color='#ff0000', size='+1').Format()
- if scriptname == 'admindb':
- who = _('Moderator')
- else:
- who = _('Administrator')
- # Language stuff
- charset = Utils.GetCharSet(mlist.preferred_language)
- print 'Content-type: text/html; charset=' + charset + '\n\n'
- print Utils.maketext(
- 'admlogin.html',
- {'listname': mlist.real_name,
- 'path' : actionurl,
- 'message' : msg,
- 'who' : who,
- }, mlist=mlist).encode(charset)
- print mlist.GetMailmanFooter().encode(charset)
diff --git a/src/web/Cgi/__init__.py b/src/web/Cgi/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/src/web/Cgi/__init__.py
+++ /dev/null
diff --git a/src/web/Cgi/admin.py b/src/web/Cgi/admin.py
deleted file mode 100644
index e5c6ee14b..000000000
--- a/src/web/Cgi/admin.py
+++ /dev/null
@@ -1,1433 +0,0 @@
-# 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/>.
-
-"""Process and produce the list-administration options forms."""
-
-import os
-import re
-import cgi
-import sha
-import sys
-import urllib
-import logging
-
-from email.Utils import unquote, parseaddr, formataddr
-from string import lowercase, digits
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import MemberAdaptor
-from Mailman import Utils
-from Mailman import i18n
-from Mailman import passwords
-from Mailman.Cgi import Auth
-from Mailman.UserDesc import UserDesc
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-NL = '\n'
-OPTCOLUMNS = 11
-
-log = logging.getLogger('mailman.error')
-
-
-
-def main():
- # Try to find out which list is being administered
- parts = Utils.GetPathPieces()
- if not parts:
- # None, so just do the admin overview and be done with it
- admin_overview()
- return
- # Get the list object
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=False)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- admin_overview(_('No such list <em>%(safelistname)s</em>'))
- log.error('admin.py access for non-existent list: %s', listname)
- return
- # Now that we know what list has been requested, all subsequent admin
- # pages are shown in that list's preferred language.
- i18n.set_language(mlist.preferred_language)
- # If the user is not authenticated, we're done.
- cgidata = cgi.FieldStorage(keep_blank_values=1)
-
- if not mlist.WebAuthenticate((config.AuthListAdmin,
- config.AuthSiteAdmin),
- cgidata.getvalue('adminpw', '')):
- if cgidata.has_key('adminpw'):
- # This is a re-authorization attempt
- msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
- else:
- msg = ''
- Auth.loginpage(mlist, 'admin', msg=msg)
- return
-
- # Which subcategory was requested? Default is `general'
- if len(parts) == 1:
- category = 'general'
- subcat = None
- elif len(parts) == 2:
- category = parts[1]
- subcat = None
- else:
- category = parts[1]
- subcat = parts[2]
-
- # Is this a log-out request?
- if category == 'logout':
- print mlist.ZapCookie(config.AuthListAdmin)
- Auth.loginpage(mlist, 'admin', frontpage=True)
- return
-
- # Sanity check
- if category not in mlist.GetConfigCategories().keys():
- category = 'general'
-
- # Is the request for variable details?
- varhelp = None
- qsenviron = os.environ.get('QUERY_STRING')
- parsedqs = None
- if qsenviron:
- parsedqs = cgi.parse_qs(qsenviron)
- if cgidata.has_key('VARHELP'):
- varhelp = cgidata.getvalue('VARHELP')
- elif parsedqs:
- # POST methods, even if their actions have a query string, don't get
- # put into FieldStorage's keys :-(
- qs = parsedqs.get('VARHELP')
- if qs and isinstance(qs, list):
- varhelp = qs[0]
- if varhelp:
- option_help(mlist, varhelp)
- return
-
- # The html page document
- doc = Document()
- doc.set_language(mlist.preferred_language)
- mlist.Lock()
- try:
- if cgidata.keys():
- # There are options to change
- change_options(mlist, category, subcat, cgidata, doc)
- # Let the list sanity check the changed values
- mlist.CheckValues()
- # Additional sanity checks
- if not mlist.digestable and not mlist.nondigestable:
- doc.addError(
- _('''You have turned off delivery of both digest and
- non-digest messages. This is an incompatible state of
- affairs. You must turn on either digest delivery or
- non-digest delivery or your mailing list will basically be
- unusable.'''), tag=_('Warning: '))
-
- if not mlist.digestable and mlist.getDigestMemberKeys():
- doc.addError(
- _('''You have digest members, but digests are turned
- off. Those people will not receive mail.'''),
- tag=_('Warning: '))
- if not mlist.nondigestable and mlist.getRegularMemberKeys():
- doc.addError(
- _('''You have regular list members but non-digestified mail is
- turned off. They will receive mail until you fix this
- problem.'''), tag=_('Warning: '))
- # Glom up the results page and print it out
- show_results(mlist, doc, category, subcat, cgidata)
- print doc.Format()
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def admin_overview(msg=''):
- # Show the administrative overview page, with the list of all the lists on
- # this host. msg is an optional error message to display at the top of
- # the page.
- #
- # This page should be displayed in the server's default language, which
- # should have already been set.
- hostname = Utils.get_request_domain()
- legend = _('%(hostname)s mailing lists - Admin Links')
- # The html `document'
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
- doc.SetTitle(legend)
- # The table that will hold everything
- table = Table(border=0, width="100%")
- table.AddRow([Center(Header(2, legend))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
- # Skip any mailing list that isn't advertised.
- advertised = []
- for name in sorted(config.list_manager.names):
- mlist = MailList.MailList(name, lock=False)
- if mlist.advertised:
- if hostname not in mlist.web_page_url:
- # This list is situated in a different virtual domain
- continue
- else:
- advertised.append((mlist.GetScriptURL('admin'),
- mlist.real_name,
- mlist.description))
- # Greeting depends on whether there was an error or not
- if msg:
- greeting = FontAttr(msg, color="ff5060", size="+1")
- else:
- greeting = _("Welcome!")
-
- welcome = []
- mailmanlink = Link(config.MAILMAN_URL, _('Mailman')).Format()
- if not advertised:
- welcome.extend([
- greeting,
- _('''<p>There currently are no publicly-advertised %(mailmanlink)s
- mailing lists on %(hostname)s.'''),
- ])
- else:
- welcome.extend([
- greeting,
- _('''<p>Below is the collection of publicly-advertised
- %(mailmanlink)s mailing lists on %(hostname)s. Click on a list
- name to visit the configuration pages for that list.'''),
- ])
-
- creatorurl = Utils.ScriptURL('create')
- mailman_owner = Utils.get_site_noreply()
- extra = msg and _('right ') or ''
- welcome.extend([
- _('''To visit the administrators configuration page for an
- unadvertised list, open a URL similar to this one, but with a '/' and
- the %(extra)slist name appended. If you have the proper authority,
- you can also <a href="%(creatorurl)s">create a new mailing list</a>.
-
- <p>General list information can be found at '''),
- Link(Utils.ScriptURL('listinfo'),
- _('the mailing list overview page')),
- '.',
- _('<p>(Send questions and comments to '),
- Link('mailto:%s' % mailman_owner, mailman_owner),
- '.)<p>',
- ])
-
- table.AddRow([Container(*welcome)])
- table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
-
- if advertised:
- table.AddRow(['&nbsp;', '&nbsp;'])
- table.AddRow([Bold(FontAttr(_('List'), size='+2')),
- Bold(FontAttr(_('Description'), size='+2'))
- ])
- highlight = 1
- for url, real_name, description in advertised:
- table.AddRow(
- [Link(url, Bold(real_name)),
- description or Italic(_('[no description available]'))])
- if highlight and config.WEB_HIGHLIGHT_COLOR:
- table.AddRowInfo(table.GetCurrentRowIndex(),
- bgcolor=config.WEB_HIGHLIGHT_COLOR)
- highlight = not highlight
-
- doc.AddItem(table)
- doc.AddItem('<hr>')
- doc.AddItem(MailmanLogo())
- print doc.Format()
-
-
-
-def option_help(mlist, varhelp):
- # The html page document
- doc = Document()
- doc.set_language(mlist.preferred_language)
- # Find out which category and variable help is being requested for.
- item = None
- reflist = varhelp.split('/')
- if len(reflist) >= 2:
- category = subcat = None
- if len(reflist) == 2:
- category, varname = reflist
- elif len(reflist) == 3:
- category, subcat, varname = reflist
- options = mlist.GetConfigInfo(category, subcat)
- if options:
- for i in options:
- if i and i[0] == varname:
- item = i
- break
- # Print an error message if we couldn't find a valid one
- if not item:
- bad = _('No valid variable name found.')
- doc.addError(bad)
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- return
- # Get the details about the variable
- varname, kind, params, dependancies, description, elaboration = \
- get_item_characteristics(item)
- # Set up the document
- realname = mlist.real_name
- legend = _("""%(realname)s Mailing list Configuration Help
- <br><em>%(varname)s</em> Option""")
-
- header = Table(width='100%')
- header.AddRow([Center(Header(3, legend))])
- header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
- doc.SetTitle(_("Mailman %(varname)s List Option Help"))
- doc.AddItem(header)
- doc.AddItem("<b>%s</b> (%s): %s<p>" % (varname, category, description))
- if elaboration:
- doc.AddItem("%s<p>" % elaboration)
-
- if subcat:
- url = '%s/%s/%s' % (mlist.GetScriptURL('admin'), category, subcat)
- else:
- url = '%s/%s' % (mlist.GetScriptURL('admin'), category)
- form = Form(url)
- valtab = Table(cellspacing=3, cellpadding=4, width='100%')
- add_options_table_item(mlist, category, subcat, valtab, item, detailsp=0)
- form.AddItem(valtab)
- form.AddItem('<p>')
- form.AddItem(Center(submit_button()))
- doc.AddItem(Center(form))
-
- doc.AddItem(_("""<em><strong>Warning:</strong> changing this option here
- could cause other screens to be out-of-sync. Be sure to reload any other
- pages that are displaying this option for this mailing list. You can also
- """))
-
- adminurl = mlist.GetScriptURL('admin')
- if subcat:
- url = '%s/%s/%s' % (adminurl, category, subcat)
- else:
- url = '%s/%s' % (adminurl, category)
- categoryname = mlist.GetConfigCategories()[category][0]
- doc.AddItem(Link(url, _('return to the %(categoryname)s options page.')))
- doc.AddItem('</em>')
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
-
-
-
-def show_results(mlist, doc, category, subcat, cgidata):
- # Produce the results page
- adminurl = mlist.GetScriptURL('admin')
- categories = mlist.GetConfigCategories()
- label = _(categories[category][0])
-
- # Set up the document's headers
- realname = mlist.real_name
- doc.SetTitle(_('%(realname)s Administration (%(label)s)'))
- doc.AddItem(Center(Header(2, _(
- '%(realname)s mailing list administration<br>%(label)s Section'))))
- doc.AddItem('<hr>')
- # Now we need to craft the form that will be submitted, which will contain
- # all the variable settings, etc. This is a bit of a kludge because we
- # know that the autoreply and members categories supports file uploads.
- encoding = None
- if category in ('autoreply', 'members'):
- encoding = 'multipart/form-data'
- if subcat:
- form = Form('%s/%s/%s' % (adminurl, category, subcat),
- encoding=encoding)
- else:
- form = Form('%s/%s' % (adminurl, category), encoding=encoding)
- # This holds the two columns of links
- linktable = Table(valign='top', width='100%')
- linktable.AddRow([Center(Bold(_("Configuration Categories"))),
- Center(Bold(_("Other Administrative Activities")))])
- # The `other links' are stuff in the right column.
- otherlinks = UnorderedList()
- otherlinks.AddItem(Link(mlist.GetScriptURL('admindb'),
- _('Tend to pending moderator requests')))
- otherlinks.AddItem(Link(mlist.GetScriptURL('listinfo'),
- _('Go to the general list information page')))
- otherlinks.AddItem(Link(mlist.GetScriptURL('edithtml'),
- _('Edit the public HTML pages and text files')))
- otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(),
- _('Go to list archives')).Format() +
- '<br>&nbsp;<br>')
- if config.OWNERS_CAN_DELETE_THEIR_OWN_LISTS:
- otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'),
- _('Delete this mailing list')).Format() +
- _(' (requires confirmation)<br>&nbsp;<br>'))
- otherlinks.AddItem(Link('%s/logout' % adminurl,
- # BAW: What I really want is a blank line, but
- # adding an &nbsp; won't do it because of the
- # bullet added to the list item.
- '<FONT SIZE="+2"><b>%s</b></FONT>' %
- _('Logout')))
- # These are links to other categories and live in the left column
- categorylinks_1 = categorylinks = UnorderedList()
- categorylinks_2 = ''
- categorykeys = categories.keys()
- half = len(categorykeys) / 2
- counter = 0
- subcat = None
- for k in categorykeys:
- label = _(categories[k][0])
- url = '%s/%s' % (adminurl, k)
- if k == category:
- # Handle subcategories
- subcats = mlist.GetConfigSubCategories(k)
- if subcats:
- subcat = Utils.GetPathPieces()[-1]
- for k, v in subcats:
- if k == subcat:
- break
- else:
- # The first subcategory in the list is the default
- subcat = subcats[0][0]
- subcat_items = []
- for sub, text in subcats:
- if sub == subcat:
- text = Bold('[%s]' % text).Format()
- subcat_items.append(Link(url + '/' + sub, text))
- categorylinks.AddItem(
- Bold(label).Format() +
- UnorderedList(*subcat_items).Format())
- else:
- categorylinks.AddItem(Link(url, Bold('[%s]' % label)))
- else:
- categorylinks.AddItem(Link(url, label))
- counter += 1
- if counter >= half:
- categorylinks_2 = categorylinks = UnorderedList()
- counter = -len(categorykeys)
- # Make the emergency stop switch a rude solo light
- etable = Table()
- # Add all the links to the links table...
- etable.AddRow([categorylinks_1, categorylinks_2])
- etable.AddRowInfo(etable.GetCurrentRowIndex(), valign='top')
- if mlist.emergency:
- label = _('Emergency moderation of all list traffic is enabled')
- etable.AddRow([Center(
- Link('?VARHELP=general/emergency', Bold(label)))])
- color = config.WEB_ERROR_COLOR
- etable.AddCellInfo(etable.GetCurrentRowIndex(), 0,
- colspan=2, bgcolor=color)
- linktable.AddRow([etable, otherlinks])
- # ...and add the links table to the document.
- form.AddItem(linktable)
- form.AddItem('<hr>')
- form.AddItem(
- _('''Make your changes in the following section, then submit them
- using the <em>Submit Your Changes</em> button below.''')
- + '<p>')
-
- # The members and passwords categories are special in that they aren't
- # defined in terms of gui elements. Create those pages here.
- if category == 'members':
- # Figure out which subcategory we should display
- subcat = Utils.GetPathPieces()[-1]
- if subcat not in ('list', 'add', 'remove'):
- subcat = 'list'
- # Add member category specific tables
- form.AddItem(membership_options(mlist, subcat, cgidata, doc, form))
- form.AddItem(Center(submit_button('setmemberopts_btn')))
- # In "list" subcategory, we can also search for members
- if subcat == 'list':
- form.AddItem('<hr>\n')
- table = Table(width='100%')
- table.AddRow([Center(Header(2, _('Additional Member Tasks')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
- # Add a blank separator row
- table.AddRow(['&nbsp;', '&nbsp;'])
- # Add a section to set the moderation bit for all members
- table.AddRow([_("""<li>Set everyone's moderation bit, including
- those members not currently visible""")])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([RadioButtonArray('allmodbit_val',
- (_('Off'), _('On')),
- mlist.default_member_moderation),
- SubmitButton('allmodbit_btn', _('Set'))])
- form.AddItem(table)
- elif category == 'passwords':
- form.AddItem(Center(password_inputs(mlist)))
- form.AddItem(Center(submit_button()))
- else:
- form.AddItem(show_variables(mlist, category, subcat, cgidata, doc))
- form.AddItem(Center(submit_button()))
- # And add the form
- doc.AddItem(form)
- doc.AddItem(mlist.GetMailmanFooter())
-
-
-
-def show_variables(mlist, category, subcat, cgidata, doc):
- options = mlist.GetConfigInfo(category, subcat)
-
- # The table containing the results
- table = Table(cellspacing=3, cellpadding=4, width='100%')
-
- # Get and portray the text label for the category.
- categories = mlist.GetConfigCategories()
- label = _(categories[category][0])
-
- table.AddRow([Center(Header(2, label))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
-
- # The very first item in the config info will be treated as a general
- # description if it is a string
- description = options[0]
- if isinstance(description, basestring):
- table.AddRow([description])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- options = options[1:]
-
- if not options:
- return table
-
- # Add the global column headers
- table.AddRow([Center(Bold(_('Description'))),
- Center(Bold(_('Value')))])
- table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0,
- width='15%')
- table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1,
- width='85%')
-
- for item in options:
- if isinstance(item, basestring):
- # The very first banner option (string in an options list) is
- # treated as a general description, while any others are
- # treated as section headers - centered and italicized...
- table.AddRow([Center(Italic(item))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- else:
- add_options_table_item(mlist, category, subcat, table, item)
- table.AddRow(['<br>'])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- return table
-
-
-
-def add_options_table_item(mlist, category, subcat, table, item, detailsp=1):
- # Add a row to an options table with the item description and value.
- varname, kind, params, extra, descr, elaboration = \
- get_item_characteristics(item)
- if elaboration is None:
- elaboration = descr
- descr = get_item_gui_description(mlist, category, subcat,
- varname, descr, elaboration, detailsp)
- val = get_item_gui_value(mlist, category, kind, varname, params, extra)
- table.AddRow([descr, val])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_ADMINITEM_COLOR)
- table.AddCellInfo(table.GetCurrentRowIndex(), 1,
- bgcolor=config.WEB_ADMINITEM_COLOR)
-
-
-
-def get_item_characteristics(record):
- # Break out the components of an item description from its description
- # record:
- #
- # 0 -- option-var name
- # 1 -- type
- # 2 -- entry size
- # 3 -- ?dependancies?
- # 4 -- Brief description
- # 5 -- Optional description elaboration
- if len(record) == 5:
- elaboration = None
- varname, kind, params, dependancies, descr = record
- elif len(record) == 6:
- varname, kind, params, dependancies, descr, elaboration = record
- else:
- raise ValueError, _('Badly formed options entry:\n %(record)s')
- return varname, kind, params, dependancies, descr, elaboration
-
-
-
-def get_item_gui_value(mlist, category, kind, varname, params, extra):
- """Return a representation of an item's settings."""
- # Give the category a chance to return the value for the variable
- value = None
- label, gui = mlist.GetConfigCategories()[category]
- if hasattr(gui, 'getValue'):
- value = gui.getValue(mlist, kind, varname, params)
- # Filter out None, and volatile attributes
- if value is None and not varname.startswith('_'):
- value = getattr(mlist, varname)
- # Now create the widget for this value
- if kind == config.Radio or kind == config.Toggle:
- # If we are returning the option for subscribe policy and this site
- # doesn't allow open subscribes, then we have to alter the value of
- # mlist.subscribe_policy as passed to RadioButtonArray in order to
- # compensate for the fact that there is one fewer option.
- # Correspondingly, we alter the value back in the change options
- # function -scott
- #
- # TBD: this is an ugly ugly hack.
- if varname.startswith('_'):
- checked = 0
- else:
- checked = value
- if varname == 'subscribe_policy' and not config.ALLOW_OPEN_SUBSCRIBE:
- checked = checked - 1
- # For Radio buttons, we're going to interpret the extra stuff as a
- # horizontal/vertical flag. For backwards compatibility, the value 0
- # means horizontal, so we use "not extra" to get the parity right.
- return RadioButtonArray(varname, params, checked, not extra)
- elif (kind == config.String or kind == config.Email or
- kind == config.Host or kind == config.Number):
- return TextBox(varname, value, params)
- elif kind == config.Text:
- if params:
- r, c = params
- else:
- r, c = None, None
- return TextArea(varname, value or '', r, c)
- elif kind in (config.EmailList, config.EmailListEx):
- if params:
- r, c = params
- else:
- r, c = None, None
- res = NL.join(value)
- return TextArea(varname, res, r, c, wrap='off')
- elif kind == config.FileUpload:
- # like a text area, but also with uploading
- if params:
- r, c = params
- else:
- r, c = None, None
- container = Container()
- container.AddItem(_('<em>Enter the text below, or...</em><br>'))
- container.AddItem(TextArea(varname, value or '', r, c))
- container.AddItem(_('<br><em>...specify a file to upload</em><br>'))
- container.AddItem(FileUpload(varname+'_upload', r, c))
- return container
- elif kind == config.Select:
- if params:
- values, legend, selected = params
- else:
- codes = mlist.language_codes
- legend = [config.languages.get_description(code) for code in codes]
- selected = codes.index(mlist.preferred_language)
- return SelectOptions(varname, values, legend, selected)
- elif kind == config.Topics:
- # A complex and specialized widget type that allows for setting of a
- # topic name, a mark button, a regexp text box, an "add after mark",
- # and a delete button. Yeesh! params are ignored.
- table = Table(border=0)
- # This adds the html for the entry widget
- def makebox(i, name, pattern, desc, empty=False, table=table):
- deltag = 'topic_delete_%02d' % i
- boxtag = 'topic_box_%02d' % i
- reboxtag = 'topic_rebox_%02d' % i
- desctag = 'topic_desc_%02d' % i
- wheretag = 'topic_where_%02d' % i
- addtag = 'topic_add_%02d' % i
- newtag = 'topic_new_%02d' % i
- if empty:
- table.AddRow([Center(Bold(_('Topic %(i)d'))),
- Hidden(newtag)])
- else:
- table.AddRow([Center(Bold(_('Topic %(i)d'))),
- SubmitButton(deltag, _('Delete'))])
- table.AddRow([Label(_('Topic name:')),
- TextBox(boxtag, value=name, size=30)])
- table.AddRow([Label(_('Regexp:')),
- TextArea(reboxtag, text=pattern,
- rows=4, cols=30, wrap='off')])
- table.AddRow([Label(_('Description:')),
- TextArea(desctag, text=desc,
- rows=4, cols=30, wrap='soft')])
- if not empty:
- table.AddRow([SubmitButton(addtag, _('Add new item...')),
- SelectOptions(wheretag, ('before', 'after'),
- (_('...before this one.'),
- _('...after this one.')),
- selected=1),
- ])
- table.AddRow(['<hr>'])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- # Now for each element in the existing data, create a widget
- i = 1
- data = getattr(mlist, varname)
- for name, pattern, desc, empty in data:
- makebox(i, name, pattern, desc, empty)
- i += 1
- # Add one more non-deleteable widget as the first blank entry, but
- # only if there are no real entries.
- if i == 1:
- makebox(i, '', '', '', empty=True)
- return table
- elif kind == config.HeaderFilter:
- # A complex and specialized widget type that allows for setting of a
- # spam filter rule including, a mark button, a regexp text box, an
- # "add after mark", up and down buttons, and a delete button. Yeesh!
- # params are ignored.
- table = Table(border=0)
- # This adds the html for the entry widget
- def makebox(i, pattern, action, empty=False, table=table):
- deltag = 'hdrfilter_delete_%02d' % i
- reboxtag = 'hdrfilter_rebox_%02d' % i
- actiontag = 'hdrfilter_action_%02d' % i
- wheretag = 'hdrfilter_where_%02d' % i
- addtag = 'hdrfilter_add_%02d' % i
- newtag = 'hdrfilter_new_%02d' % i
- uptag = 'hdrfilter_up_%02d' % i
- downtag = 'hdrfilter_down_%02d' % i
- if empty:
- table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
- Hidden(newtag)])
- else:
- table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
- SubmitButton(deltag, _('Delete'))])
- table.AddRow([Label(_('Spam Filter Regexp:')),
- TextArea(reboxtag, text=pattern,
- rows=4, cols=30, wrap='off')])
- values = [config.DEFER, config.HOLD, config.REJECT,
- config.DISCARD, config.ACCEPT]
- try:
- checked = values.index(action)
- except ValueError:
- checked = 0
- radio = RadioButtonArray(
- actiontag,
- (_('Defer'), _('Hold'), _('Reject'),
- _('Discard'), _('Accept')),
- values=values,
- checked=checked).Format()
- table.AddRow([Label(_('Action:')), radio])
- if not empty:
- table.AddRow([SubmitButton(addtag, _('Add new item...')),
- SelectOptions(wheretag, ('before', 'after'),
- (_('...before this one.'),
- _('...after this one.')),
- selected=1),
- ])
- # BAW: IWBNI we could disable the up and down buttons for the
- # first and last item respectively, but it's not easy to know
- # which is the last item, so let's not worry about that for
- # now.
- table.AddRow([SubmitButton(uptag, _('Move rule up')),
- SubmitButton(downtag, _('Move rule down'))])
- table.AddRow(['<hr>'])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- # Now for each element in the existing data, create a widget
- i = 1
- data = getattr(mlist, varname)
- for pattern, action, empty in data:
- makebox(i, pattern, action, empty)
- i += 1
- # Add one more non-deleteable widget as the first blank entry, but
- # only if there are no real entries.
- if i == 1:
- makebox(i, '', config.DEFER, empty=True)
- return table
- elif kind == config.Checkbox:
- return CheckBoxArray(varname, *params)
- else:
- assert 0, 'Bad gui widget type: %s' % kind
-
-
-
-def get_item_gui_description(mlist, category, subcat,
- varname, descr, elaboration, detailsp):
- # Return the item's description, with link to details.
- #
- # Details are not included if this is a VARHELP page, because that /is/
- # the details page!
- if detailsp:
- if subcat:
- varhelp = '?VARHELP=%s/%s/%s' % (category, subcat, varname)
- else:
- varhelp = '?VARHELP=%s/%s' % (category, varname)
- if descr == elaboration:
- linktext = _('<br>(Edit <b>%(varname)s</b>)')
- else:
- linktext = _('<br>(Details for <b>%(varname)s</b>)')
- link = Link(mlist.GetScriptURL('admin') + varhelp,
- linktext).Format()
- text = Label('%s %s' % (descr, link)).Format()
- else:
- text = Label(descr).Format()
- if varname[0] == '_':
- text += Label(_('''<br><em><strong>Note:</strong>
- setting this value performs an immediate action but does not modify
- permanent state.</em>''')).Format()
- return text
-
-
-
-def membership_options(mlist, subcat, cgidata, doc, form):
- # Show the main stuff
- adminurl = mlist.GetScriptURL('admin')
- container = Container()
- header = Table(width="100%")
- # If we're in the list subcategory, show the membership list
- if subcat == 'add':
- header.AddRow([Center(Header(2, _('Mass Subscriptions')))])
- header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
- container.AddItem(header)
- mass_subscribe(mlist, container)
- return container
- if subcat == 'remove':
- header.AddRow([Center(Header(2, _('Mass Removals')))])
- header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
- container.AddItem(header)
- mass_remove(mlist, container)
- return container
- # Otherwise...
- header.AddRow([Center(Header(2, _('Membership List')))])
- header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
- container.AddItem(header)
- # Add a "search for member" button
- table = Table(width='100%')
- link = Link('http://www.python.org/doc/current/lib/re-syntax.html',
- _('(help)')).Format()
- table.AddRow([Label(_('Find member %(link)s:')),
- TextBox('findmember',
- value=cgidata.getvalue('findmember', '')),
- SubmitButton('findmember_btn', _('Search...'))])
- container.AddItem(table)
- container.AddItem('<hr><p>')
- usertable = Table(width="90%", border='2')
- # If there are more members than allowed by chunksize, then we split the
- # membership up alphabetically. Otherwise just display them all.
- chunksz = mlist.admin_member_chunksize
- # The email addresses had /better/ be ASCII, but might be encoded in the
- # database as Unicodes.
- all = [_m.encode() for _m in mlist.getMembers()]
- all.sort(lambda x, y: cmp(x.lower(), y.lower()))
- # See if the query has a regular expression
- regexp = cgidata.getvalue('findmember', '').strip()
- if regexp:
- try:
- cre = re.compile(regexp, re.IGNORECASE)
- except re.error:
- doc.addError(_('Bad regular expression: ') + regexp)
- else:
- # BAW: There's got to be a more efficient way of doing this!
- names = [mlist.getMemberName(s) or '' for s in all]
- all = [a for n, a in zip(names, all)
- if cre.search(n) or cre.search(a)]
- chunkindex = None
- bucket = None
- actionurl = None
- if len(all) < chunksz:
- members = all
- else:
- # Split them up alphabetically, and then split the alphabetical
- # listing by chunks
- buckets = {}
- for addr in all:
- members = buckets.setdefault(addr[0].lower(), [])
- members.append(addr)
- # Now figure out which bucket we want
- bucket = None
- qs = {}
- # POST methods, even if their actions have a query string, don't get
- # put into FieldStorage's keys :-(
- qsenviron = os.environ.get('QUERY_STRING')
- if qsenviron:
- qs = cgi.parse_qs(qsenviron)
- bucket = qs.get('letter', 'a')[0].lower()
- if bucket not in digits + lowercase:
- bucket = None
- if not bucket or not buckets.has_key(bucket):
- keys = buckets.keys()
- keys.sort()
- bucket = keys[0]
- members = buckets[bucket]
- action = adminurl + '/members?letter=%s' % bucket
- if len(members) <= chunksz:
- form.set_action(action)
- else:
- i, r = divmod(len(members), chunksz)
- numchunks = i + (not not r * 1)
- # Now chunk them up
- chunkindex = 0
- if qs.has_key('chunk'):
- try:
- chunkindex = int(qs['chunk'][0])
- except ValueError:
- chunkindex = 0
- if chunkindex < 0 or chunkindex > numchunks:
- chunkindex = 0
- members = members[chunkindex*chunksz:(chunkindex+1)*chunksz]
- # And set the action URL
- form.set_action(action + '&chunk=%s' % chunkindex)
- # So now members holds all the addresses we're going to display
- allcnt = len(all)
- if bucket:
- membercnt = len(members)
- usertable.AddRow([Center(Italic(_(
- '%(allcnt)s members total, %(membercnt)s shown')))])
- else:
- usertable.AddRow([Center(Italic(_('%(allcnt)s members total')))])
- usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
- usertable.GetCurrentCellIndex(),
- colspan=OPTCOLUMNS,
- bgcolor=config.WEB_ADMINITEM_COLOR)
- # Add the alphabetical links
- if bucket:
- cells = []
- for letter in digits + lowercase:
- if not buckets.get(letter):
- continue
- url = adminurl + '/members?letter=%s' % letter
- if letter == bucket:
- show = Bold('[%s]' % letter.upper()).Format()
- else:
- show = letter.upper()
- cells.append(Link(url, show).Format())
- joiner = '&nbsp;'*2 + '\n'
- usertable.AddRow([Center(joiner.join(cells))])
- usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
- usertable.GetCurrentCellIndex(),
- colspan=OPTCOLUMNS,
- bgcolor=config.WEB_ADMINITEM_COLOR)
- usertable.AddRow([Center(h) for h in (_('unsub'),
- _('member address<br>member name'),
- _('mod'), _('hide'),
- _('nomail<br>[reason]'),
- _('ack'), _('not metoo'),
- _('nodupes'),
- _('digest'), _('plain'),
- _('language'))])
- rowindex = usertable.GetCurrentRowIndex()
- for i in range(OPTCOLUMNS):
- usertable.AddCellInfo(rowindex, i, bgcolor=config.WEB_ADMINITEM_COLOR)
- # Find the longest name in the list
- longest = 0
- if members:
- names = filter(None, [mlist.getMemberName(s) for s in members])
- # Make the name field at least as long as the longest email address
- longest = max([len(s) for s in names + members])
- # Abbreviations for delivery status details
- ds_abbrevs = {MemberAdaptor.UNKNOWN : _('?'),
- MemberAdaptor.BYUSER : _('U'),
- MemberAdaptor.BYADMIN : _('A'),
- MemberAdaptor.BYBOUNCE: _('B'),
- }
- # Now populate the rows
- for addr in members:
- link = Link(mlist.GetOptionsURL(addr, obscure=1),
- mlist.getMemberCPAddress(addr))
- fullname = mlist.getMemberName(addr)
- name = TextBox(addr + '_realname', fullname, size=longest).Format()
- cells = [Center(CheckBox(addr + '_unsub', 'off', 0).Format()),
- link.Format() + '<br>' +
- name +
- Hidden('user', urllib.quote(addr)).Format(),
- ]
- # Do the `mod' option
- if mlist.getMemberOption(addr, config.Moderate):
- value = 'on'
- checked = 1
- else:
- value = 'off'
- checked = 0
- box = CheckBox('%s_mod' % addr, value, checked)
- cells.append(Center(box).Format())
- for opt in ('hide', 'nomail', 'ack', 'notmetoo', 'nodupes'):
- extra = ''
- if opt == 'nomail':
- status = mlist.getDeliveryStatus(addr)
- if status == MemberAdaptor.ENABLED:
- value = 'off'
- checked = 0
- else:
- value = 'on'
- checked = 1
- extra = '[%s]' % ds_abbrevs[status]
- elif mlist.getMemberOption(addr, config.OPTINFO[opt]):
- value = 'on'
- checked = 1
- else:
- value = 'off'
- checked = 0
- box = CheckBox('%s_%s' % (addr, opt), value, checked)
- cells.append(Center(box.Format() + extra))
- # This code is less efficient than the original which did a has_key on
- # the underlying dictionary attribute. This version is slower and
- # less memory efficient. It points to a new MemberAdaptor interface
- # method.
- if addr in mlist.getRegularMemberKeys():
- cells.append(Center(CheckBox(addr + '_digest', 'off', 0).Format()))
- else:
- cells.append(Center(CheckBox(addr + '_digest', 'on', 1).Format()))
- if mlist.getMemberOption(addr, config.OPTINFO['plain']):
- value = 'on'
- checked = 1
- else:
- value = 'off'
- checked = 0
- cells.append(Center(CheckBox('%s_plain' % addr, value, checked)))
- # User's preferred language
- langpref = mlist.getMemberLanguage(addr)
- langs = mlist.language_codes
- langdescs = [_(config.languges.get_description(code))
- for code in langs]
- try:
- selected = langs.index(langpref)
- except ValueError:
- selected = 0
- cells.append(Center(SelectOptions(addr + '_language', langs,
- langdescs, selected)).Format())
- usertable.AddRow(cells)
- # Add the usertable and a legend
- legend = UnorderedList()
- legend.AddItem(
- _('<b>unsub</b> -- Click on this to unsubscribe the member.'))
- legend.AddItem(
- _("""<b>mod</b> -- The user's personal moderation flag. If this is
- set, postings from them will be moderated, otherwise they will be
- approved."""))
- legend.AddItem(
- _("""<b>hide</b> -- Is the member's address concealed on
- the list of subscribers?"""))
- legend.AddItem(_(
- """<b>nomail</b> -- Is delivery to the member disabled? If so, an
- abbreviation will be given describing the reason for the disabled
- delivery:
- <ul><li><b>U</b> -- Delivery was disabled by the user via their
- personal options page.
- <li><b>A</b> -- Delivery was disabled by the list
- administrators.
- <li><b>B</b> -- Delivery was disabled by the system due to
- excessive bouncing from the member's address.
- <li><b>?</b> -- The reason for disabled delivery isn't known.
- This is the case for all memberships which were disabled
- in older versions of Mailman.
- </ul>"""))
- legend.AddItem(
- _('''<b>ack</b> -- Does the member get acknowledgements of their
- posts?'''))
- legend.AddItem(
- _('''<b>not metoo</b> -- Does the member want to avoid copies of their
- own postings?'''))
- legend.AddItem(
- _('''<b>nodupes</b> -- Does the member want to avoid duplicates of the
- same message?'''))
- legend.AddItem(
- _('''<b>digest</b> -- Does the member get messages in digests?
- (otherwise, individual messages)'''))
- legend.AddItem(
- _('''<b>plain</b> -- If getting digests, does the member get plain
- text digests? (otherwise, MIME)'''))
- legend.AddItem(_("<b>language</b> -- Language preferred by the user"))
- addlegend = ''
- parsedqs = 0
- qsenviron = os.environ.get('QUERY_STRING')
- if qsenviron:
- qs = cgi.parse_qs(qsenviron).get('legend')
- if qs and isinstance(qs, list):
- qs = qs[0]
- if qs == 'yes':
- addlegend = 'legend=yes&'
- if addlegend:
- container.AddItem(legend.Format() + '<p>')
- container.AddItem(
- Link(adminurl + '/members/list',
- _('Click here to hide the legend for this table.')))
- else:
- container.AddItem(
- Link(adminurl + '/members/list?legend=yes',
- _('Click here to include the legend for this table.')))
- container.AddItem(Center(usertable))
-
- # There may be additional chunks
- if chunkindex is not None:
- buttons = []
- url = adminurl + '/members?%sletter=%s&' % (addlegend, bucket)
- footer = _('''<p><em>To view more members, click on the appropriate
- range listed below:</em>''')
- chunkmembers = buckets[bucket]
- last = len(chunkmembers)
- for i in range(numchunks):
- if i == chunkindex:
- continue
- start = chunkmembers[i*chunksz]
- end = chunkmembers[min((i+1)*chunksz, last)-1]
- link = Link(url + 'chunk=%d' % i, _('from %(start)s to %(end)s'))
- buttons.append(link)
- buttons = UnorderedList(*buttons)
- container.AddItem(footer + buttons.Format() + '<p>')
- return container
-
-
-
-def mass_subscribe(mlist, container):
- # MASS SUBSCRIBE
- GREY = config.WEB_ADMINITEM_COLOR
- table = Table(width='90%')
- table.AddRow([
- Label(_('Subscribe these users now or invite them?')),
- RadioButtonArray('subscribe_or_invite',
- (_('Subscribe'), _('Invite')),
- 0, values=(0, 1))
- ])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
- table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
- table.AddRow([
- Label(_('Send welcome messages to new subscribees?')),
- RadioButtonArray('send_welcome_msg_to_this_batch',
- (_('No'), _('Yes')),
- mlist.send_welcome_msg,
- values=(0, 1))
- ])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
- table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
- table.AddRow([
- Label(_('Send notifications of new subscriptions to the list owner?')),
- RadioButtonArray('send_notifications_to_list_owner',
- (_('No'), _('Yes')),
- mlist.admin_notify_mchanges,
- values=(0,1))
- ])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
- table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
- table.AddRow([Italic(_('Enter one address per line below...'))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Center(TextArea(name='subscribees',
- rows=10, cols='70%', wrap=None))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
- FileUpload('subscribees_upload', cols='50')])
- container.AddItem(Center(table))
- # Invitation text
- table.AddRow(['&nbsp;', '&nbsp;'])
- table.AddRow([Italic(_("""Below, enter additional text to be added to the
- top of your invitation or the subscription notification. Include at least
- one blank line at the end..."""))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Center(TextArea(name='invitation',
- rows=10, cols='70%', wrap=None))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
-
-
-
-def mass_remove(mlist, container):
- # MASS UNSUBSCRIBE
- GREY = config.WEB_ADMINITEM_COLOR
- table = Table(width='90%')
- table.AddRow([
- Label(_('Send unsubscription acknowledgement to the user?')),
- RadioButtonArray('send_unsub_ack_to_this_batch',
- (_('No'), _('Yes')),
- 0, values=(0, 1))
- ])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
- table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
- table.AddRow([
- Label(_('Send notifications to the list owner?')),
- RadioButtonArray('send_unsub_notifications_to_list_owner',
- (_('No'), _('Yes')),
- mlist.admin_notify_mchanges,
- values=(0, 1))
- ])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
- table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
- table.AddRow([Italic(_('Enter one address per line below...'))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Center(TextArea(name='unsubscribees',
- rows=10, cols='70%', wrap=None))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
- FileUpload('unsubscribees_upload', cols='50')])
- container.AddItem(Center(table))
-
-
-
-def password_inputs(mlist):
- adminurl = mlist.GetScriptURL('admin')
- table = Table(cellspacing=3, cellpadding=4)
- table.AddRow([Center(Header(2, _('Change list ownership passwords')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
- table.AddRow([_("""\
-The <em>list administrators</em> are the people who have ultimate control over
-all parameters of this mailing list. They are able to change any list
-configuration variable available through these administration web pages.
-
-<p>The <em>list moderators</em> have more limited permissions; they are not
-able to change any list configuration variable, but they are allowed to tend
-to pending administration requests, including approving or rejecting held
-subscription requests, and disposing of held postings. Of course, the
-<em>list administrators</em> can also tend to pending requests.
-
-<p>In order to split the list ownership duties into administrators and
-moderators, you must set a separate moderator password in the fields below,
-and also provide the email addresses of the list moderators in the
-<a href="%(adminurl)s/general">general options section</a>.""")])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- # Set up the admin password table on the left
- atable = Table(border=0, cellspacing=3, cellpadding=4,
- bgcolor=config.WEB_ADMINPW_COLOR)
- atable.AddRow([Label(_('Enter new administrator password:')),
- PasswordBox('newpw', size=20)])
- atable.AddRow([Label(_('Confirm administrator password:')),
- PasswordBox('confirmpw', size=20)])
- # Set up the moderator password table on the right
- mtable = Table(border=0, cellspacing=3, cellpadding=4,
- bgcolor=config.WEB_ADMINPW_COLOR)
- mtable.AddRow([Label(_('Enter new moderator password:')),
- PasswordBox('newmodpw', size=20)])
- mtable.AddRow([Label(_('Confirm moderator password:')),
- PasswordBox('confirmmodpw', size=20)])
- # Add these tables to the overall password table
- table.AddRow([atable, mtable])
- return table
-
-
-
-def submit_button(name='submit'):
- table = Table(border=0, cellspacing=0, cellpadding=2)
- table.AddRow([Bold(SubmitButton(name, _('Submit Your Changes')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, align='middle')
- return table
-
-
-
-def change_options(mlist, category, subcat, cgidata, doc):
- def safeint(formvar, defaultval=None):
- try:
- return int(cgidata.getvalue(formvar))
- except (ValueError, TypeError):
- return defaultval
- confirmed = 0
- # Handle changes to the list moderator password. Do this before checking
- # the new admin password, since the latter will force a reauthentication.
- new = cgidata.getvalue('newmodpw', '').strip()
- confirm = cgidata.getvalue('confirmmodpw', '').strip()
- if new or confirm:
- if new == confirm:
- mlist.mod_password = passwords.make_secret(
- new, config.PASSWORD_SCHEME)
- # No re-authentication necessary because the moderator's
- # password doesn't get you into these pages.
- else:
- doc.addError(_('Moderator passwords did not match'))
- # Handle changes to the list administrator password
- new = cgidata.getvalue('newpw', '').strip()
- confirm = cgidata.getvalue('confirmpw', '').strip()
- if new or confirm:
- if new == confirm:
- mlist.password = passwords.make_secret(new, config.PASSWORD_SCHEME)
- # Set new cookie
- print mlist.MakeCookie(config.AuthListAdmin)
- else:
- doc.addError(_('Administrator passwords did not match'))
- # Give the individual gui item a chance to process the form data
- categories = mlist.GetConfigCategories()
- label, gui = categories[category]
- # BAW: We handle the membership page special... for now.
- if category <> 'members':
- gui.handleForm(mlist, category, subcat, cgidata, doc)
- # mass subscription, removal processing for members category
- subscribers = ''
- subscribers += cgidata.getvalue('subscribees', '')
- subscribers += cgidata.getvalue('subscribees_upload', '')
- if subscribers:
- entries = filter(None, [n.strip() for n in subscribers.splitlines()])
- send_welcome_msg = safeint('send_welcome_msg_to_this_batch',
- mlist.send_welcome_msg)
- send_admin_notif = safeint('send_notifications_to_list_owner',
- mlist.admin_notify_mchanges)
- # Default is to subscribe
- subscribe_or_invite = safeint('subscribe_or_invite', 0)
- invitation = cgidata.getvalue('invitation', '')
- digest = mlist.digest_is_default
- if not mlist.digestable:
- digest = 0
- if not mlist.nondigestable:
- digest = 1
- subscribe_errors = []
- subscribe_success = []
- # Now cruise through all the subscribees and do the deed. BAW: we
- # should limit the number of "Successfully subscribed" status messages
- # we display. Try uploading a file with 10k names -- it takes a while
- # to render the status page.
- for entry in entries:
- fullname, address = parseaddr(entry)
- # Canonicalize the full name
- fullname = Utils.canonstr(fullname, mlist.preferred_language)
- userdesc = UserDesc(address, fullname,
- Utils.MakeRandomPassword(),
- digest, mlist.preferred_language)
- try:
- if subscribe_or_invite:
- if mlist.isMember(address):
- raise Errors.MMAlreadyAMember
- else:
- mlist.InviteNewMember(userdesc, invitation)
- else:
- mlist.ApprovedAddMember(userdesc, send_welcome_msg,
- send_admin_notif, invitation,
- whence='admin mass sub')
- except Errors.MMAlreadyAMember:
- subscribe_errors.append((entry, _('Already a member')))
- except Errors.InvalidEmailAddress:
- if userdesc.address == '':
- subscribe_errors.append((_('&lt;blank line&gt;'),
- _('Bad/Invalid email address')))
- else:
- subscribe_errors.append((entry,
- _('Bad/Invalid email address')))
- except Errors.MembershipIsBanned, pattern:
- subscribe_errors.append(
- (entry, _('Banned address (matched %(pattern)s)')))
- else:
- member = Utils.uncanonstr(formataddr((fullname, address)))
- subscribe_success.append(Utils.websafe(member))
- if subscribe_success:
- if subscribe_or_invite:
- doc.AddItem(Header(5, _('Successfully invited:')))
- else:
- doc.AddItem(Header(5, _('Successfully subscribed:')))
- doc.AddItem(UnorderedList(*subscribe_success))
- doc.AddItem('<p>')
- if subscribe_errors:
- if subscribe_or_invite:
- doc.AddItem(Header(5, _('Error inviting:')))
- else:
- doc.AddItem(Header(5, _('Error subscribing:')))
- items = ['%s -- %s' % (x0, x1) for x0, x1 in subscribe_errors]
- doc.AddItem(UnorderedList(*items))
- doc.AddItem('<p>')
- # Unsubscriptions
- removals = ''
- if cgidata.has_key('unsubscribees'):
- removals += cgidata['unsubscribees'].value
- if cgidata.has_key('unsubscribees_upload') and \
- cgidata['unsubscribees_upload'].value:
- removals += cgidata['unsubscribees_upload'].value
- if removals:
- names = filter(None, [n.strip() for n in removals.splitlines()])
- send_unsub_notifications = int(
- cgidata['send_unsub_notifications_to_list_owner'].value)
- userack = int(
- cgidata['send_unsub_ack_to_this_batch'].value)
- unsubscribe_errors = []
- unsubscribe_success = []
- for addr in names:
- try:
- mlist.ApprovedDeleteMember(
- addr, whence='admin mass unsub',
- admin_notif=send_unsub_notifications,
- userack=userack)
- unsubscribe_success.append(addr)
- except Errors.NotAMemberError:
- unsubscribe_errors.append(addr)
- if unsubscribe_success:
- doc.AddItem(Header(5, _('Successfully Unsubscribed:')))
- doc.AddItem(UnorderedList(*unsubscribe_success))
- doc.AddItem('<p>')
- if unsubscribe_errors:
- doc.AddItem(Header(3, Bold(FontAttr(
- _('Cannot unsubscribe non-members:'),
- color='#ff0000', size='+2')).Format()))
- doc.AddItem(UnorderedList(*unsubscribe_errors))
- doc.AddItem('<p>')
- # See if this was a moderation bit operation
- if cgidata.has_key('allmodbit_btn'):
- val = cgidata.getvalue('allmodbit_val')
- try:
- val = int(val)
- except VallueError:
- val = None
- if val not in (0, 1):
- doc.addError(_('Bad moderation flag value'))
- else:
- for member in mlist.getMembers():
- mlist.setMemberOption(member, config.Moderate, val)
- # do the user options for members category
- if cgidata.has_key('setmemberopts_btn') and cgidata.has_key('user'):
- user = cgidata['user']
- if isinstance(user, list):
- users = []
- for ui in range(len(user)):
- users.append(urllib.unquote(user[ui].value))
- else:
- users = [urllib.unquote(user.value)]
- errors = []
- removes = []
- for user in users:
- if cgidata.has_key('%s_unsub' % user):
- try:
- mlist.ApprovedDeleteMember(user, whence='member mgt page')
- removes.append(user)
- except Errors.NotAMemberError:
- errors.append((user, _('Not subscribed')))
- continue
- if not mlist.isMember(user):
- doc.addError(_('Ignoring changes to deleted member: %(user)s'),
- tag=_('Warning: '))
- continue
- value = cgidata.has_key('%s_digest' % user)
- try:
- mlist.setMemberOption(user, config.Digests, value)
- except (Errors.AlreadyReceivingDigests,
- Errors.AlreadyReceivingRegularDeliveries,
- Errors.CantDigestError,
- Errors.MustDigestError):
- # BAW: Hmm...
- pass
-
- newname = cgidata.getvalue(user+'_realname', '')
- newname = Utils.canonstr(newname, mlist.preferred_language)
- mlist.setMemberName(user, newname)
-
- newlang = cgidata.getvalue(user+'_language')
- oldlang = mlist.getMemberLanguage(user)
- if (newlang not in config.languages.enabled_codes
- and newlang <> oldlang):
- # Then
- mlist.setMemberLanguage(user, newlang)
-
- moderate = not not cgidata.getvalue(user+'_mod')
- mlist.setMemberOption(user, config.Moderate, moderate)
-
- # Set the `nomail' flag, but only if the user isn't already
- # disabled (otherwise we might change BYUSER into BYADMIN).
- if cgidata.has_key('%s_nomail' % user):
- if mlist.getDeliveryStatus(user) == MemberAdaptor.ENABLED:
- mlist.setDeliveryStatus(user, MemberAdaptor.BYADMIN)
- else:
- mlist.setDeliveryStatus(user, MemberAdaptor.ENABLED)
- for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain'):
- opt_code = config.OPTINFO[opt]
- if cgidata.has_key('%s_%s' % (user, opt)):
- mlist.setMemberOption(user, opt_code, 1)
- else:
- mlist.setMemberOption(user, opt_code, 0)
- # Give some feedback on who's been removed
- if removes:
- doc.AddItem(Header(5, _('Successfully Removed:')))
- doc.AddItem(UnorderedList(*removes))
- doc.AddItem('<p>')
- if errors:
- doc.AddItem(Header(5, _("Error Unsubscribing:")))
- items = ['%s -- %s' % (x[0], x[1]) for x in errors]
- doc.AddItem(apply(UnorderedList, tuple((items))))
- doc.AddItem("<p>")
diff --git a/src/web/Cgi/admindb.py b/src/web/Cgi/admindb.py
deleted file mode 100644
index 5d756fc78..000000000
--- a/src/web/Cgi/admindb.py
+++ /dev/null
@@ -1,813 +0,0 @@
-# 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/>.
-
-"""Produce and process the pending-approval items for a list."""
-
-import os
-import cgi
-import sys
-import time
-import email
-import errno
-import logging
-
-from urllib import quote_plus, unquote_plus
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Message
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.Cgi import Auth
-from Mailman.Handlers.Moderate import ModeratedMemberPost
-from Mailman.ListAdmin import readMessage
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-from Mailman.interfaces import RequestType
-
-EMPTYSTRING = ''
-NL = '\n'
-
-# Set up i18n. Until we know which list is being requested, we use the
-# server's default.
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-EXCERPT_HEIGHT = 10
-EXCERPT_WIDTH = 76
-
-log = logging.getLogger('mailman.error')
-
-
-
-def helds_by_sender(mlist):
- bysender = {}
- requests = config.db.get_list_requests(mlist)
- for request in requests.of_type(RequestType.held_message):
- key, data = requests.get_request(request.id)
- sender = data.get('sender')
- assert sender is not None, (
- 'No sender for held message: %s' % request.id)
- bysender.setdefault(sender, []).append(request.id)
- return bysender
-
-
-def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3):
- # We can't use a RadioButtonArray here because horizontal placement can be
- # confusing to the user and vertical placement takes up too much
- # real-estate. This is a hack!
- space = '&nbsp;' * spacing
- btns = Table(cellspacing='5', cellpadding='0')
- btns.AddRow([space + text + space for text in labels])
- btns.AddRow([Center(RadioButton(btnname, value, default))
- for value, default in zip(values, defaults)])
- return btns
-
-
-
-def main():
- # Figure out which list is being requested
- parts = Utils.GetPathPieces()
- if not parts:
- handle_no_list()
- return
-
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- handle_no_list(_('No such list <em>%(safelistname)s</em>'))
- log.error('No such list "%s": %s\n', listname, e)
- return
-
- # Now that we know which list to use, set the system's language to it.
- i18n.set_language(mlist.preferred_language)
-
- # Make sure the user is authorized to see this page.
- cgidata = cgi.FieldStorage(keep_blank_values=1)
-
- if not mlist.WebAuthenticate((config.AuthListAdmin,
- config.AuthListModerator,
- config.AuthSiteAdmin),
- cgidata.getvalue('adminpw', '')):
- if cgidata.has_key('adminpw'):
- # This is a re-authorization attempt
- msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
- else:
- msg = ''
- Auth.loginpage(mlist, 'admindb', msg=msg)
- return
-
- # Set up the results document
- doc = Document()
- doc.set_language(mlist.preferred_language)
-
- # See if we're requesting all the messages for a particular sender, or if
- # we want a specific held message.
- sender = None
- msgid = None
- details = None
- envar = os.environ.get('QUERY_STRING')
- if envar:
- # POST methods, even if their actions have a query string, don't get
- # put into FieldStorage's keys :-(
- qs = cgi.parse_qs(envar).get('sender')
- if qs and isinstance(qs, list):
- sender = qs[0]
- qs = cgi.parse_qs(envar).get('msgid')
- if qs and isinstance(qs, list):
- msgid = qs[0]
- qs = cgi.parse_qs(envar).get('details')
- if qs and isinstance(qs, list):
- details = qs[0]
-
- mlist.Lock()
- try:
- realname = mlist.real_name
- if not cgidata.keys() or cgidata.has_key('admlogin'):
- # If this is not a form submission (i.e. there are no keys in the
- # form) or it's a login, then we don't need to do much special.
- doc.SetTitle(_('%(realname)s Administrative Database'))
- elif not details:
- # This is a form submission
- doc.SetTitle(_('%(realname)s Administrative Database Results'))
- process_form(mlist, doc, cgidata)
- # Now print the results and we're done. Short circuit for when there
- # are no pending requests, but be sure to save the results!
- if config.db.requests.get_list_requests(mlist).count == 0:
- title = _('%(realname)s Administrative Database')
- doc.SetTitle(title)
- doc.AddItem(Header(2, title))
- doc.AddItem(_('There are no pending requests.'))
- doc.AddItem(' ')
- doc.AddItem(Link(mlist.GetScriptURL('admindb'),
- _('Click here to reload this page.')))
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- mlist.Save()
- return
-
- admindburl = mlist.GetScriptURL('admindb')
- form = Form(admindburl)
- # Add the instructions template
- if details == 'instructions':
- doc.AddItem(Header(
- 2, _('Detailed instructions for the administrative database')))
- else:
- doc.AddItem(Header(
- 2,
- _('Administrative requests for mailing list:')
- + ' <em>%s</em>' % mlist.real_name))
- if details <> 'instructions':
- form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
- requestsdb = config.db.get_list_requests(mlist)
- message_count = requestsdb.count_of(RequestType.held_message)
- if not (details or sender or msgid or message_count == 0):
- form.AddItem(Center(
- CheckBox('discardalldefersp', 0).Format() +
- '&nbsp;' +
- _('Discard all messages marked <em>Defer</em>')
- ))
- # Add a link back to the overview, if we're not viewing the overview!
- adminurl = mlist.GetScriptURL('admin')
- d = {'listname' : mlist.real_name,
- 'detailsurl': admindburl + '?details=instructions',
- 'summaryurl': admindburl,
- 'viewallurl': admindburl + '?details=all',
- 'adminurl' : adminurl,
- 'filterurl' : adminurl + '/privacy/sender',
- }
- addform = 1
- if sender:
- esender = Utils.websafe(sender)
- d['description'] = _("all of %(esender)s's held messages.")
- doc.AddItem(Utils.maketext('admindbpreamble.html', d,
- raw=1, mlist=mlist))
- show_sender_requests(mlist, form, sender)
- elif msgid:
- d['description'] = _('a single held message.')
- doc.AddItem(Utils.maketext('admindbpreamble.html', d,
- raw=1, mlist=mlist))
- show_message_requests(mlist, form, msgid)
- elif details == 'all':
- d['description'] = _('all held messages.')
- doc.AddItem(Utils.maketext('admindbpreamble.html', d,
- raw=1, mlist=mlist))
- show_detailed_requests(mlist, form)
- elif details == 'instructions':
- doc.AddItem(Utils.maketext('admindbdetails.html', d,
- raw=1, mlist=mlist))
- addform = 0
- else:
- # Show a summary of all requests
- doc.AddItem(Utils.maketext('admindbsummary.html', d,
- raw=1, mlist=mlist))
- num = show_pending_subs(mlist, form)
- num += show_pending_unsubs(mlist, form)
- num += show_helds_overview(mlist, form)
- addform = num > 0
- # Finish up the document, adding buttons to the form
- if addform:
- doc.AddItem(form)
- form.AddItem('<hr>')
- if not (details or sender or msgid or nomessages):
- form.AddItem(Center(
- CheckBox('discardalldefersp', 0).Format() +
- '&nbsp;' +
- _('Discard all messages marked <em>Defer</em>')
- ))
- form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- # Commit all changes
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def handle_no_list(msg=''):
- # Print something useful if no list was given.
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- header = _('Mailman Administrative Database Error')
- doc.SetTitle(header)
- doc.AddItem(Header(2, header))
- doc.AddItem(msg)
- url = Utils.ScriptURL('admin')
- link = Link(url, _('list of available mailing lists.')).Format()
- doc.AddItem(_('You must specify a list name. Here is the %(link)s'))
- doc.AddItem('<hr>')
- doc.AddItem(MailmanLogo())
- print doc.Format()
-
-
-
-def show_pending_subs(mlist, form):
- # Add the subscription request section
- requestsdb = config.db.get_list_requests(mlist)
- if requestsdb.count_of(RequestType.subscription) == 0:
- return 0
- form.AddItem('<hr>')
- form.AddItem(Center(Header(2, _('Subscription Requests'))))
- table = Table(border=2)
- table.AddRow([Center(Bold(_('Address/name'))),
- Center(Bold(_('Your decision'))),
- Center(Bold(_('Reason for refusal')))
- ])
- # Alphabetical order by email address
- byaddrs = {}
- for request in requestsdb.of_type(RequestType.subscription):
- key, data = requestsdb.get_request(requst.id)
- addr = data['addr']
- byaddrs.setdefault(addr, []).append(request.id)
- addrs = sorted(byaddrs)
- num = 0
- for addr, ids in byaddrs.items():
- # Eliminate duplicates
- for id in ids[1:]:
- mlist.HandleRequest(id, config.DISCARD)
- id = ids[0]
- key, data = requestsdb.get_request(id)
- time = data['time']
- addr = data['addr']
- fullname = data['fullname']
- passwd = data['passwd']
- digest = data['digest']
- lang = data['lang']
- fullname = Utils.uncanonstr(fullname, mlist.preferred_language)
- radio = RadioButtonArray(id, (_('Defer'),
- _('Approve'),
- _('Reject'),
- _('Discard')),
- values=(config.DEFER,
- config.SUBSCRIBE,
- config.REJECT,
- config.DISCARD),
- checked=0).Format()
- if addr not in mlist.ban_list:
- radio += '<br>' + CheckBox('ban-%d' % id, 1).Format() + \
- '&nbsp;' + _('Permanently ban from this list')
- # While the address may be a unicode, it must be ascii
- paddr = addr.encode('us-ascii', 'replace')
- table.AddRow(['%s<br><em>%s</em>' % (paddr, fullname),
- radio,
- TextBox('comment-%d' % id, size=40)
- ])
- num += 1
- if num > 0:
- form.AddItem(table)
- return num
-
-
-
-def show_pending_unsubs(mlist, form):
- # Add the pending unsubscription request section
- lang = mlist.preferred_language
- requestsdb = config.db.get_list_requests(mlist)
- if requestsdb.count_of(RequestType.unsubscription) == 0:
- return 0
- table = Table(border=2)
- table.AddRow([Center(Bold(_('User address/name'))),
- Center(Bold(_('Your decision'))),
- Center(Bold(_('Reason for refusal')))
- ])
- # Alphabetical order by email address
- byaddrs = {}
- for request in requestsdb.of_type(RequestType.unsubscription):
- key, data = requestsdb.get_request(request.id)
- addr = data['addr']
- byaddrs.setdefault(addr, []).append(request.id)
- addrs = sorted(byaddrs)
- num = 0
- for addr, ids in byaddrs.items():
- # Eliminate duplicates
- for id in ids[1:]:
- mlist.HandleRequest(id, config.DISCARD)
- id = ids[0]
- key, data = requestsdb.get_record(id)
- addr = data['addr']
- try:
- fullname = Utils.uncanonstr(mlist.getMemberName(addr), lang)
- except Errors.NotAMemberError:
- # They must have been unsubscribed elsewhere, so we can just
- # discard this record.
- mlist.HandleRequest(id, config.DISCARD)
- continue
- num += 1
- table.AddRow(['%s<br><em>%s</em>' % (addr, fullname),
- RadioButtonArray(id, (_('Defer'),
- _('Approve'),
- _('Reject'),
- _('Discard')),
- values=(config.DEFER,
- config.UNSUBSCRIBE,
- config.REJECT,
- config.DISCARD),
- checked=0),
- TextBox('comment-%d' % id, size=45)
- ])
- if num > 0:
- form.AddItem('<hr>')
- form.AddItem(Center(Header(2, _('Unsubscription Requests'))))
- form.AddItem(table)
- return num
-
-
-
-def show_helds_overview(mlist, form):
- # Sort the held messages by sender
- bysender = helds_by_sender(mlist)
- if not bysender:
- return 0
- form.AddItem('<hr>')
- form.AddItem(Center(Header(2, _('Held Messages'))))
- # Add the by-sender overview tables
- admindburl = mlist.GetScriptURL('admindb')
- table = Table(border=0)
- form.AddItem(table)
- senders = bysender.keys()
- senders.sort()
- for sender in senders:
- qsender = quote_plus(sender)
- esender = Utils.websafe(sender)
- senderurl = admindburl + '?sender=' + qsender
- # The encompassing sender table
- stable = Table(border=1)
- stable.AddRow([Center(Bold(_('From:')).Format() + esender)])
- stable.AddCellInfo(stable.GetCurrentRowIndex(), 0, colspan=2)
- left = Table(border=0)
- left.AddRow([_('Action to take on all these held messages:')])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- btns = hacky_radio_buttons(
- 'senderaction-' + qsender,
- (_('Defer'), _('Accept'), _('Reject'), _('Discard')),
- (config.DEFER, config.APPROVE, config.REJECT, config.DISCARD),
- (1, 0, 0, 0))
- left.AddRow([btns])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- left.AddRow([
- CheckBox('senderpreserve-' + qsender, 1).Format() +
- '&nbsp;' +
- _('Preserve messages for the site administrator')
- ])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- left.AddRow([
- CheckBox('senderforward-' + qsender, 1).Format() +
- '&nbsp;' +
- _('Forward messages (individually) to:')
- ])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- left.AddRow([
- TextBox('senderforwardto-' + qsender,
- value=mlist.GetOwnerEmail())
- ])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- # If the sender is a member and the message is being held due to a
- # moderation bit, give the admin a chance to clear the member's mod
- # bit. If this sender is not a member and is not already on one of
- # the sender filters, then give the admin a chance to add this sender
- # to one of the filters.
- if mlist.isMember(sender):
- if mlist.getMemberOption(sender, config.Moderate):
- left.AddRow([
- CheckBox('senderclearmodp-' + qsender, 1).Format() +
- '&nbsp;' +
- _("Clear this member's <em>moderate</em> flag")
- ])
- else:
- left.AddRow(
- [_('<em>The sender is now a member of this list</em>')])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- elif sender not in (mlist.accept_these_nonmembers +
- mlist.hold_these_nonmembers +
- mlist.reject_these_nonmembers +
- mlist.discard_these_nonmembers):
- left.AddRow([
- CheckBox('senderfilterp-' + qsender, 1).Format() +
- '&nbsp;' +
- _('Add <b>%(esender)s</b> to one of these sender filters:')
- ])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- btns = hacky_radio_buttons(
- 'senderfilter-' + qsender,
- (_('Accepts'), _('Holds'), _('Rejects'), _('Discards')),
- (config.ACCEPT, config.HOLD, config.REJECT, config.DISCARD),
- (0, 0, 0, 1))
- left.AddRow([btns])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- if sender not in mlist.ban_list:
- left.AddRow([
- CheckBox('senderbanp-' + qsender, 1).Format() +
- '&nbsp;' +
- _("""Ban <b>%(esender)s</b> from ever subscribing to this
- mailing list""")])
- left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
- right = Table(border=0)
- right.AddRow([
- _("""Click on the message number to view the individual
- message, or you can """) +
- Link(senderurl, _('view all messages from %(esender)s')).Format()
- ])
- right.AddCellInfo(right.GetCurrentRowIndex(), 0, colspan=2)
- right.AddRow(['&nbsp;', '&nbsp;'])
- counter = 1
- for id in bysender[sender]:
- key, data = requestsdb.get_record(id)
- ptime = data['ptime']
- sender = data['sender']
- subject = data['subject']
- reason = data['reason']
- filename = data['filename']
- msgdata = data['msgdata']
- # BAW: This is really the size of the message pickle, which should
- # be close, but won't be exact. Sigh, good enough.
- try:
- size = os.path.getsize(os.path.join(config.DATA_DIR, filename))
- except OSError, e:
- if e.errno <> errno.ENOENT: raise
- # This message must have gotten lost, i.e. it's already been
- # handled by the time we got here.
- mlist.HandleRequest(id, config.DISCARD)
- continue
- dispsubj = Utils.oneline(
- subject, Utils.GetCharSet(mlist.preferred_language))
- t = Table(border=0)
- t.AddRow([Link(admindburl + '?msgid=%d' % id, '[%d]' % counter),
- Bold(_('Subject:')),
- Utils.websafe(dispsubj)
- ])
- t.AddRow(['&nbsp;', Bold(_('Size:')), str(size) + _(' bytes')])
- if reason:
- reason = _(reason)
- else:
- reason = _('not available')
- t.AddRow(['&nbsp;', Bold(_('Reason:')), reason])
- # Include the date we received the message, if available
- when = msgdata.get('received_time')
- if when:
- t.AddRow(['&nbsp;', Bold(_('Received:')),
- time.ctime(when)])
- counter += 1
- right.AddRow([t])
- stable.AddRow([left, right])
- table.AddRow([stable])
- return 1
-
-
-
-def show_sender_requests(mlist, form, sender):
- bysender = helds_by_sender(mlist)
- if not bysender:
- return
- sender_ids = bysender.get(sender)
- if sender_ids is None:
- # BAW: should we print an error message?
- return
- total = len(sender_ids)
- requestsdb = config.db.get_list_requests(mlist)
- for i, id in enumerate(sender_ids):
- key, data = requestsdb.get_record(id)
- show_post_requests(mlist, id, data, total, count + 1, form)
-
-
-
-def show_message_requests(mlist, form, id):
- requestdb = config.db.get_list_requests(mlist)
- try:
- id = int(id)
- info = requestdb.get_record(id)
- except (ValueError, KeyError):
- # BAW: print an error message?
- return
- show_post_requests(mlist, id, info, 1, 1, form)
-
-
-
-def show_detailed_requests(mlist, form):
- requestsdb = config.db.get_list_requests(mlist)
- total = requestsdb.count_of(RequestType.held_message)
- all = requestsdb.of_type(RequestType.held_message)
- for i, request in enumerate(all):
- key, data = requestdb.get_request(request.id)
- show_post_requests(mlist, request.id, data, total, i + 1, form)
-
-
-
-def show_post_requests(mlist, id, info, total, count, form):
- # For backwards compatibility with pre 2.0beta3
- if len(info) == 5:
- ptime, sender, subject, reason, filename = info
- msgdata = {}
- else:
- ptime, sender, subject, reason, filename, msgdata = info
- form.AddItem('<hr>')
- # Header shown on each held posting (including count of total)
- msg = _('Posting Held for Approval')
- if total <> 1:
- msg += _(' (%(count)d of %(total)d)')
- form.AddItem(Center(Header(2, msg)))
- # We need to get the headers and part of the textual body of the message
- # being held. The best way to do this is to use the email Parser to get
- # an actual object, which will be easier to deal with. We probably could
- # just do raw reads on the file.
- try:
- msg = readMessage(os.path.join(config.DATA_DIR, filename))
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- form.AddItem(_('<em>Message with id #%(id)d was lost.'))
- form.AddItem('<p>')
- # BAW: kludge to remove id from requests.db.
- try:
- mlist.HandleRequest(id, config.DISCARD)
- except Errors.LostHeldMessage:
- pass
- return
- except email.Errors.MessageParseError:
- form.AddItem(_('<em>Message with id #%(id)d is corrupted.'))
- # BAW: Should we really delete this, or shuttle it off for site admin
- # to look more closely at?
- form.AddItem('<p>')
- # BAW: kludge to remove id from requests.db.
- try:
- mlist.HandleRequest(id, config.DISCARD)
- except Errors.LostHeldMessage:
- pass
- return
- # Get the header text and the message body excerpt
- lines = []
- chars = 0
- # A negative value means, include the entire message regardless of size
- limit = config.ADMINDB_PAGE_TEXT_LIMIT
- for line in email.Iterators.body_line_iterator(msg):
- lines.append(line)
- chars += len(line)
- if chars > limit > 0:
- break
- # Negative values mean display the entire message, regardless of size
- if limit > 0:
- body = EMPTYSTRING.join(lines)[:config.ADMINDB_PAGE_TEXT_LIMIT]
- else:
- body = EMPTYSTRING.join(lines)
- # Get message charset and try encode in list charset
- mcset = msg.get_param('charset', 'us-ascii').lower()
- lcset = Utils.GetCharSet(mlist.preferred_language)
- if mcset <> lcset:
- try:
- body = unicode(body, mcset).encode(lcset)
- except (LookupError, UnicodeError, ValueError):
- pass
- hdrtxt = NL.join(['%s: %s' % (k, v) for k, v in msg.items()])
- hdrtxt = Utils.websafe(hdrtxt)
- # Okay, we've reconstituted the message just fine. Now for the fun part!
- t = Table(cellspacing=0, cellpadding=0, width='100%')
- t.AddRow([Bold(_('From:')), sender])
- row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
- t.AddCellInfo(row, col-1, align='right')
- t.AddRow([Bold(_('Subject:')),
- Utils.websafe(Utils.oneline(subject, lcset))])
- t.AddCellInfo(row+1, col-1, align='right')
- t.AddRow([Bold(_('Reason:')), _(reason)])
- t.AddCellInfo(row+2, col-1, align='right')
- when = msgdata.get('received_time')
- if when:
- t.AddRow([Bold(_('Received:')), time.ctime(when)])
- t.AddCellInfo(row+2, col-1, align='right')
- # We can't use a RadioButtonArray here because horizontal placement can be
- # confusing to the user and vertical placement takes up too much
- # real-estate. This is a hack!
- buttons = Table(cellspacing="5", cellpadding="0")
- buttons.AddRow(map(lambda x, s='&nbsp;'*5: s+x+s,
- (_('Defer'), _('Approve'), _('Reject'), _('Discard'))))
- buttons.AddRow([Center(RadioButton(id, config.DEFER, 1)),
- Center(RadioButton(id, config.APPROVE, 0)),
- Center(RadioButton(id, config.REJECT, 0)),
- Center(RadioButton(id, config.DISCARD, 0)),
- ])
- t.AddRow([Bold(_('Action:')), buttons])
- t.AddCellInfo(row+3, col-1, align='right')
- t.AddRow(['&nbsp;',
- CheckBox('preserve-%d' % id, 'on', 0).Format() +
- '&nbsp;' + _('Preserve message for site administrator')
- ])
- t.AddRow(['&nbsp;',
- CheckBox('forward-%d' % id, 'on', 0).Format() +
- '&nbsp;' + _('Additionally, forward this message to: ') +
- TextBox('forward-addr-%d' % id, size=47,
- value=mlist.GetOwnerEmail()).Format()
- ])
- notice = msgdata.get('rejection_notice', _('[No explanation given]'))
- t.AddRow([
- Bold(_('If you reject this post,<br>please explain (optional):')),
- TextArea('comment-%d' % id, rows=4, cols=EXCERPT_WIDTH,
- text = Utils.wrap(_(notice), column=80))
- ])
- row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
- t.AddCellInfo(row, col-1, align='right')
- t.AddRow([Bold(_('Message Headers:')),
- TextArea('headers-%d' % id, hdrtxt,
- rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)])
- row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
- t.AddCellInfo(row, col-1, align='right')
- t.AddRow([Bold(_('Message Excerpt:')),
- TextArea('fulltext-%d' % id, Utils.websafe(body),
- rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)])
- t.AddCellInfo(row+1, col-1, align='right')
- form.AddItem(t)
- form.AddItem('<p>')
-
-
-
-def process_form(mlist, doc, cgidata):
- senderactions = {}
- # Sender-centric actions
- for k in cgidata.keys():
- for prefix in ('senderaction-', 'senderpreserve-', 'senderforward-',
- 'senderforwardto-', 'senderfilterp-', 'senderfilter-',
- 'senderclearmodp-', 'senderbanp-'):
- if k.startswith(prefix):
- action = k[:len(prefix)-1]
- sender = unquote_plus(k[len(prefix):])
- value = cgidata.getvalue(k)
- senderactions.setdefault(sender, {})[action] = value
- # discard-all-defers
- try:
- discardalldefersp = cgidata.getvalue('discardalldefersp', 0)
- except ValueError:
- discardalldefersp = 0
- for sender in senderactions.keys():
- actions = senderactions[sender]
- # Handle what to do about all this sender's held messages
- try:
- action = int(actions.get('senderaction', config.DEFER))
- except ValueError:
- action = config.DEFER
- if action == config.DEFER and discardalldefersp:
- action = config.DISCARD
- if action in (config.DEFER, config.APPROVE,
- config.REJECT, config.DISCARD):
- preserve = actions.get('senderpreserve', 0)
- forward = actions.get('senderforward', 0)
- forwardaddr = actions.get('senderforwardto', '')
- comment = _('No reason given')
- bysender = helds_by_sender(mlist)
- for id in bysender.get(sender, []):
- try:
- mlist.HandleRequest(id, action, comment, preserve,
- forward, forwardaddr)
- except (KeyError, Errors.LostHeldMessage):
- # That's okay, it just means someone else has already
- # updated the database while we were staring at the page,
- # so just ignore it
- continue
- # Now see if this sender should be added to one of the nonmember
- # sender filters.
- if actions.get('senderfilterp', 0):
- try:
- which = int(actions.get('senderfilter'))
- except ValueError:
- # Bogus form
- which = 'ignore'
- if which == config.ACCEPT:
- mlist.accept_these_nonmembers.append(sender)
- elif which == config.HOLD:
- mlist.hold_these_nonmembers.append(sender)
- elif which == config.REJECT:
- mlist.reject_these_nonmembers.append(sender)
- elif which == config.DISCARD:
- mlist.discard_these_nonmembers.append(sender)
- # Otherwise, it's a bogus form, so ignore it
- # And now see if we're to clear the member's moderation flag.
- if actions.get('senderclearmodp', 0):
- try:
- mlist.setMemberOption(sender, config.Moderate, 0)
- except Errors.NotAMemberError:
- # This person's not a member any more. Oh well.
- pass
- # And should this address be banned?
- if actions.get('senderbanp', 0):
- if sender not in mlist.ban_list:
- mlist.ban_list.append(sender)
- # Now, do message specific actions
- banaddrs = []
- erroraddrs = []
- for k in cgidata.keys():
- formv = cgidata[k]
- if isinstance(formv, list):
- continue
- try:
- v = int(formv.value)
- request_id = int(k)
- except ValueError:
- continue
- if v not in (config.DEFER, config.APPROVE, config.REJECT,
- config.DISCARD, config.SUBSCRIBE, config.UNSUBSCRIBE,
- config.ACCEPT, config.HOLD):
- continue
- # Get the action comment and reasons if present.
- commentkey = 'comment-%d' % request_id
- preservekey = 'preserve-%d' % request_id
- forwardkey = 'forward-%d' % request_id
- forwardaddrkey = 'forward-addr-%d' % request_id
- bankey = 'ban-%d' % request_id
- # Defaults
- comment = _('[No reason given]')
- preserve = 0
- forward = 0
- forwardaddr = ''
- if cgidata.has_key(commentkey):
- comment = cgidata[commentkey].value
- if cgidata.has_key(preservekey):
- preserve = cgidata[preservekey].value
- if cgidata.has_key(forwardkey):
- forward = cgidata[forwardkey].value
- if cgidata.has_key(forwardaddrkey):
- forwardaddr = cgidata[forwardaddrkey].value
- # Should we ban this address? Do this check before handling the
- # request id because that will evict the record.
- requestsdb = config.db.get_list_requests(mlist)
- if cgidata.getvalue(bankey):
- key, data = requestsdb.get_record(request_id)
- sender = data['sender']
- if sender not in mlist.ban_list:
- mlist.ban_list.append(sender)
- # Handle the request id
- try:
- mlist.HandleRequest(request_id, v, comment,
- preserve, forward, forwardaddr)
- except (KeyError, Errors.LostHeldMessage):
- # That's okay, it just means someone else has already updated the
- # database while we were staring at the page, so just ignore it
- continue
- except Errors.MMAlreadyAMember, v:
- erroraddrs.append(v)
- except Errors.MembershipIsBanned, pattern:
- data = requestsdb.get_record(request_id)
- sender = data['sender']
- banaddrs.append((sender, pattern))
- # save the list and print the results
- doc.AddItem(Header(2, _('Database Updated...')))
- if erroraddrs:
- for addr in erroraddrs:
- doc.AddItem(`addr` + _(' is already a member') + '<br>')
- if banaddrs:
- for addr, patt in banaddrs:
- doc.AddItem(_('%(addr)s is banned (matched: %(patt)s)') + '<br>')
diff --git a/src/web/Cgi/confirm.py b/src/web/Cgi/confirm.py
deleted file mode 100644
index 188e43068..000000000
--- a/src/web/Cgi/confirm.py
+++ /dev/null
@@ -1,834 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Confirm a pending action via URL."""
-
-import cgi
-import time
-import signal
-import logging
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Pending
-from Mailman import i18n
-from Mailman.UserDesc import UserDesc
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-log = logging.getLogger('mailman.error')
-
-
-
-def main():
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- parts = Utils.GetPathPieces()
- if not parts or len(parts) < 1:
- bad_confirmation(doc)
- doc.AddItem(MailmanLogo())
- print doc.Format()
- return
-
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- bad_confirmation(doc, _('No such list <em>%(safelistname)s</em>'))
- doc.AddItem(MailmanLogo())
- print doc.Format()
- log.error('No such list "%s": %s', listname, e)
- return
-
- # Set the language for the list
- i18n.set_language(mlist.preferred_language)
- doc.set_language(mlist.preferred_language)
-
- # Get the form data to see if this is a second-step confirmation
- cgidata = cgi.FieldStorage(keep_blank_values=1)
- cookie = cgidata.getvalue('cookie')
- if cookie == '':
- ask_for_cookie(mlist, doc, _('Confirmation string was empty.'))
- return
-
- if not cookie and len(parts) == 2:
- cookie = parts[1]
-
- if len(parts) > 2:
- bad_confirmation(doc)
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- return
-
- if not cookie:
- ask_for_cookie(mlist, doc)
- return
-
- days = int(config.PENDING_REQUEST_LIFE / config.days(1) + 0.5)
- confirmurl = mlist.GetScriptURL('confirm')
- # Avoid cross-site scripting attacks
- safecookie = Utils.websafe(cookie)
- badconfirmstr = _('''<b>Invalid confirmation string:</b>
- %(safecookie)s.
-
- <p>Note that confirmation strings expire approximately
- %(days)s days after the initial subscription request. If your
- confirmation has expired, please try to re-submit your subscription.
- Otherwise, <a href="%(confirmurl)s">re-enter</a> your confirmation
- string.''')
-
- content = mlist.pend_confirm(cookie, expunge=False)
- if content is None:
- bad_confirmation(doc, badconfirmstr)
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- return
-
- try:
- if content[0] == Pending.SUBSCRIPTION:
- if cgidata.getvalue('cancel'):
- subscription_cancel(mlist, doc, cookie)
- elif cgidata.getvalue('submit'):
- subscription_confirm(mlist, doc, cookie, cgidata)
- else:
- subscription_prompt(mlist, doc, cookie, content[1])
- elif content[0] == Pending.UNSUBSCRIPTION:
- try:
- if cgidata.getvalue('cancel'):
- unsubscription_cancel(mlist, doc, cookie)
- elif cgidata.getvalue('submit'):
- unsubscription_confirm(mlist, doc, cookie)
- else:
- unsubscription_prompt(mlist, doc, cookie, *content[1:])
- except Errors.NotAMemberError:
- doc.addError(_("""The address requesting unsubscription is not
- a member of the mailing list. Perhaps you have already been
- unsubscribed, e.g. by the list administrator?"""))
- # Expunge this record from the pending database.
- expunge(mlist, cookie)
- elif content[0] == Pending.CHANGE_OF_ADDRESS:
- if cgidata.getvalue('cancel'):
- addrchange_cancel(mlist, doc, cookie)
- elif cgidata.getvalue('submit'):
- addrchange_confirm(mlist, doc, cookie)
- else:
- # Watch out for users who have unsubscribed themselves in the
- # meantime!
- try:
- addrchange_prompt(mlist, doc, cookie, *content[1:])
- except Errors.NotAMemberError:
- doc.addError(_("""The address requesting to be changed has
- been subsequently unsubscribed. This request has been
- cancelled."""))
- # Expunge this record from the pending database.
- expunge(mlist, cookie)
- elif content[0] == Pending.HELD_MESSAGE:
- if cgidata.getvalue('cancel'):
- heldmsg_cancel(mlist, doc, cookie)
- elif cgidata.getvalue('submit'):
- heldmsg_confirm(mlist, doc, cookie)
- else:
- heldmsg_prompt(mlist, doc, cookie, *content[1:])
- elif content[0] == Pending.RE_ENABLE:
- if cgidata.getvalue('cancel'):
- reenable_cancel(mlist, doc, cookie)
- elif cgidata.getvalue('submit'):
- reenable_confirm(mlist, doc, cookie)
- else:
- reenable_prompt(mlist, doc, cookie, *content[1:])
- else:
- bad_confirmation(doc, _('System error, bad content: %(content)s'))
- except Errors.MMBadConfirmation:
- bad_confirmation(doc, badconfirmstr)
-
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
-
-
-
-def bad_confirmation(doc, extra=''):
- title = _('Bad confirmation string')
- doc.SetTitle(title)
- doc.AddItem(Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
- doc.AddItem(extra)
-
-
-def expunge(mlist, cookie):
- # Expunge this record from the list's pending database. This requires
- # that the list lock be acquired, however the list doesn't need to be
- # saved because this operation doesn't touch the config.pck file.
- mlist.Lock()
- try:
- mlist.pend_confirm(cookie, expunge=True)
- finally:
- mlist.Unlock()
-
-
-
-def ask_for_cookie(mlist, doc, extra=''):
- title = _('Enter confirmation cookie')
- doc.SetTitle(title)
- form = Form(mlist.GetScriptURL('confirm'))
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- colspan=2, bgcolor=config.WEB_HEADER_COLOR)
-
- if extra:
- table.AddRow([Bold(FontAttr(extra, size='+1'))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
-
- # Add cookie entry box
- table.AddRow([_("""Please enter the confirmation string
- (i.e. <em>cookie</em>) that you received in your email message, in the box
- below. Then hit the <em>Submit</em> button to proceed to the next
- confirmation step.""")])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Label(_('Confirmation string:')),
- TextBox('cookie')])
- table.AddRow([Center(SubmitButton('submit_cookie', _('Submit')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- form.AddItem(table)
- doc.AddItem(form)
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
-
-
-
-def subscription_prompt(mlist, doc, cookie, userdesc):
- email = userdesc.address
- password = userdesc.password
- digest = userdesc.digest
- lang = userdesc.language
- name = Utils.uncanonstr(userdesc.fullname, lang)
- i18n.set_language(lang)
- doc.set_language(lang)
- title = _('Confirm subscription request')
- doc.SetTitle(title)
-
- form = Form(mlist.GetScriptURL('confirm'))
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- colspan=2, bgcolor=config.WEB_HEADER_COLOR)
-
- listname = mlist.real_name
- # This is the normal, no-confirmation required results text.
- #
- # We do things this way so we don't have to reformat this paragraph, which
- # would mess up translations. If you modify this text for other reasons,
- # please refill the paragraph, and clean up the logic.
- result = _("""Your confirmation is required in order to complete the
- subscription request to the mailing list <em>%(listname)s</em>. Your
- subscription settings are shown below; make any necessary changes and hit
- <em>Subscribe</em> to complete the confirmation process. Once you've
- confirmed your subscription request, you will be shown your account
- options page which you can use to further customize your membership
- options.
-
- <p>Note: your password will be emailed to you once your subscription is
- confirmed. You can change it by visiting your personal options page.
-
- <p>Or hit <em>Cancel my subscription request</em> if you no longer want to
- subscribe to this list.""") + '<p><hr>'
- if mlist.subscribe_policy in (2, 3):
- # Confirmation is required
- result = _("""Your confirmation is required in order to continue with
- the subscription request to the mailing list <em>%(listname)s</em>.
- Your subscription settings are shown below; make any necessary changes
- and hit <em>Subscribe to list ...</em> to complete the confirmation
- process. Once you've confirmed your subscription request, the
- moderator must approve or reject your membership request. You will
- receive notice of their decision.
-
- <p>Note: your password will be emailed to you once your subscription
- is confirmed. You can change it by visiting your personal options
- page.
-
- <p>Or, if you've changed your mind and do not want to subscribe to
- this mailing list, you can hit <em>Cancel my subscription
- request</em>.""") + '<p><hr>'
- table.AddRow([result])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
-
- table.AddRow([Label(_('Your email address:')), email])
- table.AddRow([Label(_('Your real name:')),
- TextBox('realname', name)])
-## table.AddRow([Label(_('Password:')),
-## PasswordBox('password', password)])
-## table.AddRow([Label(_('Password (confirm):')),
-## PasswordBox('pwconfirm', password)])
- # Only give them a choice to receive digests if they actually have a
- # choice <wink>.
- if mlist.nondigestable and mlist.digestable:
- table.AddRow([Label(_('Receive digests?')),
- RadioButtonArray('digests', (_('No'), _('Yes')),
- checked=digest, values=(0, 1))])
- langs = mlist.language_codes
- values = [_(config.languages.get_description(code)) for code in langs]
- try:
- selected = langs.index(lang)
- except ValueError:
- selected = lang.index(mlist.preferred_language)
- table.AddRow([Label(_('Preferred language:')),
- SelectOptions('language', langs, values, selected)])
- table.AddRow([Hidden('cookie', cookie)])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([
- Label(SubmitButton('cancel', _('Cancel my subscription request'))),
- SubmitButton('submit', _('Subscribe to list %(listname)s'))
- ])
- form.AddItem(table)
- doc.AddItem(form)
-
-
-
-def subscription_cancel(mlist, doc, cookie):
- mlist.Lock()
- try:
- # Discard this cookie
- userdesc = mlist.pend_confirm(cookie)[1]
- finally:
- mlist.Unlock()
- lang = userdesc.language
- i18n.set_language(lang)
- doc.set_language(lang)
- doc.AddItem(_('You have canceled your subscription request.'))
-
-
-
-def subscription_confirm(mlist, doc, cookie, cgidata):
- # See the comment in admin.py about the need for the signal
- # handler.
- def sigterm_handler(signum, frame, mlist=mlist):
- mlist.Unlock()
- sys.exit(0)
-
- listname = mlist.real_name
- mlist.Lock()
- try:
- try:
- # Some pending values may be overridden in the form. email of
- # course is hardcoded. ;)
- lang = cgidata.getvalue('language')
- if lang not in config.languages.enabled_codes:
- lang = mlist.preferred_language
- i18n.set_language(lang)
- doc.set_language(lang)
- if cgidata.has_key('digests'):
- try:
- digest = int(cgidata.getvalue('digests'))
- except ValueError:
- digest = None
- else:
- digest = None
- userdesc = mlist.pend_confirm(cookie, expunge=False)[1]
- fullname = cgidata.getvalue('realname', None)
- if fullname is not None:
- fullname = Utils.canonstr(fullname, lang)
- overrides = UserDesc(fullname=fullname, digest=digest, lang=lang)
- userdesc += overrides
- op, addr, pw, digest, lang = mlist.ProcessConfirmation(
- cookie, userdesc)
- except Errors.MMNeedApproval:
- title = _('Awaiting moderator approval')
- doc.SetTitle(title)
- doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
- doc.AddItem(_("""\
- You have successfully confirmed your subscription request to the
- mailing list %(listname)s, however final approval is required from
- the list moderator before you will be subscribed. Your request
- has been forwarded to the list moderator, and you will be notified
- of the moderator's decision."""))
- except Errors.NotAMemberError:
- bad_confirmation(doc, _('''Invalid confirmation string. It is
- possible that you are attempting to confirm a request for an
- address that has already been unsubscribed.'''))
- except Errors.MMAlreadyAMember:
- doc.addError(_("You are already a member of this mailing list!"))
- except Errors.MembershipIsBanned:
- owneraddr = mlist.GetOwnerEmail()
- doc.addError(_("""You are currently banned from subscribing to
- this list. If you think this restriction is erroneous, please
- contact the list owners at %(owneraddr)s."""))
- except Errors.HostileSubscriptionError:
- doc.addError(_("""\
- You were not invited to this mailing list. The invitation has
- been discarded, and both list administrators have been
- alerted."""))
- else:
- # Use the user's preferred language
- i18n.set_language(lang)
- doc.set_language(lang)
- # The response
- listname = mlist.real_name
- title = _('Subscription request confirmed')
- optionsurl = mlist.GetOptionsURL(addr)
- doc.SetTitle(title)
- doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
- doc.AddItem(_('''\
- You have successfully confirmed your subscription request for
- "%(addr)s" to the %(listname)s mailing list. A separate
- confirmation message will be sent to your email address, along
- with your password, and other useful information and links.
-
- <p>You can now
- <a href="%(optionsurl)s">proceed to your membership login
- page</a>.'''))
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def unsubscription_cancel(mlist, doc, cookie):
- # Expunge this record from the pending database
- expunge(mlist, cookie)
- doc.AddItem(_('You have canceled your unsubscription request.'))
-
-
-
-def unsubscription_confirm(mlist, doc, cookie):
- # See the comment in admin.py about the need for the signal
- # handler.
- def sigterm_handler(signum, frame, mlist=mlist):
- mlist.Unlock()
- sys.exit(0)
-
- mlist.Lock()
- try:
- try:
- # Do this in two steps so we can get the preferred language for
- # the user who is unsubscribing.
- op, addr = mlist.pend_confirm(cookie, expunge=False)
- lang = mlist.getMemberLanguage(addr)
- i18n.set_language(lang)
- doc.set_language(lang)
- op, addr = mlist.ProcessConfirmation(cookie)
- except Errors.NotAMemberError:
- bad_confirmation(doc, _('''Invalid confirmation string. It is
- possible that you are attempting to confirm a request for an
- address that has already been unsubscribed.'''))
- else:
- # The response
- listname = mlist.real_name
- title = _('Unsubscription request confirmed')
- listinfourl = mlist.GetScriptURL('listinfo')
- doc.SetTitle(title)
- doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
- doc.AddItem(_("""\
- You have successfully unsubscribed from the %(listname)s mailing
- list. You can now <a href="%(listinfourl)s">visit the list's main
- information page</a>."""))
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def unsubscription_prompt(mlist, doc, cookie, addr):
- title = _('Confirm unsubscription request')
- doc.SetTitle(title)
- lang = mlist.getMemberLanguage(addr)
- i18n.set_language(lang)
- doc.set_language(lang)
-
- form = Form(mlist.GetScriptURL('confirm'))
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- colspan=2, bgcolor=config.WEB_HEADER_COLOR)
-
- listname = mlist.real_name
- fullname = mlist.getMemberName(addr)
- if fullname is None:
- fullname = _('<em>Not available</em>')
- else:
- fullname = Utils.uncanonstr(fullname, lang)
- table.AddRow([_("""Your confirmation is required in order to complete the
- unsubscription request from the mailing list <em>%(listname)s</em>. You
- are currently subscribed with
-
- <ul><li><b>Real name:</b> %(fullname)s
- <li><b>Email address:</b> %(addr)s
- </ul>
-
- Hit the <em>Unsubscribe</em> button below to complete the confirmation
- process.
-
- <p>Or hit <em>Cancel and discard</em> to cancel this unsubscription
- request.""") + '<p><hr>'])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Hidden('cookie', cookie)])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([SubmitButton('submit', _('Unsubscribe')),
- SubmitButton('cancel', _('Cancel and discard'))])
-
- form.AddItem(table)
- doc.AddItem(form)
-
-
-
-def addrchange_cancel(mlist, doc, cookie):
- # Expunge this record from the pending database
- expunge(mlist, cookie)
- doc.AddItem(_('You have canceled your change of address request.'))
-
-
-
-def addrchange_confirm(mlist, doc, cookie):
- # See the comment in admin.py about the need for the signal
- # handler.
- def sigterm_handler(signum, frame, mlist=mlist):
- mlist.Unlock()
- sys.exit(0)
-
- mlist.Lock()
- try:
- try:
- # Do this in two steps so we can get the preferred language for
- # the user who is unsubscribing.
- op, oldaddr, newaddr, globally = mlist.pend_confirm(
- cookie, expunge=False)
- lang = mlist.getMemberLanguage(oldaddr)
- i18n.set_language(lang)
- doc.set_language(lang)
- op, oldaddr, newaddr = mlist.ProcessConfirmation(cookie)
- except Errors.NotAMemberError:
- bad_confirmation(doc, _('''Invalid confirmation string. It is
- possible that you are attempting to confirm a request for an
- address that has already been unsubscribed.'''))
- except Errors.MembershipIsBanned:
- owneraddr = mlist.GetOwnerEmail()
- realname = mlist.real_name
- doc.addError(_("""%(newaddr)s is banned from subscribing to the
- %(realname)s list. If you think this restriction is erroneous,
- please contact the list owners at %(owneraddr)s."""))
- else:
- # The response
- listname = mlist.real_name
- title = _('Change of address request confirmed')
- optionsurl = mlist.GetOptionsURL(newaddr)
- doc.SetTitle(title)
- doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
- doc.AddItem(_("""\
- You have successfully changed your address on the %(listname)s
- mailing list from <b>%(oldaddr)s</b> to <b>%(newaddr)s</b>. You
- can now <a href="%(optionsurl)s">proceed to your membership
- login page</a>."""))
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def addrchange_prompt(mlist, doc, cookie, oldaddr, newaddr, globally):
- title = _('Confirm change of address request')
- doc.SetTitle(title)
- lang = mlist.getMemberLanguage(oldaddr)
- i18n.set_language(lang)
- doc.set_language(lang)
-
- form = Form(mlist.GetScriptURL('confirm'))
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- colspan=2, bgcolor=config.WEB_HEADER_COLOR)
-
- listname = mlist.real_name
- fullname = mlist.getMemberName(oldaddr)
- if fullname is None:
- fullname = _('<em>Not available</em>')
- else:
- fullname = Utils.uncanonstr(fullname, lang)
- if globally:
- globallys = _('globally')
- else:
- globallys = ''
- table.AddRow([_("""Your confirmation is required in order to complete the
- change of address request for the mailing list <em>%(listname)s</em>. You
- are currently subscribed with
-
- <ul><li><b>Real name:</b> %(fullname)s
- <li><b>Old email address:</b> %(oldaddr)s
- </ul>
-
- and you have requested to %(globallys)s change your email address to
-
- <ul><li><b>New email address:</b> %(newaddr)s
- </ul>
-
- Hit the <em>Change address</em> button below to complete the confirmation
- process.
-
- <p>Or hit <em>Cancel and discard</em> to cancel this change of address
- request.""") + '<p><hr>'])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Hidden('cookie', cookie)])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([SubmitButton('submit', _('Change address')),
- SubmitButton('cancel', _('Cancel and discard'))])
-
- form.AddItem(table)
- doc.AddItem(form)
-
-
-
-def heldmsg_cancel(mlist, doc, cookie):
- title = _('Continue awaiting approval')
- doc.SetTitle(title)
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
- # Expunge this record from the pending database.
- expunge(mlist, cookie)
- table.AddRow([_('''Okay, the list moderator will still have the
- opportunity to approve or reject this message.''')])
- doc.AddItem(table)
-
-
-
-def heldmsg_confirm(mlist, doc, cookie):
- # See the comment in admin.py about the need for the signal
- # handler.
- def sigterm_handler(signum, frame, mlist=mlist):
- mlist.Unlock()
- sys.exit(0)
-
- mlist.Lock()
- try:
- try:
- # Do this in two steps so we can get the preferred language for
- # the user who posted the message.
- op, id = mlist.pend_confirm(cookie)
- requestsdb = config.db.get_list_requests(mlist)
- key, data = requestsdb.get_record(id)
- sender = data['sender']
- msgsubject = data['msgsubject']
- subject = Utils.websafe(msgsubject)
- lang = mlist.getMemberLanguage(sender)
- i18n.set_language(lang)
- doc.set_language(lang)
- # Discard the message
- mlist.HandleRequest(id, config.DISCARD,
- _('Sender discarded message via web.'))
- except Errors.LostHeldMessage:
- bad_confirmation(doc, _('''The held message with the Subject:
- header <em>%(subject)s</em> could not be found. The most likely
- reason for this is that the list moderator has already approved or
- rejected the message. You were not able to cancel it in
- time.'''))
- else:
- # The response
- listname = mlist.real_name
- title = _('Posted message canceled')
- doc.SetTitle(title)
- doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
- doc.AddItem(_('''\
- You have successfully canceled the posting of your message with
- the Subject: header <em>%(subject)s</em> to the mailing list
- %(listname)s.'''))
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def heldmsg_prompt(mlist, doc, cookie, id):
- title = _('Cancel held message posting')
- doc.SetTitle(title)
- form = Form(mlist.GetScriptURL('confirm'))
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- colspan=2, bgcolor=config.WEB_HEADER_COLOR)
- # Blarg. The list must be locked in order to interact with the ListAdmin
- # database, even for read-only. See the comment in admin.py about the
- # need for the signal handler.
- def sigterm_handler(signum, frame, mlist=mlist):
- mlist.Unlock()
- sys.exit(0)
- # Get the record, but watch for KeyErrors which mean the admin has already
- # disposed of this message.
- mlist.Lock()
- requestdb = config.db.get_list_requests(mlist)
- try:
- try:
- key, data = requestdb.get_record(id)
- except KeyError:
- data = None
- finally:
- mlist.Unlock()
-
- if data is None:
- bad_confirmation(doc, _("""The held message you were referred to has
- already been handled by the list administrator."""))
- return
-
- # Unpack the data and present the confirmation message
- ign, sender, msgsubject, givenreason, ign, ign = data
- # Now set the language to the sender's preferred.
- lang = mlist.getMemberLanguage(sender)
- i18n.set_language(lang)
- doc.set_language(lang)
-
- subject = Utils.websafe(msgsubject)
- reason = Utils.websafe(_(givenreason))
- listname = mlist.real_name
- table.AddRow([_('''Your confirmation is required in order to cancel the
- posting of your message to the mailing list <em>%(listname)s</em>:
-
- <ul><li><b>Sender:</b> %(sender)s
- <li><b>Subject:</b> %(subject)s
- <li><b>Reason:</b> %(reason)s
- </ul>
-
- Hit the <em>Cancel posting</em> button to discard the posting.
-
- <p>Or hit the <em>Continue awaiting approval</em> button to continue to
- allow the list moderator to approve or reject the message.''')
- + '<p><hr>'])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Hidden('cookie', cookie)])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([SubmitButton('submit', _('Cancel posting')),
- SubmitButton('cancel', _('Continue awaiting approval'))])
-
- form.AddItem(table)
- doc.AddItem(form)
-
-
-
-def reenable_cancel(mlist, doc, cookie):
- # Don't actually discard this cookie, since the user may decide to
- # re-enable their membership at a future time, and we may be sending out
- # future notifications with this cookie value.
- doc.AddItem(_("""You have canceled the re-enabling of your membership. If
- we continue to receive bounces from your address, it could be deleted from
- this mailing list."""))
-
-
-
-def reenable_confirm(mlist, doc, cookie):
- # See the comment in admin.py about the need for the signal
- # handler.
- def sigterm_handler(signum, frame, mlist=mlist):
- mlist.Unlock()
- sys.exit(0)
-
- mlist.Lock()
- try:
- try:
- # Do this in two steps so we can get the preferred language for
- # the user who is unsubscribing.
- op, listname, addr = mlist.pend_confirm(cookie, expunge=False)
- lang = mlist.getMemberLanguage(addr)
- i18n.set_language(lang)
- doc.set_language(lang)
- op, addr = mlist.ProcessConfirmation(cookie)
- except Errors.NotAMemberError:
- bad_confirmation(doc, _('''Invalid confirmation string. It is
- possible that you are attempting to confirm a request for an
- address that has already been unsubscribed.'''))
- else:
- # The response
- listname = mlist.real_name
- title = _('Membership re-enabled.')
- optionsurl = mlist.GetOptionsURL(addr)
- doc.SetTitle(title)
- doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
- doc.AddItem(_("""\
- You have successfully re-enabled your membership in the
- %(listname)s mailing list. You can now <a
- href="%(optionsurl)s">visit your member options page</a>.
- """))
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def reenable_prompt(mlist, doc, cookie, list, member):
- title = _('Re-enable mailing list membership')
- doc.SetTitle(title)
- form = Form(mlist.GetScriptURL('confirm'))
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- colspan=2, bgcolor=config.WEB_HEADER_COLOR)
-
- lang = mlist.getMemberLanguage(member)
- i18n.set_language(lang)
- doc.set_language(lang)
-
- realname = mlist.real_name
- info = mlist.getBounceInfo(member)
- if not info:
- listinfourl = mlist.GetScriptURL('listinfo')
- # They've already be unsubscribed
- table.AddRow([_("""We're sorry, but you have already been unsubscribed
- from this mailing list. To re-subscribe, please visit the
- <a href="%(listinfourl)s">list information page</a>.""")])
- return
-
- date = time.strftime('%A, %B %d, %Y',
- time.localtime(time.mktime(info.date + (0,)*6)))
- daysleft = int(info.noticesleft *
- mlist.bounce_you_are_disabled_warnings_interval /
- config.days(1))
- # BAW: for consistency this should be changed to 'fullname' or the above
- # 'fullname's should be changed to 'username'. Don't want to muck with
- # the i18n catalogs though.
- username = mlist.getMemberName(member)
- if username is None:
- username = _('<em>not available</em>')
- else:
- username = Utils.uncanonstr(username, lang)
-
- table.AddRow([_("""Your membership in the %(realname)s mailing list is
- currently disabled due to excessive bounces. Your confirmation is
- required in order to re-enable delivery to your address. We have the
- following information on file:
-
- <ul><li><b>Member address:</b> %(member)s
- <li><b>Member name:</b> %(username)s
- <li><b>Last bounce received on:</b> %(date)s
- <li><b>Approximate number of days before you are permanently removed
- from this list:</b> %(daysleft)s
- </ul>
-
- Hit the <em>Re-enable membership</em> button to resume receiving postings
- from the mailing list. Or hit the <em>Cancel</em> button to defer
- re-enabling your membership.
- """)])
-
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Hidden('cookie', cookie)])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([SubmitButton('submit', _('Re-enable membership')),
- SubmitButton('cancel', _('Cancel'))])
-
- form.AddItem(table)
- doc.AddItem(form)
diff --git a/src/web/Cgi/create.py b/src/web/Cgi/create.py
deleted file mode 100644
index 4bb650957..000000000
--- a/src/web/Cgi/create.py
+++ /dev/null
@@ -1,400 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Create mailing lists through the web."""
-
-import cgi
-import sha
-import sys
-import logging
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Message
-from Mailman import i18n
-from Mailman import passwords
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-__i18n_templates__ = True
-
-log = logging.getLogger('mailman.error')
-
-
-
-def main():
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- cgidata = cgi.FieldStorage()
- parts = Utils.GetPathPieces()
- if parts:
- # Bad URL specification
- title = _('Bad URL specification')
- doc.SetTitle(title)
- doc.AddItem(
- Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
- log.error('Bad URL specification: %s', parts)
- elif cgidata.has_key('doit'):
- # We must be processing the list creation request
- process_request(doc, cgidata)
- elif cgidata.has_key('clear'):
- request_creation(doc)
- else:
- # Put up the list creation request form
- request_creation(doc)
- doc.AddItem('<hr>')
- # Always add the footer and print the document
- doc.AddItem(_('Return to the ') +
- Link(Utils.ScriptURL('listinfo'),
- _('general list overview')).Format())
- doc.AddItem(_('<br>Return to the ') +
- Link(Utils.ScriptURL('admin'),
- _('administrative list overview')).Format())
- doc.AddItem(MailmanLogo())
- print doc.Format()
-
-
-
-def process_request(doc, cgidata):
- # Lowercase the listname since this is treated as the 'internal' name.
- listname = cgidata.getvalue('listname', '').strip().lower()
- owner = cgidata.getvalue('owner', '').strip()
- try:
- autogen = bool(int(cgidata.getvalue('autogen', '0')))
- except ValueError:
- autogen = False
- try:
- notify = bool(int(cgidata.getvalue('notify', '0')))
- except ValueError:
- notify = False
- try:
- moderate = bool(int(cgidata.getvalue('moderate',
- config.DEFAULT_DEFAULT_MEMBER_MODERATION)))
- except ValueError:
- moderate = config.DEFAULT_DEFAULT_MEMBER_MODERATION
-
- password = cgidata.getvalue('password', '').strip()
- confirm = cgidata.getvalue('confirm', '').strip()
- auth = cgidata.getvalue('auth', '').strip()
- langs = cgidata.getvalue('langs', [config.DEFAULT_SERVER_LANGUAGE])
-
- if not isinstance(langs, list):
- langs = [langs]
- # Sanity checks
- safelistname = Utils.websafe(listname)
- if '@' in listname:
- request_creation(doc, cgidata,
- _('List name must not include "@": $safelistname'))
- return
- if not listname:
- request_creation(doc, cgidata,
- _('You forgot to enter the list name'))
- return
- if not owner:
- request_creation(doc, cgidata,
- _('You forgot to specify the list owner'))
- return
- if autogen:
- if password or confirm:
- request_creation(
- doc, cgidata,
- _("""Leave the initial password (and confirmation) fields
- blank if you want Mailman to autogenerate the list
- passwords."""))
- return
- password = confirm = Utils.MakeRandomPassword(
- config.ADMIN_PASSWORD_LENGTH)
- else:
- if password <> confirm:
- request_creation(doc, cgidata,
- _('Initial list passwords do not match'))
- return
- if not password:
- request_creation(
- doc, cgidata,
- # The little <!-- ignore --> tag is used so that this string
- # differs from the one in bin/newlist. The former is destined
- # for the web while the latter is destined for email, so they
- # must be different entries in the message catalog.
- _('The list password cannot be empty<!-- ignore -->'))
- return
- # The authorization password must be non-empty, and it must match either
- # the list creation password or the site admin password
- ok = False
- if auth:
- ok = Utils.check_global_password(auth, False)
- if not ok:
- ok = Utils.check_global_password(auth)
- if not ok:
- request_creation(
- doc, cgidata,
- _('You are not authorized to create new mailing lists'))
- return
- # Make sure the url host name matches one of our virtual domains. Then
- # calculate the list's posting address.
- url_host = Utils.get_request_domain()
- # Find the IDomain matching this url_host if there is one.
- for email_host, domain in config.domains:
- if domain.url_host == url_host:
- email_host = domain.email_host
- break
- else:
- safehostname = Utils.websafe(url_host)
- request_creation(doc, cgidata,
- _('Unknown virtual host: $safehostname'))
- return
- fqdn_listname = '%s@%s' % (listname, email_host)
- # We've got all the data we need, so go ahead and try to create the list
- mlist = MailList.MailList()
- try:
- scheme = passwords.lookup_scheme(config.PASSWORD_SCHEME.lower())
- pw = passwords.make_secret(password, scheme)
- try:
- mlist.Create(fqdn_listname, owner, pw, langs)
- except Errors.EmailAddressError, s:
- request_creation(doc, cgidata, _('Bad owner email address: $s'))
- return
- except Errors.MMListAlreadyExistsError:
- safelistname = Utils.websafe(listname)
- request_creation(doc, cgidata,
- _('List already exists: $safelistname'))
- return
- except Errors.InvalidEmailAddress, s:
- request_creation(doc, cgidata, _('Illegal list name: $s'))
- return
- except Errors.MMListError:
- request_creation(
- doc, cgidata,
- _("""Some unknown error occurred while creating the list.
- Please contact the site administrator for assistance."""))
- return
- # Initialize the host_name and web_page_url attributes, based on
- # virtual hosting settings and the request environment variables.
- mlist.default_member_moderation = moderate
- mlist.Save()
- finally:
- mlist.Unlock()
- # Now do the MTA-specific list creation tasks
- if config.MTA:
- modname = 'Mailman.MTA.' + config.MTA
- __import__(modname)
- sys.modules[modname].create(mlist, cgi=True)
- # And send the notice to the list owner.
- if notify:
- text = Utils.maketext(
- 'newlist.txt',
- {'listname' : listname,
- 'password' : password,
- 'admin_url' : mlist.GetScriptURL('admin', absolute=True),
- 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=True),
- 'requestaddr' : mlist.GetRequestEmail(),
- 'siteowner' : mlist.no_reply_address,
- }, mlist=mlist)
- msg = Message.UserNotification(
- owner, mlist.no_reply_address,
- _('Your new mailing list: $listname'),
- text, mlist.preferred_language)
- msg.send(mlist)
- # Success!
- listinfo_url = mlist.GetScriptURL('listinfo')
- admin_url = mlist.GetScriptURL('admin')
- create_url = Utils.ScriptURL('create')
-
- title = _('Mailing list creation results')
- doc.SetTitle(title)
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
- table.AddRow([_("""You have successfully created the mailing list
- <b>$listname</b> and notification has been sent to the list owner
- <b>$owner</b>. You can now:""")])
- ullist = UnorderedList()
- ullist.AddItem(Link(listinfo_url, _("Visit the list's info page")))
- ullist.AddItem(Link(admin_url, _("Visit the list's admin page")))
- ullist.AddItem(Link(create_url, _('Create another list')))
- table.AddRow([ullist])
- doc.AddItem(table)
-
-
-
-# Because the cgi module blows
-class Dummy:
- def getvalue(self, name, default):
- return default
-dummy = Dummy()
-
-
-
-def request_creation(doc, cgidata=dummy, errmsg=None):
- # What virtual domain are we using?
- hostname = Utils.get_request_domain()
- # Set up the document
- title = _('Create a $hostname Mailing List')
- doc.SetTitle(title)
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
- # Add any error message
- if errmsg:
- table.AddRow([Header(3, Bold(
- FontAttr(_('Error: '), color='#ff0000', size='+2').Format() +
- Italic(errmsg).Format()))])
- table.AddRow([_("""You can create a new mailing list by entering the
- relevant information into the form below. The name of the mailing list
- will be used as the primary address for posting messages to the list, so
- it should be lowercased. You will not be able to change this once the
- list is created.
-
- <p>You also need to enter the email address of the initial list owner.
- Once the list is created, the list owner will be given notification, along
- with the initial list password. The list owner will then be able to
- modify the password and add or remove additional list owners.
-
- <p>If you want Mailman to automatically generate the initial list admin
- password, click on `Yes' in the autogenerate field below, and leave the
- initial list password fields empty.
-
- <p>You must have the proper authorization to create new mailing lists.
- Each site should have a <em>list creator's</em> password, which you can
- enter in the field at the bottom. Note that the site administrator's
- password can also be used for authentication.
- """)])
- # Build the form for the necessary input
- GREY = config.WEB_ADMINITEM_COLOR
- form = Form(Utils.ScriptURL('create'))
- ftable = Table(border=0, cols='2', width='100%',
- cellspacing=3, cellpadding=4)
-
- ftable.AddRow([Center(Italic(_('List Identity')))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
-
- safelistname = Utils.websafe(cgidata.getvalue('listname', ''))
- ftable.AddRow([Label(_('Name of list:')),
- TextBox('listname', safelistname)])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- safeowner = Utils.websafe(cgidata.getvalue('owner', ''))
- ftable.AddRow([Label(_('Initial list owner address:')),
- TextBox('owner', safeowner)])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- try:
- autogen = bool(int(cgidata.getvalue('autogen', '0')))
- except ValueError:
- autogen = False
- ftable.AddRow([Label(_('Auto-generate initial list password?')),
- RadioButtonArray('autogen', (_('No'), _('Yes')),
- checked=autogen,
- values=(0, 1))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- safepasswd = Utils.websafe(cgidata.getvalue('password', ''))
- ftable.AddRow([Label(_('Initial list password:')),
- PasswordBox('password', safepasswd)])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- safeconfirm = Utils.websafe(cgidata.getvalue('confirm', ''))
- ftable.AddRow([Label(_('Confirm initial password:')),
- PasswordBox('confirm', safeconfirm)])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- try:
- notify = bool(int(cgidata.getvalue('notify', '1')))
- except ValueError:
- notify = True
- try:
- moderate = bool(int(cgidata.getvalue('moderate',
- config.DEFAULT_DEFAULT_MEMBER_MODERATION)))
- except ValueError:
- moderate = config.DEFAULT_DEFAULT_MEMBER_MODERATION
-
- ftable.AddRow([Center(Italic(_('List Characteristics')))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
-
- ftable.AddRow([
- Label(_("""Should new members be quarantined before they
- are allowed to post unmoderated to this list? Answer <em>Yes</em> to hold
- new member postings for moderator approval by default.""")),
- RadioButtonArray('moderate', (_('No'), _('Yes')),
- checked=moderate,
- values=(0,1))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
- # Create the table of initially supported languages, sorted on the long
- # name of the language.
- revmap = {}
- for key, (name, charset) in config.LC_DESCRIPTIONS.items():
- revmap[_(name)] = key
- langnames = revmap.keys()
- langnames.sort()
- langs = []
- for name in langnames:
- langs.append(revmap[name])
- try:
- langi = langs.index(config.DEFAULT_SERVER_LANGUAGE)
- except ValueError:
- # Someone must have deleted the servers's preferred language. Could
- # be other trouble lurking!
- langi = 0
- # BAW: we should preserve the list of checked languages across form
- # invocations.
- checked = [0] * len(langs)
- checked[langi] = 1
- deflang = _(
- config.languages.get_description(config.DEFAULT_SERVER_LANGUAGE))
- ftable.AddRow([Label(_(
- """Initial list of supported languages. <p>Note that if you do not
- select at least one initial language, the list will use the server
- default language of $deflang""")),
- CheckBoxArray('langs',
- [_(config.languges.get_description(code))
- for code in langs],
- checked=checked,
- values=langs)])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- ftable.AddRow([Label(_('Send "list created" email to list owner?')),
- RadioButtonArray('notify', (_('No'), _('Yes')),
- checked=notify,
- values=(0, 1))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- ftable.AddRow(['<hr>'])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
- ftable.AddRow([Label(_("List creator's (authentication) password:")),
- PasswordBox('auth')])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- ftable.AddRow([Center(SubmitButton('doit', _('Create List'))),
- Center(SubmitButton('clear', _('Clear Form')))])
- form.AddItem(ftable)
- table.AddRow([form])
- doc.AddItem(table)
diff --git a/src/web/Cgi/edithtml.py b/src/web/Cgi/edithtml.py
deleted file mode 100644
index dfc871ec1..000000000
--- a/src/web/Cgi/edithtml.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# 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/>.
-
-"""Script which implements admin editing of the list's html templates."""
-
-import os
-import re
-import cgi
-import errno
-import logging
-
-from Mailman import Defaults
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.Cgi import Auth
-from Mailman.HTMLFormatter import HTMLFormatter
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-_ = i18n._
-
-log = logging.getLogger('mailman.error')
-
-
-
-def main():
- # Trick out pygettext since we want to mark template_data as translatable,
- # but we don't want to actually translate it here.
- def _(s):
- return s
-
- template_data = (
- ('listinfo.html', _('General list information page')),
- ('subscribe.html', _('Subscribe results page')),
- ('options.html', _('User specific options page')),
- ('subscribeack.txt', _('Welcome email text file')),
- )
-
- _ = i18n._
- doc = Document()
-
- # Set up the system default language
- i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- parts = Utils.GetPathPieces()
- if not parts:
- doc.AddItem(Header(2, _("List name is required.")))
- print doc.Format()
- return
-
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- doc.AddItem(Header(2, _('No such list <em>%(safelistname)s</em>')))
- print doc.Format()
- log.error('No such list "%s": %s', listname, e)
- return
-
- # Now that we have a valid list, set the language to its default
- i18n.set_language(mlist.preferred_language)
- doc.set_language(mlist.preferred_language)
-
- # Must be authenticated to get any farther
- cgidata = cgi.FieldStorage()
-
- # Editing the html for a list is limited to the list admin and site admin.
- if not mlist.WebAuthenticate((Defaults.AuthListAdmin,
- Defaults.AuthSiteAdmin),
- cgidata.getvalue('adminpw', '')):
- if cgidata.has_key('admlogin'):
- # This is a re-authorization attempt
- msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
- else:
- msg = ''
- Auth.loginpage(mlist, 'admin', msg=msg)
- return
-
- realname = mlist.real_name
- if len(parts) > 1:
- template_name = parts[1]
- for (template, info) in template_data:
- if template == template_name:
- template_info = _(info)
- doc.SetTitle(_(
- '%(realname)s -- Edit html for %(template_info)s'))
- break
- else:
- # Avoid cross-site scripting attacks
- safetemplatename = Utils.websafe(template_name)
- doc.SetTitle(_('Edit HTML : Error'))
- doc.AddItem(Header(2, _("%(safetemplatename)s: Invalid template")))
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- return
- else:
- doc.SetTitle(_('%(realname)s -- HTML Page Editing'))
- doc.AddItem(Header(1, _('%(realname)s -- HTML Page Editing')))
- doc.AddItem(Header(2, _('Select page to edit:')))
- template_list = UnorderedList()
- for (template, info) in template_data:
- l = Link(mlist.GetScriptURL('edithtml') + '/' + template, _(info))
- template_list.AddItem(l)
- doc.AddItem(FontSize("+2", template_list))
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- return
-
- try:
- if cgidata.keys():
- ChangeHTML(mlist, cgidata, template_name, doc)
- FormatHTML(mlist, doc, template_name, template_info)
- finally:
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
-
-
-
-def FormatHTML(mlist, doc, template_name, template_info):
- doc.AddItem(Header(1,'%s:' % mlist.real_name))
- doc.AddItem(Header(1, template_info))
- doc.AddItem('<hr>')
-
- link = Link(mlist.GetScriptURL('admin'),
- _('View or edit the list configuration information.'))
-
- doc.AddItem(FontSize("+1", link))
- doc.AddItem('<p>')
- doc.AddItem('<hr>')
- form = Form(mlist.GetScriptURL('edithtml') + '/' + template_name)
- text = Utils.websafe(Utils.maketext(template_name, raw=1, mlist=mlist))
- form.AddItem(TextArea('html_code', text, rows=40, cols=75))
- form.AddItem('<p>' + _('When you are done making changes...'))
- form.AddItem(SubmitButton('submit', _('Submit Changes')))
- doc.AddItem(form)
-
-
-
-def ChangeHTML(mlist, cgi_info, template_name, doc):
- if not cgi_info.has_key('html_code'):
- doc.AddItem(Header(3,_("Can't have empty html page.")))
- doc.AddItem(Header(3,_("HTML Unchanged.")))
- doc.AddItem('<hr>')
- return
- code = cgi_info['html_code'].value
- code = re.sub(r'<([/]?script.*?)>', r'&lt;\1&gt;', code)
- langdir = os.path.join(mlist.fullpath(), mlist.preferred_language)
- # Make sure the directory exists
- Utils.makedirs(langdir)
- fp = open(os.path.join(langdir, template_name), 'w')
- try:
- fp.write(code)
- finally:
- fp.close()
- doc.AddItem(Header(3, _('HTML successfully updated.')))
- doc.AddItem('<hr>')
diff --git a/src/web/Cgi/listinfo.py b/src/web/Cgi/listinfo.py
deleted file mode 100644
index 149be715f..000000000
--- a/src/web/Cgi/listinfo.py
+++ /dev/null
@@ -1,207 +0,0 @@
-# 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/>.
-
-"""Produce listinfo page, primary web entry-point to mailing lists."""
-
-# No lock needed in this script, because we don't change data.
-
-import os
-import cgi
-import logging
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-log = logging.getLogger('mailman.error')
-
-
-
-def main():
- parts = Utils.GetPathPieces()
- if not parts:
- listinfo_overview()
- return
-
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- listinfo_overview(_('No such list <em>%(safelistname)s</em>'))
- log.error('No such list "%s": %s', listname, e)
- return
-
- # See if the user want to see this page in other language
- cgidata = cgi.FieldStorage()
- language = cgidata.getvalue('language')
- if language not in config.languages.enabled_codes:
- language = mlist.preferred_language
- i18n.set_language(language)
- list_listinfo(mlist, language)
-
-
-
-def listinfo_overview(msg=''):
- # Present the general listinfo overview
- hostname = Utils.get_request_domain()
- # Set up the document and assign it the correct language. The only one we
- # know about at the moment is the server's default.
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- legend = _("%(hostname)s Mailing Lists")
- doc.SetTitle(legend)
-
- table = Table(border=0, width="100%")
- table.AddRow([Center(Header(2, legend))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_HEADER_COLOR)
-
- # Skip any mailing lists that isn't advertised.
- advertised = []
- for name in sorted(config.list_manager.names):
- mlist = MailList.MailList(name, lock=False)
- if mlist.advertised:
- if hostname not in mlist.web_page_url:
- # This list is situated in a different virtual domain
- continue
- else:
- advertised.append((mlist.GetScriptURL('listinfo'),
- mlist.real_name,
- mlist.description))
- if msg:
- greeting = FontAttr(msg, color="ff5060", size="+1")
- else:
- greeting = FontAttr(_('Welcome!'), size='+2')
-
- welcome = [greeting]
- mailmanlink = Link(config.MAILMAN_URL, _('Mailman')).Format()
- if not advertised:
- welcome.extend(
- _('''<p>There currently are no publicly-advertised
- %(mailmanlink)s mailing lists on %(hostname)s.'''))
- else:
- welcome.append(
- _('''<p>Below is a listing of all the public mailing lists on
- %(hostname)s. Click on a list name to get more information about
- the list, or to subscribe, unsubscribe, and change the preferences
- on your subscription.'''))
-
- # set up some local variables
- adj = msg and _('right') or ''
- siteowner = Utils.get_site_noreply()
- welcome.extend(
- (_(''' To visit the general information page for an unadvertised list,
- open a URL similar to this one, but with a '/' and the %(adj)s
- list name appended.
- <p>List administrators, you can visit '''),
- Link(Utils.ScriptURL('admin'),
- _('the list admin overview page')),
- _(''' to find the management interface for your list.
- <p>If you are having trouble using the lists, please contact '''),
- Link('mailto:' + siteowner, siteowner),
- '.<p>'))
-
- table.AddRow([apply(Container, welcome)])
- table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
-
- if advertised:
- table.AddRow(['&nbsp;', '&nbsp;'])
- table.AddRow([Bold(FontAttr(_('List'), size='+2')),
- Bold(FontAttr(_('Description'), size='+2'))
- ])
- highlight = 1
- for url, real_name, description in advertised:
- table.AddRow(
- [Link(url, Bold(real_name)),
- description or Italic(_('[no description available]'))])
- if highlight and config.WEB_HIGHLIGHT_COLOR:
- table.AddRowInfo(table.GetCurrentRowIndex(),
- bgcolor=config.WEB_HIGHLIGHT_COLOR)
- highlight = not highlight
-
- doc.AddItem(table)
- doc.AddItem('<hr>')
- doc.AddItem(MailmanLogo())
- print doc.Format()
-
-
-
-def list_listinfo(mlist, lang):
- # Generate list specific listinfo
- doc = HeadlessDocument()
- doc.set_language(lang)
-
- replacements = mlist.GetStandardReplacements(lang)
-
- if not mlist.digestable or not mlist.nondigestable:
- replacements['<mm-digest-radio-button>'] = ""
- replacements['<mm-undigest-radio-button>'] = ""
- replacements['<mm-digest-question-start>'] = '<!-- '
- replacements['<mm-digest-question-end>'] = ' -->'
- else:
- replacements['<mm-digest-radio-button>'] = mlist.FormatDigestButton()
- replacements['<mm-undigest-radio-button>'] = \
- mlist.FormatUndigestButton()
- replacements['<mm-digest-question-start>'] = ''
- replacements['<mm-digest-question-end>'] = ''
- replacements['<mm-plain-digests-button>'] = \
- mlist.FormatPlainDigestsButton()
- replacements['<mm-mime-digests-button>'] = mlist.FormatMimeDigestsButton()
- replacements['<mm-subscribe-box>'] = mlist.FormatBox('email', size=30)
- replacements['<mm-subscribe-button>'] = mlist.FormatButton(
- 'email-button', text=_('Subscribe'))
- replacements['<mm-new-password-box>'] = mlist.FormatSecureBox('pw')
- replacements['<mm-confirm-password>'] = mlist.FormatSecureBox('pw-conf')
- replacements['<mm-subscribe-form-start>'] = mlist.FormatFormStart(
- 'subscribe')
- # Roster form substitutions
- replacements['<mm-roster-form-start>'] = mlist.FormatFormStart('roster')
- replacements['<mm-roster-option>'] = mlist.FormatRosterOptionForUser(lang)
- # Options form substitutions
- replacements['<mm-options-form-start>'] = mlist.FormatFormStart('options')
- replacements['<mm-editing-options>'] = mlist.FormatEditingOption(lang)
- replacements['<mm-info-button>'] = SubmitButton('UserOptions',
- _('Edit Options')).Format()
- # If only one language is enabled for this mailing list, omit the choice
- # buttons.
- if len(mlist.language_codes) == 1:
- displang = ''
- else:
- displang = mlist.FormatButton('displang-button',
- text = _("View this page in"))
- replacements['<mm-displang-box>'] = displang
- replacements['<mm-lang-form-start>'] = mlist.FormatFormStart('listinfo')
- replacements['<mm-fullname-box>'] = mlist.FormatBox('fullname', size=30)
-
- # Do the expansion.
- doc.AddItem(mlist.ParseTags('listinfo.html', replacements, lang))
- print doc.Format()
-
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/web/Cgi/options.py b/src/web/Cgi/options.py
deleted file mode 100644
index c529a3ea4..000000000
--- a/src/web/Cgi/options.py
+++ /dev/null
@@ -1,1000 +0,0 @@
-# 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/>.
-
-"""Produce and handle the member options."""
-
-import os
-import cgi
-import sys
-import urllib
-import logging
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import MemberAdaptor
-from Mailman import Utils
-from Mailman import i18n
-from Mailman import passwords
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-
-OR = '|'
-SLASH = '/'
-SETLANGUAGE = -1
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-log = logging.getLogger('mailman.error')
-mlog = logging.getLogger('mailman.mischief')
-
-
-
-def main():
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- parts = Utils.GetPathPieces()
- lenparts = parts and len(parts)
- if not parts or lenparts < 1:
- title = _('CGI script error')
- doc.SetTitle(title)
- doc.AddItem(Header(2, title))
- doc.addError(_('Invalid options to CGI script.'))
- doc.AddItem('<hr>')
- doc.AddItem(MailmanLogo())
- print doc.Format()
- return
-
- # get the list and user's name
- listname = parts[0].lower()
- # open list
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- title = _('CGI script error')
- doc.SetTitle(title)
- doc.AddItem(Header(2, title))
- doc.addError(_('No such list <em>%(safelistname)s</em>'))
- doc.AddItem('<hr>')
- doc.AddItem(MailmanLogo())
- print doc.Format()
- log.error('No such list "%s": %s\n', listname, e)
- return
-
- # The total contents of the user's response
- cgidata = cgi.FieldStorage(keep_blank_values=1)
-
- # Set the language for the page. If we're coming from the listinfo cgi,
- # we might have a 'language' key in the cgi data. That was an explicit
- # preference to view the page in, so we should honor that here. If that's
- # not available, use the list's default language.
- language = cgidata.getvalue('language')
- if language not in config.languages.enabled_codes:
- language = mlist.preferred_language
- i18n.set_language(language)
- doc.set_language(language)
-
- if lenparts < 2:
- user = cgidata.getvalue('email')
- if not user:
- # If we're coming from the listinfo page and we left the email
- # address field blank, it's not an error. listinfo.html names the
- # button UserOptions; we can use that as the descriminator.
- if not cgidata.getvalue('UserOptions'):
- doc.addError(_('No address given'))
- loginpage(mlist, doc, None, language)
- print doc.Format()
- return
- else:
- user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:])))
-
- # Avoid cross-site scripting attacks
- safeuser = Utils.websafe(user)
- try:
- Utils.ValidateEmail(user)
- except Errors.EmailAddressError:
- doc.addError(_('Illegal Email Address: %(safeuser)s'))
- loginpage(mlist, doc, None, language)
- print doc.Format()
- return
- # Sanity check the user, but only give the "no such member" error when
- # using public rosters, otherwise, we'll leak membership information.
- if not mlist.isMember(user) and mlist.private_roster == 0:
- doc.addError(_('No such member: %(safeuser)s.'))
- loginpage(mlist, doc, None, language)
- print doc.Format()
- return
-
- # Find the case preserved email address (the one the user subscribed with)
- lcuser = user.lower()
- try:
- cpuser = mlist.getMemberCPAddress(lcuser)
- except Errors.NotAMemberError:
- # This happens if the user isn't a member but we've got private rosters
- cpuser = None
- if lcuser == cpuser:
- cpuser = None
-
- # And now we know the user making the request, so set things up to for the
- # user's stored preferred language, overridden by any form settings for
- # their new language preference.
- userlang = cgidata.getvalue('language')
- if userlang not in config.languages.enabled_codes:
- userlang = mlist.getMemberLanguage(user)
- doc.set_language(userlang)
- i18n.set_language(userlang)
-
- # See if this is VARHELP on topics.
- varhelp = None
- if cgidata.has_key('VARHELP'):
- varhelp = cgidata['VARHELP'].value
- elif os.environ.get('QUERY_STRING'):
- # POST methods, even if their actions have a query string, don't get
- # put into FieldStorage's keys :-(
- qs = cgi.parse_qs(os.environ['QUERY_STRING']).get('VARHELP')
- if qs and isinstance(qs, list):
- varhelp = qs[0]
- if varhelp:
- topic_details(mlist, doc, user, cpuser, userlang, varhelp)
- return
-
- # Are we processing an unsubscription request from the login screen?
- if cgidata.has_key('login-unsub'):
- # Because they can't supply a password for unsubscribing, we'll need
- # to do the confirmation dance.
- if mlist.isMember(user):
- # We must acquire the list lock in order to pend a request.
- try:
- mlist.Lock()
- # If unsubs require admin approval, then this request has to
- # be held. Otherwise, send a confirmation.
- if mlist.unsubscribe_policy:
- mlist.HoldUnsubscription(user)
- doc.addError(_("""Your unsubscription request has been
- forwarded to the list administrator for approval."""),
- tag='')
- else:
- mlist.ConfirmUnsubscription(user, userlang)
- doc.addError(_('The confirmation email has been sent.'),
- tag='')
- mlist.Save()
- finally:
- mlist.Unlock()
- else:
- # Not a member
- if mlist.private_roster == 0:
- # Public rosters
- doc.addError(_('No such member: %(safeuser)s.'))
- else:
- mlog.error('Unsub attempt of non-member w/ private rosters: %s',
- user)
- doc.addError(_('The confirmation email has been sent.'),
- tag='')
- loginpage(mlist, doc, user, language)
- print doc.Format()
- return
-
- # Are we processing a password reminder from the login screen?
- if cgidata.has_key('login-remind'):
- if mlist.isMember(user):
- mlist.MailUserPassword(user)
- doc.addError(
- _('A reminder of your password has been emailed to you.'),
- tag='')
- else:
- # Not a member
- if mlist.private_roster == 0:
- # Public rosters
- doc.addError(_('No such member: %(safeuser)s.'))
- else:
- mlog.error(
- 'Reminder attempt of non-member w/ private rosters: %s',
- user)
- doc.addError(
- _('A reminder of your password has been emailed to you.'),
- tag='')
- loginpage(mlist, doc, user, language)
- print doc.Format()
- return
-
- # Get the password from the form.
- password = cgidata.getvalue('password', '').strip()
- # Check authentication. We need to know if the credentials match the user
- # or the site admin, because they are the only ones who are allowed to
- # change things globally. Specifically, the list admin may not change
- # values globally.
- if config.ALLOW_SITE_ADMIN_COOKIES:
- user_or_siteadmin_context = (config.AuthUser, config.AuthSiteAdmin)
- else:
- # Site and list admins are treated equal so that list admin can pass
- # site admin test. :-(
- user_or_siteadmin_context = (config.AuthUser,)
- is_user_or_siteadmin = mlist.WebAuthenticate(
- user_or_siteadmin_context, password, user)
- # Authenticate, possibly using the password supplied in the login page
- if not is_user_or_siteadmin and \
- not mlist.WebAuthenticate((config.AuthListAdmin,
- config.AuthSiteAdmin),
- password, user):
- # Not authenticated, so throw up the login page again. If they tried
- # to authenticate via cgi (instead of cookie), then print an error
- # message.
- if cgidata.has_key('password'):
- doc.addError(_('Authentication failed.'))
- # So as not to allow membership leakage, prompt for the email
- # address and the password here.
- if mlist.private_roster <> 0:
- mlog.error('Login failure with private rosters: %s', user)
- user = None
- loginpage(mlist, doc, user, language)
- print doc.Format()
- return
-
- # From here on out, the user is okay to view and modify their membership
- # options. The first set of checks does not require the list to be
- # locked.
-
- if cgidata.has_key('logout'):
- print mlist.ZapCookie(config.AuthUser, user)
- loginpage(mlist, doc, user, language)
- print doc.Format()
- return
-
- if cgidata.has_key('emailpw'):
- mlist.MailUserPassword(user)
- options_page(
- mlist, doc, user, cpuser, userlang,
- _('A reminder of your password has been emailed to you.'))
- print doc.Format()
- return
-
- if cgidata.has_key('othersubs'):
- # Only the user or site administrator can view all subscriptions.
- if not is_user_or_siteadmin:
- doc.addError(_("""The list administrator may not view the other
- subscriptions for this user."""), _('Note: '))
- options_page(mlist, doc, user, cpuser, userlang)
- print doc.Format()
- return
- hostname = mlist.host_name
- title = _('List subscriptions for %(safeuser)s on %(hostname)s')
- doc.SetTitle(title)
- doc.AddItem(Header(2, title))
- doc.AddItem(_('''Click on a link to visit your options page for the
- requested mailing list.'''))
-
- # Troll through all the mailing lists that match host_name and see if
- # the user is a member. If so, add it to the list.
- onlists = []
- for gmlist in lists_of_member(mlist, user) + [mlist]:
- url = gmlist.GetOptionsURL(user)
- link = Link(url, gmlist.real_name)
- onlists.append((gmlist.real_name, link))
- onlists.sort()
- items = OrderedList(*[link for name, link in onlists])
- doc.AddItem(items)
- print doc.Format()
- return
-
- if cgidata.has_key('change-of-address'):
- # We could be changing the user's full name, email address, or both.
- # Watch out for non-ASCII characters in the member's name.
- membername = cgidata.getvalue('fullname')
- # Canonicalize the member's name
- membername = Utils.canonstr(membername, language)
- newaddr = cgidata.getvalue('new-address')
- confirmaddr = cgidata.getvalue('confirm-address')
-
- oldname = mlist.getMemberName(user)
- set_address = set_membername = 0
-
- # See if the user wants to change their email address globally. The
- # list admin is /not/ allowed to make global changes.
- globally = cgidata.getvalue('changeaddr-globally')
- if globally and not is_user_or_siteadmin:
- doc.addError(_("""The list administrator may not change the names
- or addresses for this user's other subscriptions. However, the
- subscription for this mailing list has been changed."""),
- _('Note: '))
- globally = False
- # We will change the member's name under the following conditions:
- # - membername has a value
- # - membername has no value, but they /used/ to have a membername
- if membername and membername <> oldname:
- # Setting it to a new value
- set_membername = 1
- if not membername and oldname:
- # Unsetting it
- set_membername = 1
- # We will change the user's address if both newaddr and confirmaddr
- # are non-blank, have the same value, and aren't the currently
- # subscribed email address (when compared case-sensitively). If both
- # are blank, but membername is set, we ignore it, otherwise we print
- # an error.
- msg = ''
- if newaddr and confirmaddr:
- if newaddr <> confirmaddr:
- options_page(mlist, doc, user, cpuser, userlang,
- _('Addresses did not match!'))
- print doc.Format()
- return
- if newaddr == cpuser:
- options_page(mlist, doc, user, cpuser, userlang,
- _('You are already using that email address'))
- print doc.Format()
- return
- # If they're requesting to subscribe an address which is already a
- # member, and they're /not/ doing it globally, then refuse.
- # Otherwise, we'll agree to do it globally (with a warning
- # message) and let ApprovedChangeMemberAddress() handle already a
- # member issues.
- if mlist.isMember(newaddr):
- safenewaddr = Utils.websafe(newaddr)
- if globally:
- listname = mlist.real_name
- msg += _("""\
-The new address you requested %(newaddr)s is already a member of the
-%(listname)s mailing list, however you have also requested a global change of
-address. Upon confirmation, any other mailing list containing the address
-%(safeuser)s will be changed. """)
- # Don't return
- else:
- options_page(
- mlist, doc, user, cpuser, userlang,
- _('The new address is already a member: %(newaddr)s'))
- print doc.Format()
- return
- set_address = 1
- elif (newaddr or confirmaddr) and not set_membername:
- options_page(mlist, doc, user, cpuser, userlang,
- _('Addresses may not be blank'))
- print doc.Format()
- return
- if set_address:
- if cpuser is None:
- cpuser = user
- # Register the pending change after the list is locked
- msg += _('A confirmation message has been sent to %(newaddr)s. ')
- mlist.Lock()
- try:
- try:
- mlist.ChangeMemberAddress(cpuser, newaddr, globally)
- mlist.Save()
- finally:
- mlist.Unlock()
- except Errors.InvalidEmailAddress:
- msg = _('Invalid email address provided')
- except Errors.MMAlreadyAMember:
- msg = _('%(newaddr)s is already a member of the list.')
- except Errors.MembershipIsBanned:
- owneraddr = mlist.GetOwnerEmail()
- msg = _("""%(newaddr)s is banned from this list. If you
- think this restriction is erroneous, please contact
- the list owners at %(owneraddr)s.""")
-
- if set_membername:
- mlist.Lock()
- try:
- mlist.ChangeMemberName(user, membername, globally)
- mlist.Save()
- finally:
- mlist.Unlock()
- msg += _('Member name successfully changed. ')
-
- options_page(mlist, doc, user, cpuser, userlang, msg)
- print doc.Format()
- return
-
- if cgidata.has_key('changepw'):
- newpw = cgidata.getvalue('newpw')
- confirmpw = cgidata.getvalue('confpw')
- if not newpw or not confirmpw:
- options_page(mlist, doc, user, cpuser, userlang,
- _('Passwords may not be blank'))
- print doc.Format()
- return
- if newpw <> confirmpw:
- options_page(mlist, doc, user, cpuser, userlang,
- _('Passwords did not match!'))
- print doc.Format()
- return
-
- # See if the user wants to change their passwords globally, however
- # the list admin is /not/ allowed to change passwords globally.
- pw_globally = cgidata.getvalue('pw-globally')
- if pw_globally and not is_user_or_siteadmin:
- doc.addError(_("""The list administrator may not change the
- password for this user's other subscriptions. However, the
- password for this mailing list has been changed."""),
- _('Note: '))
- pw_globally = False
-
- mlists = [mlist]
-
- if pw_globally:
- mlists.extend(lists_of_member(mlist, user))
-
- pw = passwords.make_secret(newpw, config.PASSWORD_SCHEME)
- for gmlist in mlists:
- change_password(gmlist, user, pw)
-
- # Regenerate the cookie so a re-authorization isn't necessary
- print mlist.MakeCookie(config.AuthUser, user)
- options_page(mlist, doc, user, cpuser, userlang,
- _('Password successfully changed.'))
- print doc.Format()
- return
-
- if cgidata.has_key('unsub'):
- # Was the confirming check box turned on?
- if not cgidata.getvalue('unsubconfirm'):
- options_page(
- mlist, doc, user, cpuser, userlang,
- _('''You must confirm your unsubscription request by turning
- on the checkbox below the <em>Unsubscribe</em> button. You
- have not been unsubscribed!'''))
- print doc.Format()
- return
-
- mlist.Lock()
- needapproval = False
- try:
- try:
- mlist.DeleteMember(
- user, 'via the member options page', userack=1)
- except Errors.MMNeedApproval:
- needapproval = True
- mlist.Save()
- finally:
- mlist.Unlock()
- # Now throw up some results page, with appropriate links. We can't
- # drop them back into their options page, because that's gone now!
- fqdn_listname = mlist.GetListEmail()
- owneraddr = mlist.GetOwnerEmail()
- url = mlist.GetScriptURL('listinfo')
-
- title = _('Unsubscription results')
- doc.SetTitle(title)
- doc.AddItem(Header(2, title))
- if needapproval:
- doc.AddItem(_("""Your unsubscription request has been received and
- forwarded on to the list moderators for approval. You will
- receive notification once the list moderators have made their
- decision."""))
- else:
- doc.AddItem(_("""You have been successfully unsubscribed from the
- mailing list %(fqdn_listname)s. If you were receiving digest
- deliveries you may get one more digest. If you have any questions
- about your unsubscription, please contact the list owners at
- %(owneraddr)s."""))
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- return
-
- if cgidata.has_key('options-submit'):
- # Digest action flags
- digestwarn = 0
- cantdigest = 0
- mustdigest = 0
-
- newvals = []
- # First figure out which options have changed. The item names come
- # from FormatOptionButton() in HTMLFormatter.py
- for item, flag in (('digest', config.Digests),
- ('mime', config.DisableMime),
- ('dontreceive', config.DontReceiveOwnPosts),
- ('ackposts', config.AcknowledgePosts),
- ('disablemail', config.DisableDelivery),
- ('conceal', config.ConcealSubscription),
- ('remind', config.SuppressPasswordReminder),
- ('rcvtopic', config.ReceiveNonmatchingTopics),
- ('nodupes', config.DontReceiveDuplicates),
- ):
- try:
- newval = int(cgidata.getvalue(item))
- except (TypeError, ValueError):
- newval = None
-
- # Skip this option if there was a problem or it wasn't changed.
- # Note that delivery status is handled separate from the options
- # flags.
- if newval is None:
- continue
- elif flag == config.DisableDelivery:
- status = mlist.getDeliveryStatus(user)
- # Here, newval == 0 means enable, newval == 1 means disable
- if not newval and status <> MemberAdaptor.ENABLED:
- newval = MemberAdaptor.ENABLED
- elif newval and status == MemberAdaptor.ENABLED:
- newval = MemberAdaptor.BYUSER
- else:
- continue
- elif newval == mlist.getMemberOption(user, flag):
- continue
- # Should we warn about one more digest?
- if flag == config.Digests and \
- newval == 0 and mlist.getMemberOption(user, flag):
- digestwarn = 1
-
- newvals.append((flag, newval))
-
- # The user language is handled a little differently
- if userlang not in mlist.language_codes:
- newvals.append((SETLANGUAGE, mlist.preferred_language))
- else:
- newvals.append((SETLANGUAGE, userlang))
-
- # Process user selected topics, but don't make the changes to the
- # MailList object; we must do that down below when the list is
- # locked.
- topicnames = cgidata.getvalue('usertopic')
- if topicnames:
- # Some topics were selected. topicnames can actually be a string
- # or a list of strings depending on whether more than one topic
- # was selected or not.
- if not isinstance(topicnames, list):
- # Assume it was a bare string, so listify it
- topicnames = [topicnames]
- # unquote the topic names
- topicnames = [urllib.unquote_plus(n) for n in topicnames]
-
- # The standard sigterm handler (see above)
- def sigterm_handler(signum, frame, mlist=mlist):
- mlist.Unlock()
- sys.exit(0)
-
- # Now, lock the list and perform the changes
- mlist.Lock()
- try:
- # `values' is a tuple of flags and the web values
- for flag, newval in newvals:
- # Handle language settings differently
- if flag == SETLANGUAGE:
- mlist.setMemberLanguage(user, newval)
- # Handle delivery status separately
- elif flag == config.DisableDelivery:
- mlist.setDeliveryStatus(user, newval)
- else:
- try:
- mlist.setMemberOption(user, flag, newval)
- except Errors.CantDigestError:
- cantdigest = 1
- except Errors.MustDigestError:
- mustdigest = 1
- # Set the topics information.
- mlist.setMemberTopics(user, topicnames)
- mlist.Save()
- finally:
- mlist.Unlock()
-
- # A bag of attributes for the global options
- class Global:
- enable = None
- remind = None
- nodupes = None
- mime = None
- def __nonzero__(self):
- return len(self.__dict__.keys()) > 0
-
- globalopts = Global()
-
- # The enable/disable option and the password remind option may have
- # their global flags sets.
- if cgidata.getvalue('deliver-globally'):
- # Yes, this is inefficient, but the list is so small it shouldn't
- # make much of a difference.
- for flag, newval in newvals:
- if flag == config.DisableDelivery:
- globalopts.enable = newval
- break
-
- if cgidata.getvalue('remind-globally'):
- for flag, newval in newvals:
- if flag == config.SuppressPasswordReminder:
- globalopts.remind = newval
- break
-
- if cgidata.getvalue('nodupes-globally'):
- for flag, newval in newvals:
- if flag == config.DontReceiveDuplicates:
- globalopts.nodupes = newval
- break
-
- if cgidata.getvalue('mime-globally'):
- for flag, newval in newvals:
- if flag == config.DisableMime:
- globalopts.mime = newval
- break
-
- # Change options globally, but only if this is the user or site admin,
- # /not/ if this is the list admin.
- if globalopts:
- if not is_user_or_siteadmin:
- doc.addError(_("""The list administrator may not change the
- options for this user's other subscriptions. However the
- options for this mailing list subscription has been
- changed."""), _('Note: '))
- else:
- for gmlist in lists_of_member(mlist, user):
- global_options(gmlist, user, globalopts)
-
- # Now print the results
- if cantdigest:
- msg = _('''The list administrator has disabled digest delivery for
- this list, so your delivery option has not been set. However your
- other options have been set successfully.''')
- elif mustdigest:
- msg = _('''The list administrator has disabled non-digest delivery
- for this list, so your delivery option has not been set. However
- your other options have been set successfully.''')
- else:
- msg = _('You have successfully set your options.')
-
- if digestwarn:
- msg += _('You may get one last digest.')
-
- options_page(mlist, doc, user, cpuser, userlang, msg)
- print doc.Format()
- return
-
- if mlist.isMember(user):
- options_page(mlist, doc, user, cpuser, userlang)
- else:
- loginpage(mlist, doc, user, userlang)
- print doc.Format()
-
-
-
-def options_page(mlist, doc, user, cpuser, userlang, message=''):
- # The bulk of the document will come from the options.html template, which
- # includes it's own html armor (head tags, etc.). Suppress the head that
- # Document() derived pages get automatically.
- doc.suppress_head = 1
-
- if mlist.obscure_addresses:
- presentable_user = Utils.ObscureEmail(user, for_text=1)
- if cpuser is not None:
- cpuser = Utils.ObscureEmail(cpuser, for_text=1)
- else:
- presentable_user = user
-
- fullname = Utils.uncanonstr(mlist.getMemberName(user), userlang)
- if fullname:
- presentable_user += ', %s' % fullname
-
- # Do replacements
- replacements = mlist.GetStandardReplacements(userlang)
- replacements['<mm-results>'] = Bold(FontSize('+1', message)).Format()
- replacements['<mm-digest-radio-button>'] = mlist.FormatOptionButton(
- config.Digests, 1, user)
- replacements['<mm-undigest-radio-button>'] = mlist.FormatOptionButton(
- config.Digests, 0, user)
- replacements['<mm-plain-digests-button>'] = mlist.FormatOptionButton(
- config.DisableMime, 1, user)
- replacements['<mm-mime-digests-button>'] = mlist.FormatOptionButton(
- config.DisableMime, 0, user)
- replacements['<mm-global-mime-button>'] = (
- CheckBox('mime-globally', 1, checked=0).Format())
- replacements['<mm-delivery-enable-button>'] = mlist.FormatOptionButton(
- config.DisableDelivery, 0, user)
- replacements['<mm-delivery-disable-button>'] = mlist.FormatOptionButton(
- config.DisableDelivery, 1, user)
- replacements['<mm-disabled-notice>'] = mlist.FormatDisabledNotice(user)
- replacements['<mm-dont-ack-posts-button>'] = mlist.FormatOptionButton(
- config.AcknowledgePosts, 0, user)
- replacements['<mm-ack-posts-button>'] = mlist.FormatOptionButton(
- config.AcknowledgePosts, 1, user)
- replacements['<mm-receive-own-mail-button>'] = mlist.FormatOptionButton(
- config.DontReceiveOwnPosts, 0, user)
- replacements['<mm-dont-receive-own-mail-button>'] = (
- mlist.FormatOptionButton(config.DontReceiveOwnPosts, 1, user))
- replacements['<mm-dont-get-password-reminder-button>'] = (
- mlist.FormatOptionButton(config.SuppressPasswordReminder, 1, user))
- replacements['<mm-get-password-reminder-button>'] = (
- mlist.FormatOptionButton(config.SuppressPasswordReminder, 0, user))
- replacements['<mm-public-subscription-button>'] = (
- mlist.FormatOptionButton(config.ConcealSubscription, 0, user))
- replacements['<mm-hide-subscription-button>'] = mlist.FormatOptionButton(
- config.ConcealSubscription, 1, user)
- replacements['<mm-dont-receive-duplicates-button>'] = (
- mlist.FormatOptionButton(config.DontReceiveDuplicates, 1, user))
- replacements['<mm-receive-duplicates-button>'] = (
- mlist.FormatOptionButton(config.DontReceiveDuplicates, 0, user))
- replacements['<mm-unsubscribe-button>'] = (
- mlist.FormatButton('unsub', _('Unsubscribe')) + '<br>' +
- CheckBox('unsubconfirm', 1, checked=0).Format() +
- _('<em>Yes, I really want to unsubscribe</em>'))
- replacements['<mm-new-pass-box>'] = mlist.FormatSecureBox('newpw')
- replacements['<mm-confirm-pass-box>'] = mlist.FormatSecureBox('confpw')
- replacements['<mm-change-pass-button>'] = (
- mlist.FormatButton('changepw', _("Change My Password")))
- replacements['<mm-other-subscriptions-submit>'] = (
- mlist.FormatButton('othersubs',
- _('List my other subscriptions')))
- replacements['<mm-form-start>'] = (
- mlist.FormatFormStart('options', user))
- replacements['<mm-user>'] = user
- replacements['<mm-presentable-user>'] = presentable_user
- replacements['<mm-email-my-pw>'] = mlist.FormatButton(
- 'emailpw', (_('Email My Password To Me')))
- replacements['<mm-umbrella-notice>'] = (
- mlist.FormatUmbrellaNotice(user, _("password")))
- replacements['<mm-logout-button>'] = (
- mlist.FormatButton('logout', _('Log out')))
- replacements['<mm-options-submit-button>'] = mlist.FormatButton(
- 'options-submit', _('Submit My Changes'))
- replacements['<mm-global-pw-changes-button>'] = (
- CheckBox('pw-globally', 1, checked=0).Format())
- replacements['<mm-global-deliver-button>'] = (
- CheckBox('deliver-globally', 1, checked=0).Format())
- replacements['<mm-global-remind-button>'] = (
- CheckBox('remind-globally', 1, checked=0).Format())
- replacements['<mm-global-nodupes-button>'] = (
- CheckBox('nodupes-globally', 1, checked=0).Format())
-
- days = int(config.PENDING_REQUEST_LIFE / config.days(1))
- if days > 1:
- units = _('days')
- else:
- units = _('day')
- replacements['<mm-pending-days>'] = _('%(days)d %(units)s')
-
- replacements['<mm-new-address-box>'] = mlist.FormatBox('new-address')
- replacements['<mm-confirm-address-box>'] = mlist.FormatBox(
- 'confirm-address')
- replacements['<mm-change-address-button>'] = mlist.FormatButton(
- 'change-of-address', _('Change My Address and Name'))
- replacements['<mm-global-change-of-address>'] = CheckBox(
- 'changeaddr-globally', 1, checked=0).Format()
- replacements['<mm-fullname-box>'] = mlist.FormatBox(
- 'fullname', value=fullname)
-
- # Create the topics radios. BAW: what if the list admin deletes a topic,
- # but the user still wants to get that topic message?
- usertopics = mlist.getMemberTopics(user)
- if mlist.topics:
- table = Table(border="0")
- for name, pattern, description, emptyflag in mlist.topics:
- if emptyflag:
- continue
- quotedname = urllib.quote_plus(name)
- details = Link(mlist.GetScriptURL('options') +
- '/%s/?VARHELP=%s' % (user, quotedname),
- ' (Details)')
- if name in usertopics:
- checked = 1
- else:
- checked = 0
- table.AddRow([CheckBox('usertopic', quotedname, checked=checked),
- name + details.Format()])
- topicsfield = table.Format()
- else:
- topicsfield = _('<em>No topics defined</em>')
- replacements['<mm-topics>'] = topicsfield
- replacements['<mm-suppress-nonmatching-topics>'] = (
- mlist.FormatOptionButton(config.ReceiveNonmatchingTopics, 0, user))
- replacements['<mm-receive-nonmatching-topics>'] = (
- mlist.FormatOptionButton(config.ReceiveNonmatchingTopics, 1, user))
-
- if cpuser is not None:
- replacements['<mm-case-preserved-user>'] = _('''
-You are subscribed to this list with the case-preserved address
-<em>%(cpuser)s</em>.''')
- else:
- replacements['<mm-case-preserved-user>'] = ''
-
- doc.AddItem(mlist.ParseTags('options.html', replacements, userlang))
-
-
-
-def loginpage(mlist, doc, user, lang):
- realname = mlist.real_name
- actionurl = mlist.GetScriptURL('options')
- if user is None:
- title = _('%(realname)s list: member options login page')
- extra = _('email address and ')
- else:
- safeuser = Utils.websafe(user)
- title = _('%(realname)s list: member options for user %(safeuser)s')
- obuser = Utils.ObscureEmail(user)
- extra = ''
- # Set up the title
- doc.SetTitle(title)
- # We use a subtable here so we can put a language selection box in
- table = Table(width='100%', border=0, cellspacing=4, cellpadding=5)
- # If only one language is enabled for this mailing list, omit the choice
- # buttons.
- table.AddRow([Center(Header(2, title))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
- if len(mlist.language_codes) > 1:
- langform = Form(actionurl)
- langform.AddItem(SubmitButton('displang-button',
- _('View this page in')))
- langform.AddItem(mlist.GetLangSelectBox(lang))
- if user:
- langform.AddItem(Hidden('email', user))
- table.AddRow([Center(langform)])
- doc.AddItem(table)
- # Preamble
- # Set up the login page
- form = Form(actionurl)
- table = Table(width='100%', border=0, cellspacing=4, cellpadding=5)
- table.AddRow([_("""In order to change your membership option, you must
- first log in by giving your %(extra)smembership password in the section
- below. If you don't remember your membership password, you can have it
- emailed to you by clicking on the button below. If you just want to
- unsubscribe from this list, click on the <em>Unsubscribe</em> button and a
- confirmation message will be sent to you.
-
- <p><strong><em>Important:</em></strong> From this point on, you must have
- cookies enabled in your browser, otherwise none of your changes will take
- effect.
- """)])
- # Password and login button
- ptable = Table(width='50%', border=0, cellspacing=4, cellpadding=5)
- if user is None:
- ptable.AddRow([Label(_('Email address:')),
- TextBox('email', size=20)])
- else:
- ptable.AddRow([Hidden('email', user)])
- ptable.AddRow([Label(_('Password:')),
- PasswordBox('password', size=20)])
- ptable.AddRow([Center(SubmitButton('login', _('Log in')))])
- ptable.AddCellInfo(ptable.GetCurrentRowIndex(), 0, colspan=2)
- table.AddRow([Center(ptable)])
- # Unsubscribe section
- table.AddRow([Center(Header(2, _('Unsubscribe')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
-
- table.AddRow([_("""By clicking on the <em>Unsubscribe</em> button, a
- confirmation message will be emailed to you. This message will have a
- link that you should click on to complete the removal process (you can
- also confirm by email; see the instructions in the confirmation
- message).""")])
-
- table.AddRow([Center(SubmitButton('login-unsub', _('Unsubscribe')))])
- # Password reminder section
- table.AddRow([Center(Header(2, _('Password reminder')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
-
- table.AddRow([_("""By clicking on the <em>Remind</em> button, your
- password will be emailed to you.""")])
-
- table.AddRow([Center(SubmitButton('login-remind', _('Remind')))])
- # Finish up glomming together the login page
- form.AddItem(table)
- doc.AddItem(form)
- doc.AddItem(mlist.GetMailmanFooter())
-
-
-
-def lists_of_member(mlist, user):
- hostname = mlist.host_name
- onlists = []
- for listname in config.list_manager.names:
- # The current list will always handle things in the mainline
- if listname == mlist.internal_name():
- continue
- glist = MailList.MailList(listname, lock=0)
- if glist.host_name <> hostname:
- continue
- if not glist.isMember(user):
- continue
- onlists.append(glist)
- return onlists
-
-
-
-def change_password(mlist, user, newpw):
- # Must own the list lock!
- mlist.Lock()
- try:
- # Change the user's password. The password must already have been
- # compared to the confirmpw and otherwise been vetted for
- # acceptability.
- mlist.setMemberPassword(user, newpw)
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def global_options(mlist, user, globalopts):
- # Is there anything to do?
- for attr in dir(globalopts):
- if attr.startswith('_'):
- continue
- if getattr(globalopts, attr) is not None:
- break
- else:
- return
-
- def sigterm_handler(signum, frame, mlist=mlist):
- # Make sure the list gets unlocked...
- mlist.Unlock()
- # ...and ensure we exit, otherwise race conditions could cause us to
- # enter MailList.Save() while we're in the unlocked state, and that
- # could be bad!
- sys.exit(0)
-
- # Must own the list lock!
- mlist.Lock()
- try:
- if globalopts.enable is not None:
- mlist.setDeliveryStatus(user, globalopts.enable)
-
- if globalopts.remind is not None:
- mlist.setMemberOption(user, config.SuppressPasswordReminder,
- globalopts.remind)
-
- if globalopts.nodupes is not None:
- mlist.setMemberOption(user, config.DontReceiveDuplicates,
- globalopts.nodupes)
-
- if globalopts.mime is not None:
- mlist.setMemberOption(user, config.DisableMime, globalopts.mime)
-
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def topic_details(mlist, doc, user, cpuser, userlang, varhelp):
- # Find out which topic the user wants to get details of
- reflist = varhelp.split('/')
- name = None
- topicname = _('<missing>')
- if len(reflist) == 1:
- topicname = urllib.unquote_plus(reflist[0])
- for name, pattern, description, emptyflag in mlist.topics:
- if name == topicname:
- break
- else:
- name = None
-
- if not name:
- options_page(mlist, doc, user, cpuser, userlang,
- _('Requested topic is not valid: %(topicname)s'))
- print doc.Format()
- return
-
- table = Table(border=3, width='100%')
- table.AddRow([Center(Bold(_('Topic filter details')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
- bgcolor=config.WEB_SUBHEADER_COLOR)
- table.AddRow([Bold(Label(_('Name:'))),
- Utils.websafe(name)])
- table.AddRow([Bold(Label(_('Pattern (as regexp):'))),
- '<pre>' + Utils.websafe(OR.join(pattern.splitlines()))
- + '</pre>'])
- table.AddRow([Bold(Label(_('Description:'))),
- Utils.websafe(description)])
- # Make colors look nice
- for row in range(1, 4):
- table.AddCellInfo(row, 0, bgcolor=config.WEB_ADMINITEM_COLOR)
-
- options_page(mlist, doc, user, cpuser, userlang, table.Format())
- print doc.Format()
diff --git a/src/web/Cgi/private.py b/src/web/Cgi/private.py
deleted file mode 100644
index 87cad7966..000000000
--- a/src/web/Cgi/private.py
+++ /dev/null
@@ -1,190 +0,0 @@
-# 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/>.
-
-"""Provide a password-interface wrapper around private archives."""
-
-import os
-import sys
-import cgi
-import logging
-import mimetypes
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-# Set up i18n. Until we know which list is being requested, we use the
-# server's default.
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-SLASH = '/'
-
-log = logging.getLogger('mailman.error')
-mlog = logging.getLogger('mailman.mischief')
-
-
-
-def true_path(path):
- "Ensure that the path is safe by removing .."
- # Workaround for path traverse vulnerability. Unsuccessful attempts will
- # be logged in logs/error.
- parts = [x for x in path.split(SLASH) if x not in ('.', '..')]
- return SLASH.join(parts)[1:]
-
-
-
-def guess_type(url, strict):
- if hasattr(mimetypes, 'common_types'):
- return mimetypes.guess_type(url, strict)
- return mimetypes.guess_type(url)
-
-
-
-def main():
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- parts = Utils.GetPathPieces()
- if not parts:
- doc.SetTitle(_("Private Archive Error"))
- doc.AddItem(Header(3, _("You must specify a list.")))
- print doc.Format()
- return
-
- path = os.environ.get('PATH_INFO')
- tpath = true_path(path)
- if tpath <> path[1:]:
- msg = _('Private archive - "./" and "../" not allowed in URL.')
- doc.SetTitle(msg)
- doc.AddItem(Header(2, msg))
- print doc.Format()
- mlog.error('Private archive hostile path: %s', path)
- return
- # BAW: This needs to be converted to the Site module abstraction
- true_filename = os.path.join(
- config.PRIVATE_ARCHIVE_FILE_DIR, tpath)
-
- listname = parts[0].lower()
- mboxfile = ''
- if len(parts) > 1:
- mboxfile = parts[1]
-
- # See if it's the list's mbox file is being requested
- if listname.endswith('.mbox') and mboxfile.endswith('.mbox') and \
- listname[:-5] == mboxfile[:-5]:
- listname = listname[:-5]
- else:
- mboxfile = ''
-
- # If it's a directory, we have to append index.html in this script. We
- # must also check for a gzipped file, because the text archives are
- # usually stored in compressed form.
- if os.path.isdir(true_filename):
- true_filename = true_filename + '/index.html'
- if not os.path.exists(true_filename) and \
- os.path.exists(true_filename + '.gz'):
- true_filename = true_filename + '.gz'
-
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- msg = _('No such list <em>%(safelistname)s</em>')
- doc.SetTitle(_("Private Archive Error - %(msg)s"))
- doc.AddItem(Header(2, msg))
- print doc.Format()
- log.error('No such list "%s": %s\n', listname, e)
- return
-
- i18n.set_language(mlist.preferred_language)
- doc.set_language(mlist.preferred_language)
-
- cgidata = cgi.FieldStorage()
- username = cgidata.getvalue('username', '')
- password = cgidata.getvalue('password', '')
-
- is_auth = 0
- realname = mlist.real_name
- message = ''
-
- if not mlist.WebAuthenticate((config.AuthUser,
- config.AuthListModerator,
- config.AuthListAdmin,
- config.AuthSiteAdmin),
- password, username):
- if cgidata.has_key('submit'):
- # This is a re-authorization attempt
- message = Bold(FontSize('+1', _('Authorization failed.'))).Format()
- # Output the password form
- charset = Utils.GetCharSet(mlist.preferred_language)
- print 'Content-type: text/html; charset=' + charset + '\n\n'
- # Put the original full path in the authorization form, but avoid
- # trailing slash if we're not adding parts. We add it below.
- action = mlist.GetScriptURL('private')
- if parts[1:]:
- action = os.path.join(action, SLASH.join(parts[1:]))
- # If we added '/index.html' to true_filename, add a slash to the URL.
- # We need this because we no longer add the trailing slash in the
- # private.html template. It's always OK to test parts[-1] since we've
- # already verified parts[0] is listname. The basic rule is if the
- # post URL (action) is a directory, it must be slash terminated, but
- # not if it's a file. Otherwise, relative links in the target archive
- # page don't work.
- if true_filename.endswith('/index.html') and parts[-1] <> 'index.html':
- action += SLASH
- # Escape web input parameter to avoid cross-site scripting.
- print Utils.maketext(
- 'private.html',
- {'action' : Utils.websafe(action),
- 'realname': mlist.real_name,
- 'message' : message,
- }, mlist=mlist)
- return
-
- lang = mlist.getMemberLanguage(username)
- i18n.set_language(lang)
- doc.set_language(lang)
-
- # Authorization confirmed... output the desired file
- try:
- ctype, enc = guess_type(path, strict=0)
- if ctype is None:
- ctype = 'text/html'
- if mboxfile:
- f = open(os.path.join(mlist.archive_dir() + '.mbox',
- mlist.internal_name() + '.mbox'))
- ctype = 'text/plain'
- elif true_filename.endswith('.gz'):
- import gzip
- f = gzip.open(true_filename, 'r')
- else:
- f = open(true_filename, 'r')
- except IOError:
- msg = _('Private archive file not found')
- doc.SetTitle(msg)
- doc.AddItem(Header(2, msg))
- print doc.Format()
- log.error('Private archive file not found: %s', true_filename)
- else:
- print 'Content-type: %s\n' % ctype
- sys.stdout.write(f.read())
- f.close()
diff --git a/src/web/Cgi/rmlist.py b/src/web/Cgi/rmlist.py
deleted file mode 100644
index 4b62f09fb..000000000
--- a/src/web/Cgi/rmlist.py
+++ /dev/null
@@ -1,243 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Remove/delete mailing lists through the web."""
-
-import os
-import cgi
-import sys
-import errno
-import shutil
-import logging
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-log = logging.getLogger('mailman.error')
-mlog = logging.getLogger('mailman.mischief')
-
-
-
-def main():
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- cgidata = cgi.FieldStorage()
- parts = Utils.GetPathPieces()
-
- if not parts:
- # Bad URL specification
- title = _('Bad URL specification')
- doc.SetTitle(title)
- doc.AddItem(
- Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
- doc.AddItem('<hr>')
- doc.AddItem(MailmanLogo())
- print doc.Format()
- log.error('Bad URL specification: %s', parts)
- return
-
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- title = _('No such list <em>%(safelistname)s</em>')
- doc.SetTitle(title)
- doc.AddItem(
- Header(3,
- Bold(FontAttr(title, color='#ff0000', size='+2'))))
- doc.AddItem('<hr>')
- doc.AddItem(MailmanLogo())
- print doc.Format()
- log.error('No such list "%s": %s\n', listname, e)
- return
-
- # Now that we have a valid mailing list, set the language
- i18n.set_language(mlist.preferred_language)
- doc.set_language(mlist.preferred_language)
-
- # Be sure the list owners are not sneaking around!
- if not config.OWNERS_CAN_DELETE_THEIR_OWN_LISTS:
- title = _("You're being a sneaky list owner!")
- doc.SetTitle(title)
- doc.AddItem(
- Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- mlog.error('Attempt to sneakily delete a list: %s', listname)
- return
-
- if cgidata.has_key('doit'):
- process_request(doc, cgidata, mlist)
- print doc.Format()
- return
-
- request_deletion(doc, mlist)
- # Always add the footer and print the document
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
-
-
-
-def process_request(doc, cgidata, mlist):
- password = cgidata.getvalue('password', '').strip()
- try:
- delarchives = int(cgidata.getvalue('delarchives', '0'))
- except ValueError:
- delarchives = 0
-
- # Removing a list is limited to the list-creator (a.k.a. list-destroyer),
- # the list-admin, or the site-admin. Don't use WebAuthenticate here
- # because we want to be sure the actual typed password is valid, not some
- # password sitting in a cookie.
- if mlist.Authenticate((config.AuthCreator,
- config.AuthListAdmin,
- config.AuthSiteAdmin),
- password) == config.UnAuthorized:
- request_deletion(
- doc, mlist,
- _('You are not authorized to delete this mailing list'))
- return
-
- # Do the MTA-specific list deletion tasks
- if config.MTA:
- modname = 'Mailman.MTA.' + config.MTA
- __import__(modname)
- sys.modules[modname].remove(mlist, cgi=1)
-
- REMOVABLES = ['lists/%s']
-
- if delarchives:
- REMOVABLES.extend(['archives/private/%s',
- 'archives/private/%s.mbox',
- 'archives/public/%s',
- 'archives/public/%s.mbox',
- ])
-
- problems = 0
- listname = mlist.internal_name()
- for dirtmpl in REMOVABLES:
- dir = os.path.join(config.VAR_DIR, dirtmpl % listname)
- if os.path.islink(dir):
- try:
- os.unlink(dir)
- except OSError, e:
- if e.errno not in (errno.EACCES, errno.EPERM): raise
- problems += 1
- log.error('link %s not deleted due to permission problems', dir)
- elif os.path.isdir(dir):
- try:
- shutil.rmtree(dir)
- except OSError, e:
- if e.errno not in (errno.EACCES, errno.EPERM): raise
- problems += 1
- log.error('directory %s not deleted due to permission problems',
- dir)
-
- title = _('Mailing list deletion results')
- doc.SetTitle(title)
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
- if not problems:
- table.AddRow([_('''You have successfully deleted the mailing list
- <b>%(listname)s</b>.''')])
- else:
- sitelist = mlist.no_reply_address
- table.AddRow([_('''There were some problems deleting the mailing list
- <b>%(listname)s</b>. Contact your site administrator at %(sitelist)s
- for details.''')])
- doc.AddItem(table)
- doc.AddItem('<hr>')
- doc.AddItem(_('Return to the ') +
- Link(Utils.ScriptURL('listinfo'),
- _('general list overview')).Format())
- doc.AddItem(_('<br>Return to the ') +
- Link(Utils.ScriptURL('admin'),
- _('administrative list overview')).Format())
- doc.AddItem(MailmanLogo())
-
-
-
-def request_deletion(doc, mlist, errmsg=None):
- realname = mlist.real_name
- title = _('Permanently remove mailing list <em>%(realname)s</em>')
- doc.SetTitle(title)
-
- table = Table(border=0, width='100%')
- table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
- table.AddCellInfo(table.GetCurrentRowIndex(), 0,
- bgcolor=config.WEB_HEADER_COLOR)
-
- # Add any error message
- if errmsg:
- table.AddRow([Header(3, Bold(
- FontAttr(_('Error: '), color='#ff0000', size='+2').Format() +
- Italic(errmsg).Format()))])
-
- table.AddRow([_("""This page allows you as the list owner, to permanent
- remove this mailing list from the system. <strong>This action is not
- undoable</strong> so you should undertake it only if you are absolutely
- sure this mailing list has served its purpose and is no longer necessary.
-
- <p>Note that no warning will be sent to your list members and after this
- action, any subsequent messages sent to the mailing list, or any of its
- administrative addreses will bounce.
-
- <p>You also have the option of removing the archives for this mailing list
- at this time. It is almost always recommended that you do
- <strong>not</strong> remove the archives, since they serve as the
- historical record of your mailing list.
-
- <p>For your safety, you will be asked to reconfirm the list password.
- """)])
- GREY = config.WEB_ADMINITEM_COLOR
- form = Form(mlist.GetScriptURL('rmlist'))
- ftable = Table(border=0, cols='2', width='100%',
- cellspacing=3, cellpadding=4)
-
- ftable.AddRow([Label(_('List password:')), PasswordBox('password')])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- ftable.AddRow([Label(_('Also delete archives?')),
- RadioButtonArray('delarchives', (_('No'), _('Yes')),
- checked=0, values=(0, 1))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
-
- ftable.AddRow([Center(Link(
- mlist.GetScriptURL('admin'),
- _('<b>Cancel</b> and return to list administration')))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
-
- ftable.AddRow([Center(SubmitButton('doit', _('Delete this list')))])
- ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
- form.AddItem(ftable)
- table.AddRow([form])
- doc.AddItem(table)
diff --git a/src/web/Cgi/roster.py b/src/web/Cgi/roster.py
deleted file mode 100644
index 2351d6915..000000000
--- a/src/web/Cgi/roster.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# 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/>.
-
-"""Produce subscriber roster, using listinfo form data, roster.html template.
-
-Takes listname in PATH_INFO.
-"""
-
-
-# We don't need to lock in this script, because we're never going to change
-# data.
-
-import os
-import cgi
-import sys
-import urllib
-import logging
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-log = logging.getLogger('mailman.error')
-
-
-
-def main():
- parts = Utils.GetPathPieces()
- if not parts:
- error_page(_('Invalid options to CGI script'))
- return
-
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- error_page(_('No such list <em>%(safelistname)s</em>'))
- log.error('roster: no such list "%s": %s', listname, e)
- return
-
- cgidata = cgi.FieldStorage()
-
- # messages in form should go in selected language (if any...)
- lang = cgidata.getvalue('language')
- if lang not in config.languages.enabled_codes:
- lang = mlist.preferred_language
- i18n.set_language(lang)
-
- # Perform authentication for protected rosters. If the roster isn't
- # protected, then anybody can see the pages. If members-only or
- # "admin"-only, then we try to cookie authenticate the user, and failing
- # that, we check roster-email and roster-pw fields for a valid password.
- # (also allowed: the list moderator, the list admin, and the site admin).
- if mlist.private_roster == 0:
- # No privacy
- ok = 1
- elif mlist.private_roster == 1:
- # Members only
- addr = cgidata.getvalue('roster-email', '')
- password = cgidata.getvalue('roster-pw', '')
- ok = mlist.WebAuthenticate((config.AuthUser,
- config.AuthListModerator,
- config.AuthListAdmin,
- config.AuthSiteAdmin),
- password, addr)
- else:
- # Admin only, so we can ignore the address field
- password = cgidata.getvalue('roster-pw', '')
- ok = mlist.WebAuthenticate((config.AuthListModerator,
- config.AuthListAdmin,
- config.AuthSiteAdmin),
- password)
- if not ok:
- realname = mlist.real_name
- doc = Document()
- doc.set_language(lang)
- error_page_doc(doc, _('%(realname)s roster authentication failed.'))
- doc.AddItem(mlist.GetMailmanFooter())
- print doc.Format()
- return
-
- # The document and its language
- doc = HeadlessDocument()
- doc.set_language(lang)
-
- replacements = mlist.GetAllReplacements(lang)
- replacements['<mm-displang-box>'] = mlist.FormatButton(
- 'displang-button',
- text = _('View this page in'))
- replacements['<mm-lang-form-start>'] = mlist.FormatFormStart('roster')
- doc.AddItem(mlist.ParseTags('roster.html', replacements, lang))
- print doc.Format()
-
-
-
-def error_page(errmsg):
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
- error_page_doc(doc, errmsg)
- print doc.Format()
-
-
-def error_page_doc(doc, errmsg, *args):
- # Produce a simple error-message page on stdout and exit.
- doc.SetTitle(_("Error"))
- doc.AddItem(Header(2, _("Error")))
- doc.AddItem(Bold(errmsg % args))
diff --git a/src/web/Cgi/subscribe.py b/src/web/Cgi/subscribe.py
deleted file mode 100644
index 0eefd0111..000000000
--- a/src/web/Cgi/subscribe.py
+++ /dev/null
@@ -1,252 +0,0 @@
-# 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/>.
-
-"""Process subscription or roster requests from listinfo form."""
-
-from __future__ import with_statement
-
-import os
-import cgi
-import sys
-import logging
-
-from Mailman import Errors
-from Mailman import MailList
-from Mailman import Message
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.UserDesc import UserDesc
-from Mailman.configuration import config
-from Mailman.htmlformat import *
-
-SLASH = '/'
-ERRORSEP = '\n\n<p>'
-
-# Set up i18n
-_ = i18n._
-i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
-log = logging.getLogger('mailman.error')
-mlog = logging.getLogger('mailman.mischief')
-
-
-
-def main():
- doc = Document()
- doc.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- parts = Utils.GetPathPieces()
- if not parts:
- doc.AddItem(Header(2, _("Error")))
- doc.AddItem(Bold(_('Invalid options to CGI script')))
- print doc.Format()
- return
-
- listname = parts[0].lower()
- try:
- mlist = MailList.MailList(listname, lock=0)
- except Errors.MMListError, e:
- # Avoid cross-site scripting attacks
- safelistname = Utils.websafe(listname)
- doc.AddItem(Header(2, _("Error")))
- doc.AddItem(Bold(_('No such list <em>%(safelistname)s</em>')))
- print doc.Format()
- log.error('No such list "%s": %s\n', listname, e)
- return
-
- # See if the form data has a preferred language set, in which case, use it
- # for the results. If not, use the list's preferred language.
- cgidata = cgi.FieldStorage()
- language = cgidata.getvalue('language')
- if language not in config.languages.enabled_codes:
- language = mlist.preferred_language
- i18n.set_language(language)
- doc.set_language(language)
-
- mlist.Lock()
- try:
- process_form(mlist, doc, cgidata, language)
- mlist.Save()
- finally:
- mlist.Unlock()
-
-
-
-def process_form(mlist, doc, cgidata, lang):
- listowner = mlist.GetOwnerEmail()
- realname = mlist.real_name
- results = []
-
- # The email address being subscribed, required
- email = cgidata.getvalue('email', '')
- if not email:
- results.append(_('You must supply a valid email address.'))
-
- fullname = cgidata.getvalue('fullname', '')
- # Canonicalize the full name
- fullname = Utils.canonstr(fullname, lang)
- # Who was doing the subscribing?
- remote = os.environ.get('REMOTE_HOST',
- os.environ.get('REMOTE_ADDR',
- 'unidentified origin'))
- # Was an attempt made to subscribe the list to itself?
- if email == mlist.GetListEmail():
- mlog.error('Attempt to self subscribe %s: %s', email, remote)
- results.append(_('You may not subscribe a list to itself!'))
- # If the user did not supply a password, generate one for him
- password = cgidata.getvalue('pw')
- confirmed = cgidata.getvalue('pw-conf')
-
- if password is None and confirmed is None:
- password = Utils.MakeRandomPassword()
- elif password is None or confirmed is None:
- results.append(_('If you supply a password, you must confirm it.'))
- elif password <> confirmed:
- results.append(_('Your passwords did not match.'))
-
- # Get the digest option for the subscription.
- digestflag = cgidata.getvalue('digest')
- if digestflag:
- try:
- digest = int(digestflag)
- except ValueError:
- digest = 0
- else:
- digest = mlist.digest_is_default
-
- # Sanity check based on list configuration. BAW: It's actually bogus that
- # the page allows you to set the digest flag if you don't really get the
- # choice. :/
- if not mlist.digestable:
- digest = 0
- elif not mlist.nondigestable:
- digest = 1
-
- if results:
- print_results(mlist, ERRORSEP.join(results), doc, lang)
- return
-
- # If this list has private rosters, we have to be careful about the
- # message that gets printed, otherwise the subscription process can be
- # used to mine for list members. It may be inefficient, but it's still
- # possible, and that kind of defeats the purpose of private rosters.
- # We'll use this string for all successful or unsuccessful subscription
- # results.
- if mlist.private_roster == 0:
- # Public rosters
- privacy_results = ''
- else:
- privacy_results = _("""\
-Your subscription request has been received, and will soon be acted upon.
-Depending on the configuration of this mailing list, your subscription request
-may have to be first confirmed by you via email, or approved by the list
-moderator. If confirmation is required, you will soon get a confirmation
-email which contains further instructions.""")
-
- try:
- userdesc = UserDesc(email, fullname, password, digest, lang)
- mlist.AddMember(userdesc, remote)
- results = ''
- # Check for all the errors that mlist.AddMember can throw options on the
- # web page for this cgi
- except Errors.MembershipIsBanned:
- results = _("""The email address you supplied is banned from this
- mailing list. If you think this restriction is erroneous, please
- contact the list owners at %(listowner)s.""")
- except Errors.InvalidEmailAddress:
- results = _('The email address you supplied is not valid.')
- except Errors.MMSubscribeNeedsConfirmation:
- # Results string depends on whether we have private rosters or not
- if privacy_results:
- results = privacy_results
- else:
- results = _("""\
-Confirmation from your email address is required, to prevent anyone from
-subscribing you without permission. Instructions are being sent to you at
-%(email)s. Please note your subscription will not start until you confirm
-your subscription.""")
- except Errors.MMNeedApproval, x:
- # Results string depends on whether we have private rosters or not
- if privacy_results:
- results = privacy_results
- else:
- # We need to interpolate into x
- x = _(x)
- results = _("""\
-Your subscription request was deferred because %(x)s. Your request has been
-forwarded to the list moderator. You will receive email informing you of the
-moderator's decision when they get to your request.""")
- except Errors.MMAlreadyAMember:
- # Results string depends on whether we have private rosters or not
- if not privacy_results:
- results = _('You are already subscribed.')
- else:
- results = privacy_results
- # This could be a membership probe. For safety, let the user know
- # a probe occurred. BAW: should we inform the list moderator?
- listaddr = mlist.GetListEmail()
- # Set the language for this email message to the member's language.
- mlang = mlist.getMemberLanguage(email)
- with i18n.using_language(mlang):
- msg = Message.UserNotification(
- mlist.getMemberCPAddress(email),
- mlist.GetBouncesEmail(),
- _('Mailman privacy alert'),
- _("""\
-An attempt was made to subscribe your address to the mailing list
-%(listaddr)s. You are already subscribed to this mailing list.
-
-Note that the list membership is not public, so it is possible that a bad
-person was trying to probe the list for its membership. This would be a
-privacy violation if we let them do this, but we didn't.
-
-If you submitted the subscription request and forgot that you were already
-subscribed to the list, then you can ignore this message. If you suspect that
-an attempt is being made to covertly discover whether you are a member of this
-list, and you are worried about your privacy, then feel free to send a message
-to the list administrator at %(listowner)s.
-"""), lang=mlang)
- msg.send(mlist)
- # These shouldn't happen unless someone's tampering with the form
- except Errors.MMCantDigestError:
- results = _('This list does not support digest delivery.')
- except Errors.MMMustDigestError:
- results = _('This list only supports digest delivery.')
- else:
- # Everything's cool. Our return string actually depends on whether
- # this list has private rosters or not
- if privacy_results:
- results = privacy_results
- else:
- results = _("""\
-You have been successfully subscribed to the %(realname)s mailing list.""")
- # Show the results
- print_results(mlist, results, doc, lang)
-
-
-
-def print_results(mlist, results, doc, lang):
- # The bulk of the document will come from the options.html template, which
- # includes its own html armor (head tags, etc.). Suppress the head that
- # Document() derived pages get automatically.
- doc.suppress_head = 1
-
- replacements = mlist.GetStandardReplacements(lang)
- replacements['<mm-results>'] = results
- output = mlist.ParseTags('subscribe.html', replacements, lang)
- doc.AddItem(output)
- print doc.Format()
diff --git a/src/web/Cgi/wsgi_app.py b/src/web/Cgi/wsgi_app.py
deleted file mode 100644
index b22dbf452..000000000
--- a/src/web/Cgi/wsgi_app.py
+++ /dev/null
@@ -1,286 +0,0 @@
-# Copyright (C) 2006-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-import sys
-
-from cStringIO import StringIO
-from email import message_from_string
-from urlparse import urlparse
-
-from Mailman.configuration import config
-
-# XXX Should this be configurable in Defaults.py?
-STEALTH_MODE = False
-MOVED_RESPONSE = '302 Found'
-# Above is for debugging convenience. We should use:
-# MOVED_RESPONSE = '301 Moved Permanently'
-
-
-
-def websafe(s):
- return s
-
-
-SCRIPTS = ['admin', 'admindb', 'confirm', 'create',
- 'edithtml', 'listinfo', 'options', 'private',
- 'rmlist', 'roster', 'subscribe']
-ARCHVIEW = ['private']
-
-SLASH = '/'
-NL2 = '\n\n'
-CRLF2 = '\r\n\r\n'
-
-dotonly = re.compile(r'^\.+$')
-
-
-
-# WSGI to CGI wrapper. Mostly copied from scripts/driver.
-def mailman_app(environ, start_response):
- """Wrapper to *.py CGI commands"""
- global STEALTH_MODE, websafe
- try:
- try:
- if not STEALTH_MODE:
- from Mailman.Utils import websafe
- except:
- STEALTH_MODE = True
- raise
-
- import logging
- log = logging.getLogger('mailman.error')
-
- from Mailman import i18n
- i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
-
- path = environ['PATH_INFO']
- paths = path.split(SLASH)
- # sanity check for paths
- spaths = [ i for i in paths[1:] if i and not dotonly.match(i) ]
- if spaths and spaths != paths[1:]:
- newpath = SLASH + SLASH.join(spaths)
- start_response(MOVED_RESPONSE, [('Location', newpath)])
- return 'Location: ' + newpath
- # find script name
- for script in SCRIPTS:
- if script in spaths:
- # Get script position in spaths and break.
- scrpos = spaths.index(script)
- break
- else:
- # Can't find valid script.
- start_response('404 Not Found', [])
- return '404 Not Found'
- # Compose CGI SCRIPT_NAME and PATH_INFO from WSGI path.
- script_name = SLASH + SLASH.join(spaths[:scrpos+1])
- environ['SCRIPT_NAME'] = script_name
- if len(paths) > scrpos+2:
- path_info = SLASH + SLASH.join(paths[scrpos+2:])
- if script in ARCHVIEW \
- and path_info.count('/') in (1,2) \
- and not paths[-1].split('.')[-1] in ('html', 'txt', 'gz'):
- # Add index.html if /private/listname or
- # /private/listname/YYYYmm is requested.
- newpath = script_name + path_info + '/index.html'
- start_response(MOVED_RESPONSE, [('Location', newpath)])
- return 'Location: ' + newpath
- environ['PATH_INFO'] = path_info
- else:
- environ['PATH_INFO'] = ''
- # Reverse proxy environment.
- if environ.has_key('HTTP_X_FORWARDED_HOST'):
- environ['HTTP_HOST'] = environ['HTTP_X_FORWARDED_HOST']
- if environ.has_key('HTTP_X_FORWARDED_FOR'):
- environ['REMOTE_HOST'] = environ['HTTP_X_FORWARDED_FOR']
- modname = 'Mailman.Cgi.' + script
- # Clear previous cookie before setting new one.
- os.environ['HTTP_COOKIE'] = ''
- for k, v in environ.items():
- os.environ[k] = str(v)
- # Prepare for redirection
- save_stdin = sys.stdin
- # CGI writes its output to sys.stdout, while wsgi app should
- # return (list of) strings.
- save_stdout = sys.stdout
- save_stderr = sys.stderr
- tmpstdout = StringIO()
- tmpstderr = StringIO()
- response = ''
- try:
- try:
- sys.stdin = environ['wsgi.input']
- sys.stdout = tmpstdout
- sys.stderr = tmpstderr
- __import__(modname)
- sys.modules[modname].main()
- response = sys.stdout.getvalue()
- finally:
- sys.stdin = save_stdin
- sys.stdout = save_stdout
- sys.stderr = save_stderr
- except SystemExit:
- sys.stdout.write(tmpstdout.getvalue())
- if response:
- try:
- head, content = response.split(NL2, 1)
- except ValueError:
- head, content = response.split(CRLF2, 1)
- m = message_from_string(head + CRLF2)
- start_response('200 OK', m.items())
- return [content]
- else:
- # TBD: Error Code/Message
- start_response('500 Server Error', [])
- return '500 Internal Server Error'
- except:
- start_response('200 OK', [('Content-Type', 'text/html')])
- retstring = print_traceback(log)
- retstring += print_environment(log)
- return retstring
-
-
-
-# These functions are extracted and modified from scripts/driver.
-#
-# If possible, we print the error to two places. One will always be stdout
-# and the other will be the log file if a log file was created. It is assumed
-# that stdout is an HTML sink.
-def print_traceback(log=None):
- try:
- import traceback
- except ImportError:
- traceback = None
- try:
- from mailman.version import VERSION
- except ImportError:
- VERSION = '&lt;undetermined&gt;'
-
- # Write to the log file first.
- if log:
- outfp = StringIO()
-
- print >> outfp, '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
- print >> outfp, '[----- Mailman Version: %s -----]' % VERSION
- print >> outfp, '[----- Traceback ------]'
- if traceback:
- traceback.print_exc(file=outfp)
- else:
- print >> outfp, '[failed to import module traceback]'
- print >> outfp, '[exc: %s, var: %s]' % sys.exc_info()[0:2]
- # Don't use .exception() since that'll give us the exception twice.
- # IWBNI we could print directly to the log's stream, or treat a log
- # like an output stream.
- log.error('%s', outfp.getvalue())
-
- # return HTML sink.
- htfp = StringIO()
- print >> htfp, """\
-<head><title>Bug in Mailman version %(VERSION)s</title></head>
-<body bgcolor=#ffffff><h2>Bug in Mailman version %(VERSION)s</h2>
-<p><h3>We're sorry, we hit a bug!</h3>
-""" % locals()
- if not STEALTH_MODE:
- print >> htfp, '''<p>If you would like to help us identify the problem,
-please email a copy of this page to the webmaster for this site with
-a description of what happened. Thanks!
-
-<h4>Traceback:</h4><p><pre>'''
- exc_info = sys.exc_info()
- if traceback:
- for line in traceback.format_exception(*exc_info):
- print >> htfp, websafe(line)
- else:
- print >> htfp, '[failed to import module traceback]'
- print >> htfp, '[exc: %s, var: %s]' %\
- [websafe(x) for x in exc_info[0:2]]
- print >> htfp, '\n\n</pre></body>'
- else:
- print >> htfp, '''<p>Please inform the webmaster for this site of this
-problem. Printing of traceback and other system information has been
-explicitly inhibited, but the webmaster can find this information in the
-Mailman error logs.'''
- return htfp.getvalue()
-
-
-
-def print_environment(log=None):
- try:
- import os
- except ImportError:
- os = None
-
- if log:
- outfp = StringIO()
-
- # Write some information about our Python executable to the log file.
- print >> outfp, '[----- Python Information -----]'
- print >> outfp, 'sys.version =', sys.version
- print >> outfp, 'sys.executable =', sys.executable
- print >> outfp, 'sys.prefix =', sys.prefix
- print >> outfp, 'sys.exec_prefix =', sys.exec_prefix
- print >> outfp, 'sys.path =', sys.exec_prefix
- print >> outfp, 'sys.platform =', sys.platform
-
- # Write the same information to the HTML sink.
- htfp = StringIO()
- if not STEALTH_MODE:
- print >> htfp, """\
-<p><hr><h4>Python information:</h4>
-
-<p><table>
-<tr><th>Variable</th><th>Value</th></tr>
-<tr><td><tt>sys.version</tt></td><td> %s </td></tr>
-<tr><td><tt>sys.executable</tt></td><td> %s </td></tr>
-<tr><td><tt>sys.prefix</tt></td><td> %s </td></tr>
-<tr><td><tt>sys.exec_prefix</tt></td><td> %s </td></tr>
-<tr><td><tt>sys.path</tt></td><td> %s </td></tr>
-<tr><td><tt>sys.platform</tt></td><td> %s </td></tr>
-</table>""" % (sys.version, sys.executable, sys.prefix,
- sys.exec_prefix, sys.path, sys.platform)
-
- # Write environment variables to the log file.
- if log:
- print >> outfp, '[----- Environment Variables -----]'
- if os:
- for k, v in os.environ.items():
- print >> outfp, '\t%s: %s' % (k, v)
- else:
- print >> outfp, '[failed to import module os]'
-
- # Write environment variables to the HTML sink.
- if not STEALTH_MODE:
- print >> htfp, """\
-<p><hr><h4>Environment variables:</h4>
-
-<p><table>
-<tr><th>Variable</th><th>Value</th></tr>
-"""
- if os:
- for k, v in os.environ.items():
- print >> htfp, '<tr><td><tt>' + websafe(k) + \
- '</tt></td><td>' + websafe(v) + \
- '</td></tr>'
- print >> htfp, '</table>'
- else:
- print >> htfp, '<p><hr>[failed to import module os]'
-
- # Dump the log output
- if log:
- log.error('%s', outfp.getvalue())
-
- return htfp.getvalue()
diff --git a/src/web/Gui/Archive.py b/src/web/Gui/Archive.py
deleted file mode 100644
index 4090e74e9..000000000
--- a/src/web/Gui/Archive.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-
-
-class Archive(GUIBase):
- def GetConfigCategory(self):
- return 'archive', _('Archiving Options')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'archive':
- return None
- return [
- _("List traffic archival policies."),
-
- ('archive', config.Toggle, (_('No'), _('Yes')), 0,
- _('Archive messages?')),
-
- ('archive_private', config.Radio, (_('public'), _('private')), 0,
- _('Is archive file source for public or private archival?')),
-
- ('archive_volume_frequency', config.Radio,
- (_('Yearly'), _('Monthly'), _('Quarterly'),
- _('Weekly'), _('Daily')),
- 0,
- _('How often should a new archive volume be started?')),
- ]
diff --git a/src/web/Gui/Autoresponse.py b/src/web/Gui/Autoresponse.py
deleted file mode 100644
index 72ef42cad..000000000
--- a/src/web/Gui/Autoresponse.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Administrative GUI for the autoresponder."""
-
-from Mailman import Utils
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-# These are the allowable string substitution variables
-ALLOWEDS = ('listname', 'listurl', 'requestemail', 'adminemail', 'owneremail')
-
-
-
-class Autoresponse(GUIBase):
- def GetConfigCategory(self):
- return 'autoreply', _('Auto-responder')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'autoreply':
- return None
- WIDTH = config.TEXTFIELDWIDTH
-
- return [
- _("""\
-Auto-responder characteristics.<p>
-
-In the text fields below, string interpolation is performed with
-the following key/value substitutions:
-<p><ul>
- <li><b>listname</b> - <em>gets the name of the mailing list</em>
- <li><b>listurl</b> - <em>gets the list's listinfo URL</em>
- <li><b>requestemail</b> - <em>gets the list's -request address</em>
- <li><b>owneremail</b> - <em>gets the list's -owner address</em>
-</ul>
-
-<p>For each text field, you can either enter the text directly into the text
-box, or you can specify a file on your local system to upload as the text."""),
-
- ('autorespond_postings', config.Toggle, (_('No'), _('Yes')), 0,
- _('''Should Mailman send an auto-response to mailing list
- posters?''')),
-
- ('autoresponse_postings_text', config.FileUpload,
- (6, WIDTH), 0,
- _('Auto-response text to send to mailing list posters.')),
-
- ('autorespond_admin', config.Toggle, (_('No'), _('Yes')), 0,
- _('''Should Mailman send an auto-response to emails sent to the
- -owner address?''')),
-
- ('autoresponse_admin_text', config.FileUpload,
- (6, WIDTH), 0,
- _('Auto-response text to send to -owner emails.')),
-
- ('autorespond_requests', config.Radio,
- (_('No'), _('Yes, w/discard'), _('Yes, w/forward')), 0,
- _('''Should Mailman send an auto-response to emails sent to the
- -request address? If you choose yes, decide whether you want
- Mailman to discard the original email, or forward it on to the
- system as a normal mail command.''')),
-
- ('autoresponse_request_text', config.FileUpload,
- (6, WIDTH), 0,
- _('Auto-response text to send to -request emails.')),
-
- ('autoresponse_graceperiod', config.Number, 3, 0,
- _('''Number of days between auto-responses to either the mailing
- list or -request/-owner address from the same poster. Set to
- zero (or negative) for no grace period (i.e. auto-respond to
- every message).''')),
- ]
-
- def _setValue(self, mlist, property, val, doc):
- # Handle these specially because we may need to convert to/from
- # external $-string representation.
- if property in ('autoresponse_postings_text',
- 'autoresponse_admin_text',
- 'autoresponse_request_text'):
- val = self._convertString(mlist, property, ALLOWEDS, val, doc)
- if val is None:
- # There was a problem, so don't set it
- return
- GUIBase._setValue(self, mlist, property, val, doc)
diff --git a/src/web/Gui/Bounce.py b/src/web/Gui/Bounce.py
deleted file mode 100644
index a2f6f887a..000000000
--- a/src/web/Gui/Bounce.py
+++ /dev/null
@@ -1,195 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-
-
-class Bounce(GUIBase):
- def GetConfigCategory(self):
- return 'bounce', _('Bounce processing')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'bounce':
- return None
- return [
- _("""These policies control the automatic bounce processing system
- in Mailman. Here's an overview of how it works.
-
- <p>When a bounce is received, Mailman tries to extract two pieces
- of information from the message: the address of the member the
- message was intended for, and the severity of the problem causing
- the bounce. The severity can be either <em>hard</em> or
- <em>soft</em> meaning either a fatal error occurred, or a
- transient error occurred. When in doubt, a hard severity is used.
-
- <p>If no member address can be extracted from the bounce, then the
- bounce is usually discarded. Otherwise, each member is assigned a
- <em>bounce score</em> and every time we encounter a bounce from
- this member we increment the score. Hard bounces increment by 1
- while soft bounces increment by 0.5. We only increment the bounce
- score once per day, so even if we receive ten hard bounces from a
- member per day, their score will increase by only 1 for that day.
-
- <p>When a member's bounce score is greater than the
- <a href="?VARHELP=bounce/bounce_score_threshold">bounce score
- threshold</a>, the subscription is disabled. Once disabled, the
- member will not receive any postings from the list until their
- membership is explicitly re-enabled (either by the list
- administrator or the user). However, they will receive occasional
- reminders that their membership has been disabled, and these
- reminders will include information about how to re-enable their
- membership.
-
- <p>You can control both the
- <a href="?VARHELP=bounce/bounce_you_are_disabled_warnings">number
- of reminders</a> the member will receive and the
- <a href="?VARHELP=bounce/bounce_you_are_disabled_warnings_interval"
- >frequency</a> with which these reminders are sent.
-
- <p>There is one other important configuration variable; after a
- certain period of time -- during which no bounces from the member
- are received -- the bounce information is
- <a href="?VARHELP=bounce/bounce_info_stale_after">considered
- stale</a> and discarded. Thus by adjusting this value, and the
- score threshold, you can control how quickly bouncing members are
- disabled. You should tune both of these to the frequency and
- traffic volume of your list."""),
-
- _('Bounce detection sensitivity'),
-
- ('bounce_processing', config.Toggle, (_('No'), _('Yes')), 0,
- _('Should Mailman perform automatic bounce processing?'),
- _("""By setting this value to <em>No</em>, you disable all
- automatic bounce processing for this list, however bounce
- messages will still be discarded so that the list administrator
- isn't inundated with them.""")),
-
- ('bounce_score_threshold', config.Number, 5, 0,
- _("""The maximum member bounce score before the member's
- subscription is disabled. This value can be a floating point
- number."""),
- _("""Each subscriber is assigned a bounce score, as a floating
- point number. Whenever Mailman receives a bounce from a list
- member, that member's score is incremented. Hard bounces (fatal
- errors) increase the score by 1, while soft bounces (temporary
- errors) increase the score by 0.5. Only one bounce per day
- counts against a member's score, so even if 10 bounces are
- received for a member on the same day, their score will increase
- by just 1.
-
- This variable describes the upper limit for a member's bounce
- score, above which they are automatically disabled, but not
- removed from the mailing list.""")),
-
- ('bounce_info_stale_after', config.Number, 5, 0,
- _("""The number of days after which a member's bounce information
- is discarded, if no new bounces have been received in the
- interim. This value must be an integer.""")),
-
- ('bounce_you_are_disabled_warnings', config.Number, 5, 0,
- _("""How many <em>Your Membership Is Disabled</em> warnings a
- disabled member should get before their address is removed from
- the mailing list. Set to 0 to immediately remove an address from
- the list once their bounce score exceeds the threshold. This
- value must be an integer.""")),
-
- ('bounce_you_are_disabled_warnings_interval', config.Number, 5, 0,
- _("""The number of days between sending the <em>Your Membership
- Is Disabled</em> warnings. This value must be an integer.""")),
-
- _('Notifications'),
-
- ('bounce_unrecognized_goes_to_list_owner', config.Toggle,
- (_('No'), _('Yes')), 0,
- _('''Should Mailman send you, the list owner, any bounce messages
- that failed to be detected by the bounce processor? <em>Yes</em>
- is recommended.'''),
- _("""While Mailman's bounce detector is fairly robust, it's
- impossible to detect every bounce format in the world. You
- should keep this variable set to <em>Yes</em> for two reasons: 1)
- If this really is a permanent bounce from one of your members,
- you should probably manually remove them from your list, and 2)
- you might want to send the message on to the Mailman developers
- so that this new format can be added to its known set.
-
- <p>If you really can't be bothered, then set this variable to
- <em>No</em> and all non-detected bounces will be discarded
- without further processing.
-
- <p><b>Note:</b> This setting will also affect all messages sent
- to your list's -admin address. This address is deprecated and
- should never be used, but some people may still send mail to this
- address. If this happens, and this variable is set to
- <em>No</em> those messages too will get discarded. You may want
- to set up an
- <a href="?VARHELP=autoreply/autoresponse_admin_text">autoresponse
- message</a> for email to the -owner and -admin address.""")),
-
- ('bounce_notify_owner_on_disable', config.Toggle,
- (_('No'), _('Yes')), 0,
- _("""Should Mailman notify you, the list owner, when bounces
- cause a member's subscription to be disabled?"""),
- _("""By setting this value to <em>No</em>, you turn off
- notification messages that are normally sent to the list owners
- when a member's delivery is disabled due to excessive bounces.
- An attempt to notify the member will always be made.""")),
-
- ('bounce_notify_owner_on_removal', config.Toggle,
- (_('No'), _('Yes')), 0,
- _("""Should Mailman notify you, the list owner, when bounces
- cause a member to be unsubscribed?"""),
- _("""By setting this value to <em>No</em>, you turn off
- notification messages that are normally sent to the list owners
- when a member is unsubscribed due to excessive bounces. An
- attempt to notify the member will always be made.""")),
-
- ]
-
- def _setValue(self, mlist, property, val, doc):
- # Do value conversion from web representation to internal
- # representation.
- try:
- if property == 'bounce_processing':
- val = int(val)
- elif property == 'bounce_score_threshold':
- val = float(val)
- elif property == 'bounce_info_stale_after':
- val = config.days(int(val))
- elif property == 'bounce_you_are_disabled_warnings':
- val = int(val)
- elif property == 'bounce_you_are_disabled_warnings_interval':
- val = config.days(int(val))
- elif property == 'bounce_notify_owner_on_disable':
- val = int(val)
- elif property == 'bounce_notify_owner_on_removal':
- val = int(val)
- except ValueError:
- doc.addError(
- _("""Bad value for <a href="?VARHELP=bounce/%(property)s"
- >%(property)s</a>: %(val)s"""),
- tag = _('Error: '))
- return
- GUIBase._setValue(self, mlist, property, val, doc)
-
- def getValue(self, mlist, kind, varname, params):
- if varname not in ('bounce_info_stale_after',
- 'bounce_you_are_disabled_warnings_interval'):
- return None
- return int(getattr(mlist, varname) / config.days(1))
diff --git a/src/web/Gui/ContentFilter.py b/src/web/Gui/ContentFilter.py
deleted file mode 100644
index 09817f4aa..000000000
--- a/src/web/Gui/ContentFilter.py
+++ /dev/null
@@ -1,199 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""GUI component managing the content filtering options."""
-
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-NL = '\n'
-
-
-
-class ContentFilter(GUIBase):
- def GetConfigCategory(self):
- return 'contentfilter', _('Content&nbsp;filtering')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'contentfilter':
- return None
- WIDTH = config.TEXTFIELDWIDTH
-
- actions = [_('Discard'), _('Reject'), _('Forward to List Owner')]
- if config.OWNERS_CAN_PRESERVE_FILTERED_MESSAGES:
- actions.append(_('Preserve'))
-
- return [
- _("""Policies concerning the content of list traffic.
-
- <p>Content filtering works like this: when a message is
- received by the list and you have enabled content filtering, the
- individual attachments are first compared to the
- <a href="?VARHELP=contentfilter/filter_mime_types">filter
- types</a>. If the attachment type matches an entry in the filter
- types, it is discarded.
-
- <p>Then, if there are <a
- href="?VARHELP=contentfilter/pass_mime_types">pass types</a>
- defined, any attachment type that does <em>not</em> match a
- pass type is also discarded. If there are no pass types defined,
- this check is skipped.
-
- <p>After this initial filtering, any <tt>multipart</tt>
- attachments that are empty are removed. If the outer message is
- left empty after this filtering, then the whole message is
- discarded.
-
- <p> Then, each <tt>multipart/alternative</tt> section will
- be replaced by just the first alternative that is non-empty after
- filtering if
- <a href="?VARHELP=contentfilter/collapse_alternatives"
- >collapse_alternatives</a> is enabled.
-
- <p>Finally, any <tt>text/html</tt> parts that are left in the
- message may be converted to <tt>text/plain</tt> if
- <a href="?VARHELP=contentfilter/convert_html_to_plaintext"
- >convert_html_to_plaintext</a> is enabled and the site is
- configured to allow these conversions."""),
-
- ('filter_content', config.Radio, (_('No'), _('Yes')), 0,
- _("""Should Mailman filter the content of list traffic according
- to the settings below?""")),
-
- ('filter_mime_types', config.Text, (10, WIDTH), 0,
- _("""Remove message attachments that have a matching content
- type."""),
-
- _("""Use this option to remove each message attachment that
- matches one of these content types. Each line should contain a
- string naming a MIME <tt>type/subtype</tt>,
- e.g. <tt>image/gif</tt>. Leave off the subtype to remove all
- parts with a matching major content type, e.g. <tt>image</tt>.
-
- <p>Blank lines are ignored.
-
- <p>See also <a href="?VARHELP=contentfilter/pass_mime_types"
- >pass_mime_types</a> for a content type whitelist.""")),
-
- ('pass_mime_types', config.Text, (10, WIDTH), 0,
- _("""Remove message attachments that don't have a matching
- content type. Leave this field blank to skip this filter
- test."""),
-
- _("""Use this option to remove each message attachment that does
- not have a matching content type. Requirements and formats are
- exactly like <a href="?VARHELP=contentfilter/filter_mime_types"
- >filter_mime_types</a>.
-
- <p><b>Note:</b> if you add entries to this list but don't add
- <tt>multipart</tt> to this list, any messages with attachments
- will be rejected by the pass filter.""")),
-
- ('filter_filename_extensions', config.Text, (10, WIDTH), 0,
- _("""Remove message attachments that have a matching filename
- extension."""),),
-
- ('pass_filename_extensions', config.Text, (10, WIDTH), 0,
- _("""Remove message attachments that don't have a matching
- filename extension. Leave this field blank to skip this filter
- test."""),),
-
- ('collapse_alternatives', config.Radio, (_('No'), _('Yes')), 0,
- _("""Should Mailman collapse multipart/alternative to its
- first part content?""")),
-
- ('convert_html_to_plaintext', config.Radio, (_('No'), _('Yes')), 0,
- _("""Should Mailman convert <tt>text/html</tt> parts to plain
- text? This conversion happens after MIME attachments have been
- stripped.""")),
-
- ('filter_action', config.Radio, tuple(actions), 0,
-
- _("""Action to take when a message matches the content filtering
- rules."""),
-
- _("""One of these actions is take when the message matches one of
- the content filtering rules, meaning, the top-level
- content type matches one of the <a
- href="?VARHELP=contentfilter/filter_mime_types"
- >filter_mime_types</a>, or the top-level content type does
- <strong>not</strong> match one of the
- <a href="?VARHELP=contentfilter/pass_mime_types"
- >pass_mime_types</a>, or if after filtering the subparts of the
- message, the message ends up empty.
-
- <p>Note this action is not taken if after filtering the message
- still contains content. In that case the message is always
- forwarded on to the list membership.
-
- <p>When messages are discarded, a log entry is written
- containing the Message-ID of the discarded message. When
- messages are rejected or forwarded to the list owner, a reason
- for the rejection is included in the bounce message to the
- original author. When messages are preserved, they are saved in
- a special queue directory on disk for the site administrator to
- view (and possibly rescue) but otherwise discarded. This last
- option is only available if enabled by the site
- administrator.""")),
- ]
-
- def _setValue(self, mlist, property, val, doc):
- if property in ('filter_mime_types', 'pass_mime_types'):
- types = []
- for spectype in [s.strip() for s in val.splitlines()]:
- ok = 1
- slashes = spectype.count('/')
- if slashes == 0 and not spectype:
- ok = 0
- elif slashes == 1:
- maintype, subtype = [s.strip().lower()
- for s in spectype.split('/')]
- if not maintype or not subtype:
- ok = 0
- elif slashes > 1:
- ok = 0
- if not ok:
- doc.addError(_('Bad MIME type ignored: %(spectype)s'))
- else:
- types.append(spectype.strip().lower())
- if property == 'filter_mime_types':
- mlist.filter_mime_types = types
- elif property == 'pass_mime_types':
- mlist.pass_mime_types = types
- elif property in ('filter_filename_extensions',
- 'pass_filename_extensions'):
- fexts = []
- for ext in [s.strip() for s in val.splitlines()]:
- fexts.append(ext.lower())
- if property == 'filter_filename_extensions':
- mlist.filter_filename_extensions = fexts
- elif property == 'pass_filename_extensions':
- mlist.pass_filename_extensions = fexts
- else:
- GUIBase._setValue(self, mlist, property, val, doc)
-
- def getValue(self, mlist, kind, property, params):
- if property == 'filter_mime_types':
- return NL.join(mlist.filter_mime_types)
- if property == 'pass_mime_types':
- return NL.join(mlist.pass_mime_types)
- if property == 'filter_filename_extensions':
- return NL.join(mlist.filter_filename_extensions)
- if property == 'pass_filename_extensions':
- return NL.join(mlist.pass_filename_extensions)
- return None
diff --git a/src/web/Gui/Digest.py b/src/web/Gui/Digest.py
deleted file mode 100644
index 821d0684f..000000000
--- a/src/web/Gui/Digest.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# 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/>.
-
-"""Administrative GUI for digest deliveries."""
-
-from Mailman import Utils
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-# Intra-package import
-from Mailman.Gui.GUIBase import GUIBase
-
-# Common b/w nondigest and digest headers & footers. Personalizations may add
-# to this.
-ALLOWEDS = ('real_name', 'list_name', 'host_name', 'web_page_url',
- 'description', 'info', 'cgiext', '_internal_name',
- )
-
-
-
-class Digest(GUIBase):
- def GetConfigCategory(self):
- return 'digest', _('Digest options')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'digest':
- return None
- WIDTH = config.TEXTFIELDWIDTH
-
- info = [
- _("Batched-delivery digest characteristics."),
-
- ('digestable', config.Toggle, (_('No'), _('Yes')), 1,
- _('Can list members choose to receive list traffic '
- 'bunched in digests?')),
-
- ('digest_is_default', config.Radio,
- (_('Regular'), _('Digest')), 0,
- _('Which delivery mode is the default for new users?')),
-
- ('mime_is_default_digest', config.Radio,
- (_('Plain'), _('MIME')), 0,
- _('When receiving digests, which format is default?')),
-
- ('digest_size_threshhold', config.Number, 3, 0,
- _('How big in Kb should a digest be before it gets sent out?')),
- # Should offer a 'set to 0' for no size threshhold.
-
- ('digest_send_periodic', config.Radio, (_('No'), _('Yes')), 1,
- _('Should a digest be dispatched daily when the size threshold '
- "isn't reached?")),
-
- ('digest_header', config.Text, (4, WIDTH), 0,
- _('Header added to every digest'),
- _("Text attached (as an initial message, before the table"
- " of contents) to the top of digests. ")
- + Utils.maketext('headfoot.html', raw=1, mlist=mlist)),
-
- ('digest_footer', config.Text, (4, WIDTH), 0,
- _('Footer added to every digest'),
- _("Text attached (as a final message) to the bottom of digests. ")
- + Utils.maketext('headfoot.html', raw=1, mlist=mlist)),
-
- ('digest_volume_frequency', config.Radio,
- (_('Yearly'), _('Monthly'), _('Quarterly'),
- _('Weekly'), _('Daily')), 0,
- _('How often should a new digest volume be started?'),
- _('''When a new digest volume is started, the volume number is
- incremented and the issue number is reset to 1.''')),
-
- ('_new_volume', config.Toggle, (_('No'), _('Yes')), 0,
- _('Should Mailman start a new digest volume?'),
- _('''Setting this option instructs Mailman to start a new volume
- with the next digest sent out.''')),
-
- ('_send_digest_now', config.Toggle, (_('No'), _('Yes')), 0,
- _('''Should Mailman send the next digest right now, if it is not
- empty?''')),
- ]
-
-## if config.OWNERS_CAN_ENABLE_PERSONALIZATION:
-## info.extend([
-## ('digest_personalize', config.Toggle, (_('No'), _('Yes')), 1,
-
-## _('''Should Mailman personalize each digest delivery?
-## This is often useful for announce-only lists, but <a
-## href="?VARHELP=digest/digest_personalize">read the details</a>
-## section for a discussion of important performance
-## issues.'''),
-
-## _("""Normally, Mailman sends the digest messages to
-## the mail server in batches. This is much more efficent
-## because it reduces the amount of traffic between Mailman and
-## the mail server.
-
-## <p>However, some lists can benefit from a more personalized
-## approach. In this case, Mailman crafts a new message for
-## each member on the digest delivery list. Turning this on
-## adds a few more expansion variables that can be included in
-## the <a href="?VARHELP=digest/digest_header">message header</a>
-## and <a href="?VARHELP=digest/digest_footer">message footer</a>
-## but it may degrade the performance of your site as
-## a whole.
-
-## <p>You need to carefully consider whether the trade-off is
-## worth it, or whether there are other ways to accomplish what
-## you want. You should also carefully monitor your system load
-## to make sure it is acceptable.
-
-## <p>These additional substitution variables will be available
-## for your headers and footers, when this feature is enabled:
-
-## <ul><li><b>user_address</b> - The address of the user,
-## coerced to lower case.
-## <li><b>user_delivered_to</b> - The case-preserved address
-## that the user is subscribed with.
-## <li><b>user_password</b> - The user's password.
-## <li><b>user_name</b> - The user's full name.
-## <li><b>user_optionsurl</b> - The url to the user's option
-## page.
-## """))
-## ])
-
- return info
-
- def _setValue(self, mlist, property, val, doc):
- # Watch for the special, immediate action attributes
- if property == '_new_volume' and val:
- mlist.bump_digest_volume()
- volume = mlist.volume
- number = mlist.next_digest_number
- doc.AddItem(_("""The next digest will be sent as volume
- %(volume)s, number %(number)s"""))
- elif property == '_send_digest_now' and val:
- status = mlist.send_digest_now()
- if status:
- doc.AddItem(_("""A digest has been sent."""))
- else:
- doc.AddItem(_("""There was no digest to send."""))
- else:
- # Everything else...
- if property in ('digest_header', 'digest_footer'):
- val = self._convertString(mlist, property, ALLOWEDS, val, doc)
- if val is None:
- # There was a problem, so don't set it
- return
- GUIBase._setValue(self, mlist, property, val, doc)
diff --git a/src/web/Gui/GUIBase.py b/src/web/Gui/GUIBase.py
deleted file mode 100644
index b2846eb7b..000000000
--- a/src/web/Gui/GUIBase.py
+++ /dev/null
@@ -1,209 +0,0 @@
-# Copyright (C) 2002-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Base class for all web GUI components."""
-
-import re
-
-from Mailman import Defaults
-from Mailman import Errors
-from Mailman import Utils
-from Mailman.i18n import _
-
-NL = '\n'
-BADJOINER = '</code>, <code>'
-
-
-
-class GUIBase:
- # Providing a common interface for GUI component form processing. Most
- # GUI components won't need to override anything, but some may want to
- # override _setValue() to provide some specialized processing for some
- # attributes.
- def _getValidValue(self, mlist, property, wtype, val):
- # Coerce and validate the new value.
- #
- # Radio buttons and boolean toggles both have integral type
- if wtype in (Defaults.Radio, Defaults.Toggle):
- # Let ValueErrors propagate
- return int(val)
- # String and Text widgets both just return their values verbatim
- # but convert into unicode (for 2.2)
- if wtype in (Defaults.String, Defaults.Text):
- return unicode(val, Utils.GetCharSet(mlist.preferred_language))
- # This widget contains a single email address
- if wtype == Defaults.Email:
- # BAW: We must allow blank values otherwise reply_to_address can't
- # be cleared. This is currently the only Defaults.Email type
- # widget in the interface, so watch out if we ever add any new
- # ones.
- if val:
- # Let InvalidEmailAddress propagate.
- Utils.ValidateEmail(val)
- return val
- # These widget types contain lists of email addresses, one per line.
- # The EmailListEx allows each line to contain either an email address
- # or a regular expression
- if wtype in (Defaults.EmailList, Defaults.EmailListEx):
- # BAW: value might already be a list, if this is coming from
- # config_list input. Sigh.
- if isinstance(val, list):
- return val
- addrs = []
- for addr in [s.strip() for s in val.split(NL)]:
- # Discard empty lines
- if not addr:
- continue
- try:
- # This throws an exception if the address is invalid
- Utils.ValidateEmail(addr)
- except Errors.EmailAddressError:
- # See if this is a context that accepts regular
- # expressions, and that the re is legal
- if wtype == Defaults.EmailListEx and addr.startswith('^'):
- try:
- re.compile(addr)
- except re.error:
- raise ValueError
- else:
- raise
- addrs.append(addr)
- return addrs
- # This is a host name, i.e. verbatim
- if wtype == Defaults.Host:
- return val
- # This is a number, either a float or an integer
- if wtype == Defaults.Number:
- num = -1
- try:
- num = int(val)
- except ValueError:
- # Let ValueErrors percolate up
- num = float(val)
- if num < 0:
- return getattr(mlist, property)
- return num
- # This widget is a select box, i.e. verbatim
- if wtype == Defaults.Select:
- return val
- # Checkboxes return a list of the selected items, even if only one is
- # selected.
- if wtype == Defaults.Checkbox:
- if isinstance(val, list):
- return val
- return [val]
- if wtype == Defaults.FileUpload:
- return val
- if wtype == Defaults.Topics:
- return val
- if wtype == Defaults.HeaderFilter:
- return val
- # Should never get here
- assert 0, 'Bad gui widget type: %s' % wtype
-
- def _setValue(self, mlist, property, val, doc):
- # Set the value, or override to take special action on the property
- if not property.startswith('_') and getattr(mlist, property) <> val:
- setattr(mlist, property, val)
-
- def _postValidate(self, mlist, doc):
- # Validate all the attributes for this category
- pass
-
- def _escape(self, property, value):
- value = value.replace('<', '&lt;')
- return value
-
- def handleForm(self, mlist, category, subcat, cgidata, doc):
- for item in self.GetConfigInfo(mlist, category, subcat):
- # Skip descriptions and legacy non-attributes
- if not isinstance(item, tuple) or len(item) < 5:
- continue
- # Unpack the gui item description
- property, wtype, args, deps, desc = item[0:5]
- # BAW: I know this code is a little crufty but I wanted to
- # reproduce the semantics of the original code in admin.py as
- # closely as possible, for now. We can clean it up later.
- #
- # The property may be uploadable...
- uploadprop = property + '_upload'
- if cgidata.has_key(uploadprop) and cgidata[uploadprop].value:
- val = cgidata[uploadprop].value
- elif not cgidata.has_key(property):
- continue
- elif isinstance(cgidata[property], list):
- val = [self._escape(property, x.value)
- for x in cgidata[property]]
- else:
- val = self._escape(property, cgidata[property].value)
- # Coerce the value to the expected type, raising exceptions if the
- # value is invalid.
- try:
- val = self._getValidValue(mlist, property, wtype, val)
- except ValueError:
- doc.addError(_('Invalid value for variable: %(property)s'))
- # This is the parent of InvalidEmailAddress
- except Errors.EmailAddressError:
- doc.addError(
- _('Bad email address for option %(property)s: %(val)s'))
- else:
- # Set the attribute, which will normally delegate to the mlist
- self._setValue(mlist, property, val, doc)
- # Do a final sweep once all the attributes have been set. This is how
- # we can do cross-attribute assertions
- self._postValidate(mlist, doc)
-
- # Convenience method for handling $-string attributes
- def _convertString(self, mlist, property, alloweds, val, doc):
- # Is the list using $-strings?
- dollarp = getattr(mlist, 'use_dollar_strings', 0)
- if dollarp:
- ids = Utils.dollar_identifiers(val)
- else:
- # %-strings
- ids = Utils.percent_identifiers(val)
- # Here's the list of allowable interpolations
- for allowed in alloweds:
- if ids.has_key(allowed):
- del ids[allowed]
- if ids:
- # What's left are not allowed
- badkeys = ids.keys()
- badkeys.sort()
- bad = BADJOINER.join(badkeys)
- doc.addError(_(
- """The following illegal substitution variables were
- found in the <code>%(property)s</code> string:
- <code>%(bad)s</code>
- <p>Your list may not operate properly until you correct this
- problem."""), tag=_('Warning: '))
- return val
- # Now if we're still using %-strings, do a roundtrip conversion and
- # see if the converted value is the same as the new value. If not,
- # then they probably left off a trailing `s'. We'll warn them and use
- # the corrected string.
- if not dollarp:
- fixed = Utils.to_percent(Utils.to_dollar(val))
- if fixed <> val:
- doc.addError(_(
- """Your <code>%(property)s</code> string appeared to
- have some correctable problems in its new value.
- The fixed value will be used instead. Please
- double check that this is what you intended.
- """))
- return fixed
- return val
diff --git a/src/web/Gui/General.py b/src/web/Gui/General.py
deleted file mode 100644
index 27ef354be..000000000
--- a/src/web/Gui/General.py
+++ /dev/null
@@ -1,464 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""MailList mixin class managing the general options."""
-
-import re
-
-from Mailman import Errors
-from Mailman import Utils
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-OPTIONS = ('hide', 'ack', 'notmetoo', 'nodupes')
-
-
-
-class General(GUIBase):
- def GetConfigCategory(self):
- return 'general', _('General Options')
-
- def GetConfigInfo(self, mlist, category, subcat):
- if category <> 'general':
- return None
- WIDTH = config.TEXTFIELDWIDTH
-
- # These are for the default_options checkboxes below.
- bitfields = {'hide' : config.ConcealSubscription,
- 'ack' : config.AcknowledgePosts,
- 'notmetoo' : config.DontReceiveOwnPosts,
- 'nodupes' : config.DontReceiveDuplicates
- }
- bitdescrs = {
- 'hide' : _("Conceal the member's address"),
- 'ack' : _("Acknowledge the member's posting"),
- 'notmetoo' : _("Do not send a copy of a member's own post"),
- 'nodupes' :
- _('Filter out duplicate messages to list members (if possible)'),
- }
-
- optvals = [mlist.new_member_options & bitfields[o] for o in OPTIONS]
- opttext = [bitdescrs[o] for o in OPTIONS]
-
- rtn = [
- _('''Fundamental list characteristics, including descriptive
- info and basic behaviors.'''),
-
- _('General list personality'),
-
- ('real_name', config.String, WIDTH, 0,
- _('The public name of this list (make case-changes only).'),
- _('''The capitalization of this name can be changed to make it
- presentable in polite company as a proper noun, or to make an
- acronym part all upper case, etc. However, the name will be
- advertised as the email address (e.g., in subscribe confirmation
- notices), so it should <em>not</em> be otherwise altered. (Email
- addresses are not case sensitive, but they are sensitive to
- almost everything else :-)''')),
-
- ('owner', config.EmailList, (3, WIDTH), 0,
- _("""The list administrator email addresses. Multiple
- administrator addresses, each on separate line is okay."""),
-
- _('''There are two ownership roles associated with each mailing
- list. The <em>list administrators</em> are the people who have
- ultimate control over all parameters of this mailing list. They
- are able to change any list configuration variable available
- through these administration web pages.
-
- <p>The <em>list moderators</em> have more limited permissions;
- they are not able to change any list configuration variable, but
- they are allowed to tend to pending administration requests,
- including approving or rejecting held subscription requests, and
- disposing of held postings. Of course, the <em>list
- administrators</em> can also tend to pending requests.
-
- <p>In order to split the list ownership duties into
- administrators and moderators, you must
- <a href="passwords">set a separate moderator password</a>,
- and also provide the <a href="?VARHELP=general/moderator">email
- addresses of the list moderators</a>. Note that the field you
- are changing here specifies the list administrators.''')),
-
- ('moderator', config.EmailList, (3, WIDTH), 0,
- _("""The list moderator email addresses. Multiple
- moderator addresses, each on separate line is okay."""),
-
- _('''There are two ownership roles associated with each mailing
- list. The <em>list administrators</em> are the people who have
- ultimate control over all parameters of this mailing list. They
- are able to change any list configuration variable available
- through these administration web pages.
-
- <p>The <em>list moderators</em> have more limited permissions;
- they are not able to change any list configuration variable, but
- they are allowed to tend to pending administration requests,
- including approving or rejecting held subscription requests, and
- disposing of held postings. Of course, the <em>list
- administrators</em> can also tend to pending requests.
-
- <p>In order to split the list ownership duties into
- administrators and moderators, you must
- <a href="passwords">set a separate moderator password</a>,
- and also provide the email addresses of the list moderators in
- this section. Note that the field you are changing here
- specifies the list moderators.''')),
-
- ('description', config.String, WIDTH, 0,
- _('A terse phrase identifying this list.'),
-
- _('''This description is used when the mailing list is listed with
- other mailing lists, or in headers, and so forth. It should
- be as succinct as you can get it, while still identifying what
- the list is.''')),
-
- ('info', config.Text, (7, WIDTH), 0,
- _('''An introductory description - a few paragraphs - about the
- list. It will be included, as html, at the top of the listinfo
- page. Carriage returns will end a paragraph - see the details
- for more info.'''),
- _("""The text will be treated as html <em>except</em> that
- newlines will be translated to &lt;br&gt; - so you can use links,
- preformatted text, etc, but don't put in carriage returns except
- where you mean to separate paragraphs. And review your changes -
- bad html (like some unterminated HTML constructs) can prevent
- display of the entire listinfo page.""")),
-
- ('subject_prefix', config.String, WIDTH, 0,
- _('Prefix for subject line of list postings.'),
- _("""This text will be prepended to subject lines of messages
- posted to the list, to distinguish mailing list messages in in
- mailbox summaries. Brevity is premium here, it's ok to shorten
- long mailing list names to something more concise, as long as it
- still identifies the mailing list.
- You can also add a sequencial number by %%d substitution
- directive. eg.; [listname %%d] -> [listname 123]
- (listname %%05d) -> (listname 00123)
- """)),
-
- ('anonymous_list', config.Radio, (_('No'), _('Yes')), 0,
- _("""Hide the sender of a message, replacing it with the list
- address (Removes From, Sender and Reply-To fields)""")),
-
- _('''<tt>Reply-To:</tt> header munging'''),
-
- ('first_strip_reply_to', config.Radio, (_('No'), _('Yes')), 0,
- _('''Should any existing <tt>Reply-To:</tt> header found in the
- original message be stripped? If so, this will be done
- regardless of whether an explict <tt>Reply-To:</tt> header is
- added by Mailman or not.''')),
-
- ('reply_goes_to_list', config.Radio,
- (_('Poster'), _('This list'), _('Explicit address')), 0,
- _('''Where are replies to list messages directed?
- <tt>Poster</tt> is <em>strongly</em> recommended for most mailing
- lists.'''),
-
- # Details for reply_goes_to_list
- _("""This option controls what Mailman does to the
- <tt>Reply-To:</tt> header in messages flowing through this
- mailing list. When set to <em>Poster</em>, no <tt>Reply-To:</tt>
- header is added by Mailman, although if one is present in the
- original message, it is not stripped. Setting this value to
- either <em>This list</em> or <em>Explicit address</em> causes
- Mailman to insert a specific <tt>Reply-To:</tt> header in all
- messages, overriding the header in the original message if
- necessary (<em>Explicit address</em> inserts the value of <a
- href="?VARHELP=general/reply_to_address">reply_to_address</a>).
-
- <p>There are many reasons not to introduce or override the
- <tt>Reply-To:</tt> header. One is that some posters depend on
- their own <tt>Reply-To:</tt> settings to convey their valid
- return address. Another is that modifying <tt>Reply-To:</tt>
- makes it much more difficult to send private replies. See <a
- href="http://www.unicom.com/pw/reply-to-harmful.html">`Reply-To'
- Munging Considered Harmful</a> for a general discussion of this
- issue. See <a
- href="http://www.metasystema.net/essays/reply-to.mhtml">Reply-To
- Munging Considered Useful</a> for a dissenting opinion.
-
- <p>Some mailing lists have restricted posting privileges, with a
- parallel list devoted to discussions. Examples are `patches' or
- `checkin' lists, where software changes are posted by a revision
- control system, but discussion about the changes occurs on a
- developers mailing list. To support these types of mailing
- lists, select <tt>Explicit address</tt> and set the
- <tt>Reply-To:</tt> address below to point to the parallel
- list.""")),
-
- ('reply_to_address', config.Email, WIDTH, 0,
- _('Explicit <tt>Reply-To:</tt> header.'),
- # Details for reply_to_address
- _("""This is the address set in the <tt>Reply-To:</tt> header
- when the <a
- href="?VARHELP=general/reply_goes_to_list">reply_goes_to_list</a>
- option is set to <em>Explicit address</em>.
-
- <p>There are many reasons not to introduce or override the
- <tt>Reply-To:</tt> header. One is that some posters depend on
- their own <tt>Reply-To:</tt> settings to convey their valid
- return address. Another is that modifying <tt>Reply-To:</tt>
- makes it much more difficult to send private replies. See <a
- href="http://www.unicom.com/pw/reply-to-harmful.html">`Reply-To'
- Munging Considered Harmful</a> for a general discussion of this
- issue. See <a
- href="http://www.metasystema.net/essays/reply-to.mhtml">Reply-To
- Munging Considered Useful</a> for a dissenting opinion.
-
- <p>Some mailing lists have restricted posting privileges, with a
- parallel list devoted to discussions. Examples are `patches' or
- `checkin' lists, where software changes are posted by a revision
- control system, but discussion about the changes occurs on a
- developers mailing list. To support these types of mailing
- lists, specify the explicit <tt>Reply-To:</tt> address here. You
- must also specify <tt>Explicit address</tt> in the
- <tt>reply_goes_to_list</tt>
- variable.
-
- <p>Note that if the original message contains a
- <tt>Reply-To:</tt> header, it will not be changed.""")),
-
- _('Umbrella list settings'),
-
- ('umbrella_list', config.Radio, (_('No'), _('Yes')), 0,
- _('''Send password reminders to, eg, "-owner" address instead of
- directly to user.'''),
-
- _("""Set this to yes when this list is intended to cascade only
- to other mailing lists. When set, meta notices like
- confirmations and password reminders will be directed to an
- address derived from the member\'s address - it will have the
- value of "umbrella_member_suffix" appended to the member's
- account name.""")),
-
- ('umbrella_member_suffix', config.String, WIDTH, 0,
- _('''Suffix for use when this list is an umbrella for other
- lists, according to setting of previous "umbrella_list"
- setting.'''),
-
- _("""When "umbrella_list" is set to indicate that this list has
- other mailing lists as members, then administrative notices like
- confirmations and password reminders need to not be sent to the
- member list addresses, but rather to the owner of those member
- lists. In that case, the value of this setting is appended to
- the member's account name for such notices. `-owner' is the
- typical choice. This setting has no effect when "umbrella_list"
- is "No".""")),
-
- _('Notifications'),
-
- ('send_reminders', config.Radio, (_('No'), _('Yes')), 0,
- _('''Send monthly password reminders?'''),
-
- _('''Turn this on if you want password reminders to be sent once
- per month to your members. Note that members may disable their
- own individual password reminders.''')),
-
- ('welcome_msg', config.Text, (4, WIDTH), 0,
- _('''List-specific text prepended to new-subscriber welcome
- message'''),
-
- _("""This value, if any, will be added to the front of the
- new-subscriber welcome message. The rest of the welcome message
- already describes the important addresses and URLs for the
- mailing list, so you don't need to include any of that kind of
- stuff here. This should just contain mission-specific kinds of
- things, like etiquette policies or team orientation, or that kind
- of thing.
-
- <p>Note that this text will be wrapped, according to the
- following rules:
- <ul><li>Each paragraph is filled so that no line is longer than
- 70 characters.
- <li>Any line that begins with whitespace is not filled.
- <li>A blank line separates paragraphs.
- </ul>""")),
-
- ('send_welcome_msg', config.Radio, (_('No'), _('Yes')), 0,
- _('Send welcome message to newly subscribed members?'),
- _("""Turn this off only if you plan on subscribing people manually
- and don't want them to know that you did so. This option is most
- useful for transparently migrating lists from some other mailing
- list manager to Mailman.""")),
-
- ('goodbye_msg', config.Text, (4, WIDTH), 0,
- _('''Text sent to people leaving the list. If empty, no special
- text will be added to the unsubscribe message.''')),
-
- ('send_goodbye_msg', config.Radio, (_('No'), _('Yes')), 0,
- _('Send goodbye message to members when they are unsubscribed?')),
-
- ('admin_immed_notify', config.Radio, (_('No'), _('Yes')), 0,
- _('''Should the list moderators get immediate notice of new
- requests, as well as daily notices about collected ones?'''),
-
- _('''List moderators (and list administrators) are sent daily
- reminders of requests pending approval, like subscriptions to a
- moderated list, or postings that are being held for one reason or
- another. Setting this option causes notices to be sent
- immediately on the arrival of new requests as well.''')),
-
- ('admin_notify_mchanges', config.Radio, (_('No'), _('Yes')), 0,
- _('''Should administrator get notices of subscribes and
- unsubscribes?''')),
-
- ('respond_to_post_requests', config.Radio,
- (_('No'), _('Yes')), 0,
- _('Send mail to poster when their posting is held for approval?')
- ),
-
- _('Additional settings'),
-
- ('emergency', config.Toggle, (_('No'), _('Yes')), 0,
- _('Emergency moderation of all list traffic.'),
- _("""When this option is enabled, all list traffic is emergency
- moderated, i.e. held for moderation. Turn this option on when
- your list is experiencing a flamewar and you want a cooling off
- period.""")),
-
- ('new_member_options', config.Checkbox,
- (opttext, optvals, 0, OPTIONS),
- # The description for new_member_options includes a kludge where
- # we add a hidden field so that even when all the checkboxes are
- # deselected, the form data will still have a new_member_options
- # key (it will always be a list). Otherwise, we'd never be able
- # to tell if all were deselected!
- 0, _('''Default options for new members joining this list.<input
- type="hidden" name="new_member_options" value="ignore">'''),
-
- _("""When a new member is subscribed to this list, their initial
- set of options is taken from the this variable's setting.""")),
-
- ('administrivia', config.Radio, (_('No'), _('Yes')), 0,
- _('''(Administrivia filter) Check postings and intercept ones
- that seem to be administrative requests?'''),
-
- _("""Administrivia tests will check postings to see whether it's
- really meant as an administrative request (like subscribe,
- unsubscribe, etc), and will add it to the the administrative
- requests queue, notifying the administrator of the new request,
- in the process.""")),
-
- ('max_message_size', config.Number, 7, 0,
- _('''Maximum length in kilobytes (KB) of a message body. Use 0
- for no limit.''')),
-
- ('host_name', config.Host, WIDTH, 0,
- _('Host name this list prefers for email.'),
-
- _("""The "host_name" is the preferred name for email to
- mailman-related addresses on this host, and generally should be
- the mail host's exchanger address, if any. This setting can be
- useful for selecting among alternative names of a host that has
- multiple addresses.""")),
-
- ]
-
- if config.ALLOW_RFC2369_OVERRIDES:
- rtn.append(
- ('include_rfc2369_headers', config.Radio,
- (_('No'), _('Yes')), 0,
- _("""Should messages from this mailing list include the
- <a href="http://www.faqs.org/rfcs/rfc2369.html">RFC 2369</a>
- (i.e. <tt>List-*</tt>) headers? <em>Yes</em> is highly
- recommended."""),
-
- _("""RFC 2369 defines a set of List-* headers that are
- normally added to every message sent to the list membership.
- These greatly aid end-users who are using standards compliant
- mail readers. They should normally always be enabled.
-
- <p>However, not all mail readers are standards compliant yet,
- and if you have a large number of members who are using
- non-compliant mail readers, they may be annoyed at these
- headers. You should first try to educate your members as to
- why these headers exist, and how to hide them in their mail
- clients. As a last resort you can disable these headers, but
- this is not recommended (and in fact, your ability to disable
- these headers may eventually go away)."""))
- )
- # Suppression of List-Post: headers
- rtn.append(
- ('include_list_post_header', config.Radio,
- (_('No'), _('Yes')), 0,
- _('Should postings include the <tt>List-Post:</tt> header?'),
- _("""The <tt>List-Post:</tt> header is one of the headers
- recommended by
- <a href="http://www.faqs.org/rfcs/rfc2369.html">RFC 2369</a>.
- However for some <em>announce-only</em> mailing lists, only a
- very select group of people are allowed to post to the list; the
- general membership is usually not allowed to post. For lists of
- this nature, the <tt>List-Post:</tt> header is misleading.
- Select <em>No</em> to disable the inclusion of this header. (This
- does not affect the inclusion of the other <tt>List-*:</tt>
- headers.)"""))
- )
-
- # Discard held messages after this number of days
- rtn.append(
- ('max_days_to_hold', config.Number, 7, 0,
- _("""Discard held messages older than this number of days.
- Use 0 for no automatic discarding."""))
- )
-
- return rtn
-
- def _setValue(self, mlist, property, val, doc):
- if property == 'real_name' and \
- val.lower() <> mlist.internal_name().lower():
- # These values can't differ by other than case
- doc.addError(_("""<b>real_name</b> attribute not
- changed! It must differ from the list's name by case
- only."""))
- elif property == 'new_member_options':
- newopts = 0
- for opt in OPTIONS:
- bitfield = config.OPTINFO[opt]
- if opt in val:
- newopts |= bitfield
- mlist.new_member_options = newopts
- elif property == 'subject_prefix':
- # Convert any html entities to Unicode
- mlist.subject_prefix = Utils.canonstr(
- val, mlist.preferred_language)
- else:
- GUIBase._setValue(self, mlist, property, val, doc)
-
- def _escape(self, property, value):
- # The 'info' property allows HTML, but lets sanitize it to avoid XSS
- # exploits. Everything else should be fully escaped.
- if property <> 'info':
- return GUIBase._escape(self, property, value)
- # Sanitize <script> and </script> tags but nothing else. Not the best
- # solution, but expedient.
- return re.sub(r'<([/]?script.*?)>', r'&lt;\1&gt;', value)
-
- def _postValidate(self, mlist, doc):
- if not mlist.reply_to_address.strip() and \
- mlist.reply_goes_to_list == 2:
- # You can't go to an explicit address that is blank
- doc.addError(_("""You cannot add a Reply-To: to an explicit
- address if that address is blank. Resetting these values."""))
- mlist.reply_to_address = ''
- mlist.reply_goes_to_list = 0
-
- def getValue(self, mlist, kind, varname, params):
- if varname <> 'subject_prefix':
- return None
- # The subject_prefix may be Unicode
- return Utils.uncanonstr(mlist.subject_prefix, mlist.preferred_language)
diff --git a/src/web/Gui/Language.py b/src/web/Gui/Language.py
deleted file mode 100644
index 05824be5e..000000000
--- a/src/web/Gui/Language.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""MailList mixin class managing the language options."""
-
-import codecs
-
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-
-_ = i18n._
-
-
-
-class Language(GUIBase):
- def GetConfigCategory(self):
- return 'language', _('Language&nbsp;options')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'language':
- return None
- # Set things up for the language choices
- langs = mlist.language_codes
- langnames = [_(description) for description in config.enabled_names]
- try:
- langi = langs.index(mlist.preferred_language)
- except ValueError:
- # Someone must have deleted the list's preferred language. Could
- # be other trouble lurking!
- langi = 0
- # Only allow the admin to choose a language if the system has a
- # charset for it. I think this is the best way to test for that.
- def checkcodec(charset):
- try:
- codecs.lookup(charset)
- return 1
- except LookupError:
- return 0
-
- all = sorted(code for code in config.languages.enabled_codes
- if checkcodec(Utils.GetCharSet(code)))
- checked = [L in langs for L in all]
- allnames = [_(config.languages.get_description(code)) for code in all]
- return [
- _('Natural language (internationalization) options.'),
-
- ('preferred_language', config.Select,
- (langs, langnames, langi),
- 0,
- _('Default language for this list.'),
- _('''This is the default natural language for this mailing list.
- If <a href="?VARHELP=language/available_languages">more than one
- language</a> is supported then users will be able to select their
- own preferences for when they interact with the list. All other
- interactions will be conducted in the default language. This
- applies to both web-based and email-based messages, but not to
- email posted by list members.''')),
-
- ('available_languages', config.Checkbox,
- (allnames, checked, 0, all), 0,
- _('Languages supported by this list.'),
-
- _('''These are all the natural languages supported by this list.
- Note that the
- <a href="?VARHELP=language/preferred_language">default
- language</a> must be included.''')),
-
- ('encode_ascii_prefixes', config.Radio,
- (_('Never'), _('Always'), _('As needed')), 0,
- _("""Encode the
- <a href="?VARHELP=general/subject_prefix">subject
- prefix</a> even when it consists of only ASCII characters?"""),
-
- _("""If your mailing list's default language uses a non-ASCII
- character set and the prefix contains non-ASCII characters, the
- prefix will always be encoded according to the relevant
- standards. However, if your prefix contains only ASCII
- characters, you may want to set this option to <em>Never</em> to
- disable prefix encoding. This can make the subject headers
- slightly more readable for users with mail readers that don't
- properly handle non-ASCII encodings.
-
- <p>Note however, that if your mailing list receives both encoded
- and unencoded subject headers, you might want to choose <em>As
- needed</em>. Using this setting, Mailman will not encode ASCII
- prefixes when the rest of the header contains only ASCII
- characters, but if the original header contains non-ASCII
- characters, it will encode the prefix. This avoids an ambiguity
- in the standards which could cause some mail readers to display
- extra, or missing spaces between the prefix and the original
- header.""")),
-
- ]
-
- def _setValue(self, mlist, prop, val, doc):
- # If we're changing the list's preferred language, change the I18N
- # context as well
- if prop == 'preferred_language':
- i18n.set_language(val)
- doc.set_language(val)
- # Language codes must be wrapped
- if prop == 'available_languages':
- mlist.set_languages(*val)
- else:
- GUIBase._setValue(self, mlist, prop, val, doc)
-
- def getValue(self, mlist, kind, varname, params):
- if varname == 'available_languages':
- # Unwrap Language instances, to return just the code
- return [language.code for language in mlist.available_languages]
- # Returning None tells the infrastructure to use getattr
- return None
diff --git a/src/web/Gui/Membership.py b/src/web/Gui/Membership.py
deleted file mode 100644
index bbfdb438b..000000000
--- a/src/web/Gui/Membership.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""MailList mixin class managing the membership pseudo-options."""
-
-from Mailman.i18n import _
-
-
-
-class Membership:
- def GetConfigCategory(self):
- return 'members', _('Membership&nbsp;Management...')
-
- def GetConfigSubCategories(self, category):
- if category == 'members':
- return [('list', _('Membership&nbsp;List')),
- ('add', _('Mass&nbsp;Subscription')),
- ('remove', _('Mass&nbsp;Removal')),
- ]
- return None
diff --git a/src/web/Gui/NonDigest.py b/src/web/Gui/NonDigest.py
deleted file mode 100644
index 92fb768ad..000000000
--- a/src/web/Gui/NonDigest.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""GUI component for managing the non-digest delivery options."""
-
-from Mailman import Utils
-from Mailman import Defaults
-from Mailman.i18n import _
-from Mailman.configuration import config
-from Mailman.Gui.GUIBase import GUIBase
-
-from Mailman.Gui.Digest import ALLOWEDS
-PERSONALIZED_ALLOWEDS = ('user_address', 'user_delivered_to', 'user_password',
- 'user_name', 'user_optionsurl',
- )
-
-
-
-class NonDigest(GUIBase):
- def GetConfigCategory(self):
- return 'nondigest', _('Non-digest&nbsp;options')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'nondigest':
- return None
- WIDTH = config.TEXTFIELDWIDTH
-
- info = [
- _("Policies concerning immediately delivered list traffic."),
-
- ('nondigestable', Defaults.Toggle, (_('No'), _('Yes')), 1,
- _("""Can subscribers choose to receive mail immediately, rather
- than in batched digests?""")),
- ]
-
- if config.OWNERS_CAN_ENABLE_PERSONALIZATION:
- info.extend([
- ('personalize', Defaults.Radio,
- (_('No'), _('Yes'), _('Full Personalization')), 1,
-
- _('''Should Mailman personalize each non-digest delivery?
- This is often useful for announce-only lists, but <a
- href="?VARHELP=nondigest/personalize">read the details</a>
- section for a discussion of important performance
- issues.'''),
-
- _("""Normally, Mailman sends the regular delivery messages to
- the mail server in batches. This is much more efficent
- because it reduces the amount of traffic between Mailman and
- the mail server.
-
- <p>However, some lists can benefit from a more personalized
- approach. In this case, Mailman crafts a new message for
- each member on the regular delivery list. Turning this
- feature on may degrade the performance of your site, so you
- need to carefully consider whether the trade-off is worth it,
- or whether there are other ways to accomplish what you want.
- You should also carefully monitor your system load to make
- sure it is acceptable.
-
- <p>Select <em>No</em> to disable personalization and send
- messages to the members in batches. Select <em>Yes</em> to
- personalize deliveries and allow additional substitution
- variables in message headers and footers (see below). In
- addition, by selecting <em>Full Personalization</em>, the
- <code>To</code> header of posted messages will be modified to
- include the member's address instead of the list's posting
- address.
-
- <p>When personalization is enabled, a few more expansion
- variables that can be included in the <a
- href="?VARHELP=nondigest/msg_header">message header</a> and
- <a href="?VARHELP=nondigest/msg_footer">message footer</a>.
-
- <p>These additional substitution variables will be available
- for your headers and footers, when this feature is enabled:
-
- <ul><li><b>user_address</b> - The address of the user,
- coerced to lower case.
- <li><b>user_delivered_to</b> - The case-preserved address
- that the user is subscribed with.
- <li><b>user_password</b> - The user's password.
- <li><b>user_name</b> - The user's full name.
- <li><b>user_optionsurl</b> - The url to the user's option
- page.
- </ul>
- """))
- ])
- # BAW: for very dumb reasons, we want the `personalize' attribute to
- # show up before the msg_header and msg_footer attrs, otherwise we'll
- # get a bogus warning if the header/footer contains a personalization
- # substitution variable, and we're transitioning from no
- # personalization to personalization enabled.
- headfoot = Utils.maketext('headfoot.html', mlist=mlist, raw=1)
- if config.OWNERS_CAN_ENABLE_PERSONALIZATION:
- extra = _("""\
-When <a href="?VARHELP=nondigest/personalize">personalization</a> is enabled
-for this list, additional substitution variables are allowed in your headers
-and footers:
-
-<ul><li><b>user_address</b> - The address of the user,
- coerced to lower case.
- <li><b>user_delivered_to</b> - The case-preserved address
- that the user is subscribed with.
- <li><b>user_password</b> - The user's password.
- <li><b>user_name</b> - The user's full name.
- <li><b>user_optionsurl</b> - The url to the user's option
- page.
-</ul>
-""")
- else:
- extra = ''
-
- info.extend([('msg_header', Defaults.Text, (10, WIDTH), 0,
- _('Header added to mail sent to regular list members'),
- _('''Text prepended to the top of every immediately-delivery
- message. ''') + headfoot + extra),
-
- ('msg_footer', Defaults.Text, (10, WIDTH), 0,
- _('Footer added to mail sent to regular list members'),
- _('''Text appended to the bottom of every immediately-delivery
- message. ''') + headfoot + extra),
- ])
-
- info.extend([
- ('scrub_nondigest', Defaults.Toggle, (_('No'), _('Yes')), 0,
- _('Scrub attachments of regular delivery message?'),
- _('''When you scrub attachments, they are stored in archive
- area and links are made in the message so that the member can
- access via web browser. If you want the attachments totally
- disappear, you can use content filter options.''')),
- ])
- return info
-
- def _setValue(self, mlist, property, val, doc):
- alloweds = list(ALLOWEDS)
- if mlist.personalize:
- alloweds.extend(PERSONALIZED_ALLOWEDS)
- if property in ('msg_header', 'msg_footer'):
- val = self._convertString(mlist, property, alloweds, val, doc)
- if val is None:
- # There was a problem, so don't set it
- return
- GUIBase._setValue(self, mlist, property, val, doc)
diff --git a/src/web/Gui/Passwords.py b/src/web/Gui/Passwords.py
deleted file mode 100644
index b2fea0fb5..000000000
--- a/src/web/Gui/Passwords.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""MailList mixin class managing the password pseudo-options."""
-
-from Mailman.i18n import _
-from Mailman.Gui.GUIBase import GUIBase
-
-
-
-class Passwords(GUIBase):
- def GetConfigCategory(self):
- return 'passwords', _('Passwords')
-
- def handleForm(self, mlist, category, subcat, cgidata, doc):
- # Nothing more needs to be done
- pass
diff --git a/src/web/Gui/Privacy.py b/src/web/Gui/Privacy.py
deleted file mode 100644
index 8d60f9203..000000000
--- a/src/web/Gui/Privacy.py
+++ /dev/null
@@ -1,537 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""MailList mixin class managing the privacy options."""
-
-import re
-
-from Mailman import Utils
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-
-
-class Privacy(GUIBase):
- def GetConfigCategory(self):
- return 'privacy', _('Privacy options...')
-
- def GetConfigSubCategories(self, category):
- if category == 'privacy':
- return [('subscribing', _('Subscription&nbsp;rules')),
- ('sender', _('Sender&nbsp;filters')),
- ('recipient', _('Recipient&nbsp;filters')),
- ('spam', _('Spam&nbsp;filters')),
- ]
- return None
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'privacy':
- return None
- # Pre-calculate some stuff. Technically, we shouldn't do the
- # sub_cfentry calculation here, but it's too ugly to indent it any
- # further, and besides, that'll mess up i18n catalogs.
- WIDTH = config.TEXTFIELDWIDTH
- if config.ALLOW_OPEN_SUBSCRIBE:
- sub_cfentry = ('subscribe_policy', config.Radio,
- # choices
- (_('None'),
- _('Confirm'),
- _('Require approval'),
- _('Confirm and approve')),
- 0,
- _('What steps are required for subscription?<br>'),
- _('''None - no verification steps (<em>Not
- Recommended </em>)<br>
- Confirm (*) - email confirmation step required <br>
- Require approval - require list administrator
- Approval for subscriptions <br>
- Confirm and approve - both confirm and approve
-
- <p>(*) when someone requests a subscription,
- Mailman sends them a notice with a unique
- subscription request number that they must reply to
- in order to subscribe.<br>
-
- This prevents mischievous (or malicious) people
- from creating subscriptions for others without
- their consent.'''))
- else:
- sub_cfentry = ('subscribe_policy', config.Radio,
- # choices
- (_('Confirm'),
- _('Require approval'),
- _('Confirm and approve')),
- 1,
- _('What steps are required for subscription?<br>'),
- _('''Confirm (*) - email confirmation required <br>
- Require approval - require list administrator
- approval for subscriptions <br>
- Confirm and approve - both confirm and approve
-
- <p>(*) when someone requests a subscription,
- Mailman sends them a notice with a unique
- subscription request number that they must reply to
- in order to subscribe.<br> This prevents
- mischievous (or malicious) people from creating
- subscriptions for others without their consent.'''))
-
- # some helpful values
- admin = mlist.GetScriptURL('admin')
-
- subscribing_rtn = [
- _("""This section allows you to configure subscription and
- membership exposure policy. You can also control whether this
- list is public or not. See also the
- <a href="%(admin)s/archive">Archival Options</a> section for
- separate archive-related privacy settings."""),
-
- _('Subscribing'),
- ('advertised', config.Radio, (_('No'), _('Yes')), 0,
- _('''Advertise this list when people ask what lists are on this
- machine?''')),
-
- sub_cfentry,
-
- ('subscribe_auto_approval', config.EmailListEx, (10, WIDTH), 1,
- _("""List of addresses (or regexps) whose subscriptions do not
- require approval."""),
-
- _("""When subscription requires approval, addresses in this list
- are allowed to subscribe without administrator approval. Add
- addresses one per line. You may begin a line with a ^ character
- to designate a (case insensitive) regular expression match.""")),
-
- ('unsubscribe_policy', config.Radio, (_('No'), _('Yes')), 0,
- _("""Is the list moderator's approval required for unsubscription
- requests? (<em>No</em> is recommended)"""),
-
- _("""When members want to leave a list, they will make an
- unsubscription request, either via the web or via email.
- Normally it is best for you to allow open unsubscriptions so that
- users can easily remove themselves from mailing lists (they get
- really upset if they can't get off lists!).
-
- <p>For some lists though, you may want to impose moderator
- approval before an unsubscription request is processed. Examples
- of such lists include a corporate mailing list that all employees
- are required to be members of.""")),
-
- _('Ban list'),
- ('ban_list', config.EmailListEx, (10, WIDTH), 1,
- _("""List of addresses which are banned from membership in this
- mailing list."""),
-
- _("""Addresses in this list are banned outright from subscribing
- to this mailing list, with no further moderation required. Add
- addresses one per line; start the line with a ^ character to
- designate a regular expression match.""")),
-
- _("Membership exposure"),
- ('private_roster', config.Radio,
- (_('Anyone'), _('List members'), _('List admin only')), 0,
- _('Who can view subscription list?'),
-
- _('''When set, the list of subscribers is protected by member or
- admin password authentication.''')),
-
- ('obscure_addresses', config.Radio, (_('No'), _('Yes')), 0,
- _("""Show member addresses so they're not directly recognizable
- as email addresses?"""),
- _("""Setting this option causes member email addresses to be
- transformed when they are presented on list web pages (both in
- text and as links), so they're not trivially recognizable as
- email addresses. The intention is to prevent the addresses
- from being snarfed up by automated web scanners for use by
- spammers.""")),
- ]
-
- adminurl = mlist.GetScriptURL('admin')
- sender_rtn = [
- _("""When a message is posted to the list, a series of
- moderation steps are take to decide whether the a moderator must
- first approve the message or not. This section contains the
- controls for moderation of both member and non-member postings.
-
- <p>Member postings are held for moderation if their
- <b>moderation flag</b> is turned on. You can control whether
- member postings are moderated by default or not.
-
- <p>Non-member postings can be automatically
- <a href="?VARHELP=privacy/sender/accept_these_nonmembers"
- >accepted</a>,
- <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held for
- moderation</a>,
- <a href="?VARHELP=privacy/sender/reject_these_nonmembers"
- >rejected</a> (bounced), or
- <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
- >discarded</a>,
- either individually or as a group. Any
- posting from a non-member who is not explicitly accepted,
- rejected, or discarded, will have their posting filtered by the
- <a href="?VARHELP=privacy/sender/generic_nonmember_action">general
- non-member rules</a>.
-
- <p>In the text boxes below, add one address per line; start the
- line with a ^ character to designate a <a href=
- "http://www.python.org/doc/current/lib/module-re.html"
- >Python regular expression</a>. When entering backslashes, do so
- as if you were using Python raw strings (i.e. you generally just
- use a single backslash).
-
- <p>Note that non-regexp matches are always done first."""),
-
- _('Member filters'),
-
- ('default_member_moderation', config.Radio, (_('No'), _('Yes')),
- 0, _('By default, should new list member postings be moderated?'),
-
- _("""Each list member has a <em>moderation flag</em> which says
- whether messages from the list member can be posted directly to
- the list, or must first be approved by the list moderator. When
- the moderation flag is turned on, list member postings must be
- approved first. You, the list administrator can decide whether a
- specific individual's postings will be moderated or not.
-
- <p>When a new member is subscribed, their initial moderation flag
- takes its value from this option. Turn this option off to accept
- member postings by default. Turn this option on to, by default,
- moderate member postings first. You can always manually set an
- individual member's moderation bit by using the
- <a href="%(adminurl)s/members">membership management
- screens</a>.""")),
-
- ('member_moderation_action', config.Radio,
- (_('Hold'), _('Reject'), _('Discard')), 0,
- _("""Action to take when a moderated member posts to the
- list."""),
- _("""<ul><li><b>Hold</b> -- this holds the message for approval
- by the list moderators.
-
- <p><li><b>Reject</b> -- this automatically rejects the message by
- sending a bounce notice to the post's author. The text of the
- bounce notice can be <a
- href="?VARHELP=privacy/sender/member_moderation_notice"
- >configured by you</a>.
-
- <p><li><b>Discard</b> -- this simply discards the message, with
- no notice sent to the post's author.
- </ul>""")),
-
- ('member_moderation_notice', config.Text, (10, WIDTH), 1,
- _("""Text to include in any
- <a href="?VARHELP/privacy/sender/member_moderation_action"
- >rejection notice</a> to
- be sent to moderated members who post to this list.""")),
-
- _('Non-member filters'),
-
- ('accept_these_nonmembers', config.EmailListEx, (10, WIDTH), 1,
- _("""List of non-member addresses whose postings should be
- automatically accepted."""),
-
- _("""Postings from any of these non-members will be automatically
- accepted with no further moderation applied. Add member
- addresses one per line; start the line with a ^ character to
- designate a regular expression match.""")),
-
- ('hold_these_nonmembers', config.EmailListEx, (10, WIDTH), 1,
- _("""List of non-member addresses whose postings will be
- immediately held for moderation."""),
-
- _("""Postings from any of these non-members will be immediately
- and automatically held for moderation by the list moderators.
- The sender will receive a notification message which will allow
- them to cancel their held message. Add member addresses one per
- line; start the line with a ^ character to designate a regular
- expression match.""")),
-
- ('reject_these_nonmembers', config.EmailListEx, (10, WIDTH), 1,
- _("""List of non-member addresses whose postings will be
- automatically rejected."""),
-
- _("""Postings from any of these non-members will be automatically
- rejected. In other words, their messages will be bounced back to
- the sender with a notification of automatic rejection. This
- option is not appropriate for known spam senders; their messages
- should be
- <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
- >automatically discarded</a>.
-
- <p>Add member addresses one per line; start the line with a ^
- character to designate a regular expression match.""")),
-
- ('discard_these_nonmembers', config.EmailListEx, (10, WIDTH), 1,
- _("""List of non-member addresses whose postings will be
- automatically discarded."""),
-
- _("""Postings from any of these non-members will be automatically
- discarded. That is, the message will be thrown away with no
- further processing or notification. The sender will not receive
- a notification or a bounce, however the list moderators can
- optionally <a href="?VARHELP=privacy/sender/forward_auto_discards"
- >receive copies of auto-discarded messages.</a>.
-
- <p>Add member addresses one per line; start the line with a ^
- character to designate a regular expression match.""")),
-
- ('generic_nonmember_action', config.Radio,
- (_('Accept'), _('Hold'), _('Reject'), _('Discard')), 0,
- _("""Action to take for postings from non-members for which no
- explicit action is defined."""),
-
- _("""When a post from a non-member is received, the message's
- sender is matched against the list of explicitly
- <a href="?VARHELP=privacy/sender/accept_these_nonmembers"
- >accepted</a>,
- <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held</a>,
- <a href="?VARHELP=privacy/sender/reject_these_nonmembers"
- >rejected</a> (bounced), and
- <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
- >discarded</a> addresses. If no match is found, then this action
- is taken.""")),
-
- ('forward_auto_discards', config.Radio, (_('No'), _('Yes')), 0,
- _("""Should messages from non-members, which are automatically
- discarded, be forwarded to the list moderator?""")),
-
- ('nonmember_rejection_notice', config.Text, (10, WIDTH), 1,
- _("""Text to include in any rejection notice to be sent to
- non-members who post to this list. This notice can include
- the list's owner address by %%(listowner)s and replaces the
- internally crafted default message.""")),
-
- ]
-
- recip_rtn = [
- _("""This section allows you to configure various filters based on
- the recipient of the message."""),
-
- _('Recipient filters'),
-
- ('require_explicit_destination', config.Radio,
- (_('No'), _('Yes')), 0,
- _("""Must posts have list named in destination (to, cc) field
- (or be among the acceptable alias names, specified below)?"""),
-
- _("""Many (in fact, most) spams do not explicitly name their
- myriad destinations in the explicit destination addresses - in
- fact often the To: field has a totally bogus address for
- obfuscation. The constraint applies only to the stuff in the
- address before the '@' sign, but still catches all such spams.
-
- <p>The cost is that the list will not accept unhindered any
- postings relayed from other addresses, unless
-
- <ol>
- <li>The relaying address has the same name, or
-
- <li>The relaying address name is included on the options that
- specifies acceptable aliases for the list.
-
- </ol>""")),
-
- ('acceptable_aliases', config.Text, (4, WIDTH), 0,
- _("""Alias names (regexps) which qualify as explicit to or cc
- destination names for this list."""),
-
- _("""Alternate addresses that are acceptable when
- `require_explicit_destination' is enabled. This option takes a
- list of regular expressions, one per line, which is matched
- against every recipient address in the message. The matching is
- performed with Python's re.match() function, meaning they are
- anchored to the start of the string.
-
- <p>For backwards compatibility with Mailman 1.1, if the regexp
- does not contain an `@', then the pattern is matched against just
- the local part of the recipient address. If that match fails, or
- if the pattern does contain an `@', then the pattern is matched
- against the entire recipient address.
-
- <p>Matching against the local part is deprecated; in a future
- release, the pattern will always be matched against the entire
- recipient address.""")),
-
- ('max_num_recipients', config.Number, 5, 0,
- _('Ceiling on acceptable number of recipients for a posting.'),
-
- _('''If a posting has this number, or more, of recipients, it is
- held for admin approval. Use 0 for no ceiling.''')),
- ]
-
- spam_rtn = [
- _("""This section allows you to configure various anti-spam
- filters posting filters, which can help reduce the amount of spam
- your list members end up receiving.
- """),
-
- _('Header filters'),
-
- ('header_filter_rules', config.HeaderFilter, 0, 0,
- _('Filter rules to match against the headers of a message.'),
-
- _("""Each header filter rule has two parts, a list of regular
- expressions, one per line, and an action to take. Mailman
- matches the message's headers against every regular expression in
- the rule and if any match, the message is rejected, held, or
- discarded based on the action you specify. Use <em>Defer</em> to
- temporarily disable a rule.
-
- You can have more than one filter rule for your list. In that
- case, each rule is matched in turn, with processing stopped after
- the first match.
-
- Note that headers are collected from all the attachments
- (except for the mailman administrivia message) and
- matched against the regular expressions. With this feature,
- you can effectively sort out messages with dangerous file
- types or file name extensions.""")),
-
- _('Legacy anti-spam filters'),
-
- ('bounce_matching_headers', config.Text, (6, WIDTH), 0,
- _('Hold posts with header value matching a specified regexp.'),
- _("""Use this option to prohibit posts according to specific
- header values. The target value is a regular-expression for
- matching against the specified header. The match is done
- disregarding letter case. Lines beginning with '#' are ignored
- as comments.
-
- <p>For example:<pre>to: .*@public.com </pre> says to hold all
- postings with a <em>To:</em> mail header containing '@public.com'
- anywhere among the addresses.
-
- <p>Note that leading whitespace is trimmed from the regexp. This
- can be circumvented in a number of ways, e.g. by escaping or
- bracketing it.""")),
- ]
-
- if subcat == 'sender':
- return sender_rtn
- elif subcat == 'recipient':
- return recip_rtn
- elif subcat == 'spam':
- return spam_rtn
- else:
- return subscribing_rtn
-
- def _setValue(self, mlist, property, val, doc):
- # Ignore any hdrfilter_* form variables
- if property.startswith('hdrfilter_'):
- return
- # For subscribe_policy when ALLOW_OPEN_SUBSCRIBE is true, we need to
- # add one to the value because the page didn't present an open list as
- # an option.
- if property == 'subscribe_policy' and not config.ALLOW_OPEN_SUBSCRIBE:
- val += 1
- setattr(mlist, property, val)
-
- # We need to handle the header_filter_rules widgets specially, but
- # everything else can be done by the base class's handleForm() method.
- # However, to do this we need an awful hack. _setValue() and
- # _getValidValue() will essentially ignore any hdrfilter_* form variables.
- # TK: we should call this function only in subcat == 'spam'
- def _handleForm(self, mlist, category, subcat, cgidata, doc):
- # TK: If there is no hdrfilter_* in cgidata, we should not touch
- # the header filter rules.
- if not cgidata.has_key('hdrfilter_rebox_01'):
- return
- # First deal with
- rules = []
- # We start i at 1 and keep going until we no longer find items keyed
- # with the marked tags.
- i = 1
- downi = None
- while True:
- deltag = 'hdrfilter_delete_%02d' % i
- reboxtag = 'hdrfilter_rebox_%02d' % i
- actiontag = 'hdrfilter_action_%02d' % i
- wheretag = 'hdrfilter_where_%02d' % i
- addtag = 'hdrfilter_add_%02d' % i
- newtag = 'hdrfilter_new_%02d' % i
- uptag = 'hdrfilter_up_%02d' % i
- downtag = 'hdrfilter_down_%02d' % i
- i += 1
- # Was this a delete? If so, we can just ignore this entry
- if cgidata.has_key(deltag):
- continue
- # Get the data for the current box
- pattern = cgidata.getvalue(reboxtag)
- try:
- action = int(cgidata.getvalue(actiontag))
- # We'll get a TypeError when the actiontag is missing and the
- # .getvalue() call returns None.
- except (ValueError, TypeError):
- action = config.DEFER
- if pattern is None:
- # We came to the end of the boxes
- break
- if cgidata.has_key(newtag) and not pattern:
- # This new entry is incomplete.
- if i == 2:
- # OK it is the first.
- continue
- doc.addError(_("""Header filter rules require a pattern.
- Incomplete filter rules will be ignored."""))
- continue
- # Make sure the pattern was a legal regular expression
- try:
- re.compile(pattern)
- except (re.error, TypeError):
- safepattern = Utils.websafe(pattern)
- doc.addError(_("""The header filter rule pattern
- '%(safepattern)s' is not a legal regular expression. This
- rule will be ignored."""))
- continue
- # Was this an add item?
- if cgidata.has_key(addtag):
- # Where should the new one be added?
- where = cgidata.getvalue(wheretag)
- if where == 'before':
- # Add a new empty rule box before the current one
- rules.append(('', config.DEFER, True))
- rules.append((pattern, action, False))
- # Default is to add it after...
- else:
- rules.append((pattern, action, False))
- rules.append(('', config.DEFER, True))
- # Was this an up movement?
- elif cgidata.has_key(uptag):
- # As long as this one isn't the first rule, move it up
- if rules:
- rules.insert(-1, (pattern, action, False))
- else:
- rules.append((pattern, action, False))
- # Was this the down movement?
- elif cgidata.has_key(downtag):
- downi = i - 2
- rules.append((pattern, action, False))
- # Otherwise, just retain this one in the list
- else:
- rules.append((pattern, action, False))
- # Move any down button filter rule
- if downi is not None:
- rule = rules[downi]
- del rules[downi]
- rules.insert(downi+1, rule)
- mlist.header_filter_rules = rules
-
- def handleForm(self, mlist, category, subcat, cgidata, doc):
- if subcat == 'spam':
- self._handleForm(mlist, category, subcat, cgidata, doc)
- # Everything else is dealt with by the base handler
- GUIBase.handleForm(self, mlist, category, subcat, cgidata, doc)
diff --git a/src/web/Gui/Topics.py b/src/web/Gui/Topics.py
deleted file mode 100644
index 00df988be..000000000
--- a/src/web/Gui/Topics.py
+++ /dev/null
@@ -1,162 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-import re
-
-from Mailman import Utils
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-OR = '|'
-
-
-
-class Topics(GUIBase):
- def GetConfigCategory(self):
- return 'topics', _('Topics')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'topics':
- return None
- WIDTH = config.TEXTFIELDWIDTH
-
- return [
- _('List topic keywords'),
-
- ('topics_enabled', config.Radio, (_('Disabled'), _('Enabled')), 0,
- _('''Should the topic filter be enabled or disabled?'''),
-
- _("""The topic filter categorizes each incoming email message
- according to <a
- href="http://www.python.org/doc/current/lib/module-re.html">regular
- expression filters</a> you specify below. If the message's
- <code>Subject:</code> or <code>Keywords:</code> header contains a
- match against a topic filter, the message is logically placed
- into a topic <em>bucket</em>. Each user can then choose to only
- receive messages from the mailing list for a particular topic
- bucket (or buckets). Any message not categorized in a topic
- bucket registered with the user is not delivered to the list.
-
- <p>Note that this feature only works with regular delivery, not
- digest delivery.
-
- <p>The body of the message can also be optionally scanned for
- <code>Subject:</code> and <code>Keywords:</code> headers, as
- specified by the <a
- href="?VARHELP=topics/topics_bodylines_limit">topics_bodylines_limit</a>
- configuration variable.""")),
-
- ('topics_bodylines_limit', config.Number, 5, 0,
- _('How many body lines should the topic matcher scan?'),
-
- _("""The topic matcher will scan this many lines of the message
- body looking for topic keyword matches. Body scanning stops when
- either this many lines have been looked at, or a non-header-like
- body line is encountered. By setting this value to zero, no body
- lines will be scanned (i.e. only the <code>Keywords:</code> and
- <code>Subject:</code> headers will be scanned). By setting this
- value to a negative number, then all body lines will be scanned
- until a non-header-like line is encountered.
- """)),
-
- ('topics', config.Topics, 0, 0,
- _('Topic keywords, one per line, to match against each message.'),
-
- _("""Each topic keyword is actually a regular expression, which is
- matched against certain parts of a mail message, specifically the
- <code>Keywords:</code> and <code>Subject:</code> message headers.
- Note that the first few lines of the body of the message can also
- contain a <code>Keywords:</code> and <code>Subject:</code>
- "header" on which matching is also performed.""")),
-
- ]
-
- def handleForm(self, mlist, category, subcat, cgidata, doc):
- # MAS: Did we come from the authentication page?
- if not cgidata.has_key('topic_box_01'):
- return
- topics = []
- # We start i at 1 and keep going until we no longer find items keyed
- # with the marked tags.
- i = 1
- while True:
- deltag = 'topic_delete_%02d' % i
- boxtag = 'topic_box_%02d' % i
- reboxtag = 'topic_rebox_%02d' % i
- desctag = 'topic_desc_%02d' % i
- wheretag = 'topic_where_%02d' % i
- addtag = 'topic_add_%02d' % i
- newtag = 'topic_new_%02d' % i
- i += 1
- # Was this a delete? If so, we can just ignore this entry
- if cgidata.has_key(deltag):
- continue
- # Get the data for the current box
- name = cgidata.getvalue(boxtag)
- pattern = cgidata.getvalue(reboxtag)
- desc = cgidata.getvalue(desctag)
- if name is None:
- # We came to the end of the boxes
- break
- if cgidata.has_key(newtag) and (not name or not pattern):
- # This new entry is incomplete.
- doc.addError(_("""Topic specifications require both a name and
- a pattern. Incomplete topics will be ignored."""))
- continue
- # Make sure the pattern was a legal regular expression
- name = Utils.websafe(name)
- try:
- orpattern = OR.join(pattern.splitlines())
- re.compile(orpattern)
- except (re.error, TypeError):
- safepattern = Utils.websafe(orpattern)
- doc.addError(_("""The topic pattern '%(safepattern)s' is not a
- legal regular expression. It will be discarded."""))
- continue
- # Was this an add item?
- if cgidata.has_key(addtag):
- # Where should the new one be added?
- where = cgidata.getvalue(wheretag)
- if where == 'before':
- # Add a new empty topics box before the current one
- topics.append(('', '', '', True))
- topics.append((name, pattern, desc, False))
- # Default is to add it after...
- else:
- topics.append((name, pattern, desc, False))
- topics.append(('', '', '', True))
- # Otherwise, just retain this one in the list
- else:
- topics.append((name, pattern, desc, False))
- # Add these topics to the mailing list object, and deal with other
- # options.
- mlist.topics = topics
- try:
- mlist.topics_enabled = int(cgidata.getvalue(
- 'topics_enabled',
- mlist.topics_enabled))
- except ValueError:
- # BAW: should really print a warning
- pass
- try:
- mlist.topics_bodylines_limit = int(cgidata.getvalue(
- 'topics_bodylines_limit',
- mlist.topics_bodylines_limit))
- except ValueError:
- # BAW: should really print a warning
- pass
diff --git a/src/web/Gui/Usenet.py b/src/web/Gui/Usenet.py
deleted file mode 100644
index 9c1b50809..000000000
--- a/src/web/Gui/Usenet.py
+++ /dev/null
@@ -1,140 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-from Mailman.Gui.GUIBase import GUIBase
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-
-
-class Usenet(GUIBase):
- def GetConfigCategory(self):
- return 'gateway', _('Mail&lt;-&gt;News&nbsp;gateways')
-
- def GetConfigInfo(self, mlist, category, subcat=None):
- if category <> 'gateway':
- return None
-
- WIDTH = config.TEXTFIELDWIDTH
- VERTICAL = 1
-
- return [
- _('Mail-to-News and News-to-Mail gateway services.'),
-
- _('News server settings'),
-
- ('nntp_host', config.String, WIDTH, 0,
- _('The hostname of the machine your news server is running on.'),
- _('''This value may be either the name of your news server, or
- optionally of the format name:port, where port is a port number.
-
- The news server is not part of Mailman proper. You have to
- already have access to an NNTP server, and that NNTP server must
- recognize the machine this mailing list runs on as a machine
- capable of reading and posting news.''')),
-
- ('linked_newsgroup', config.String, WIDTH, 0,
- _('The name of the Usenet group to gateway to and/or from.')),
-
- ('gateway_to_news', config.Toggle, (_('No'), _('Yes')), 0,
- _('''Should new posts to the mailing list be sent to the
- newsgroup?''')),
-
- ('gateway_to_mail', config.Toggle, (_('No'), _('Yes')), 0,
- _('''Should new posts to the newsgroup be sent to the mailing
- list?''')),
-
- _('Forwarding options'),
-
- ('news_moderation', config.Radio,
- (_('None'), _('Open list, moderated group'), _('Moderated')),
- VERTICAL,
-
- _("""The moderation policy of the newsgroup."""),
-
- _("""This setting determines the moderation policy of the
- newsgroup and its interaction with the moderation policy of the
- mailing list. This only applies to the newsgroup that you are
- gatewaying <em>to</em>, so if you are only gatewaying from
- Usenet, or the newsgroup you are gatewaying to is not moderated,
- set this option to <em>None</em>.
-
- <p>If the newsgroup is moderated, you can set this mailing list
- up to be the moderation address for the newsgroup. By selecting
- <em>Moderated</em>, an additional posting hold will be placed in
- the approval process. All messages posted to the mailing list
- will have to be approved before being sent on to the newsgroup,
- or to the mailing list membership.
-
- <p><em>Note that if the message has an <tt>Approved</tt> header
- with the list's administrative password in it, this hold test
- will be bypassed, allowing privileged posters to send messages
- directly to the list and the newsgroup.</em>
-
- <p>Finally, if the newsgroup is moderated, but you want to have
- an open posting policy anyway, you should select <em>Open list,
- moderated group</em>. The effect of this is to use the normal
- Mailman moderation facilities, but to add an <tt>Approved</tt>
- header to all messages that are gatewayed to Usenet.""")),
-
- ('news_prefix_subject_too', config.Toggle, (_('No'), _('Yes')), 0,
- _('Prefix <tt>Subject:</tt> headers on postings gated to news?'),
- _("""Mailman prefixes <tt>Subject:</tt> headers with
- <a href="?VARHELP=general/subject_prefix">text you can
- customize</a> and normally, this prefix shows up in messages
- gatewayed to Usenet. You can set this option to <em>No</em> to
- disable the prefix on gated messages. Of course, if you turn off
- normal <tt>Subject:</tt> prefixes, they won't be prefixed for
- gated messages either.""")),
-
- _('Mass catch up'),
-
- ('_mass_catchup', config.Toggle, (_('No'), _('Yes')), 0,
- _('Should Mailman perform a <em>catchup</em> on the newsgroup?'),
- _('''When you tell Mailman to perform a catchup on the newsgroup,
- this means that you want to start gating messages to the mailing
- list with the next new message found. All earlier messages on
- the newsgroup will be ignored. This is as if you were reading
- the newsgroup yourself, and you marked all current messages as
- <em>read</em>. By catching up, your mailing list members will
- not see any of the earlier messages.''')),
-
- ]
-
- def _setValue(self, mlist, property, val, doc):
- # Watch for the special, immediate action attributes
- if property == '_mass_catchup' and val:
- mlist.usenet_watermark = None
- doc.AddItem(_('Mass catchup completed'))
- else:
- GUIBase._setValue(self, mlist, property, val, doc)
-
- def _postValidate(self, mlist, doc):
- # Make sure that if we're gating, that the newsgroups and host
- # information are not blank.
- if mlist.gateway_to_news or mlist.gateway_to_mail:
- # BAW: It's too expensive and annoying to ensure that both the
- # host is valid and that the newsgroup is a valid n.g. on the
- # server. This should be good enough.
- if not mlist.nntp_host or not mlist.linked_newsgroup:
- doc.addError(_("""You cannot enable gatewaying unless both the
- <a href="?VARHELP=gateway/nntp_host">news server field</a> and
- the <a href="?VARHELP=gateway/linked_newsgroup">linked
- newsgroup</a> fields are filled in."""))
- # And reset these values
- mlist.gateway_to_news = 0
- mlist.gateway_to_mail = 0
diff --git a/src/web/Gui/__init__.py b/src/web/Gui/__init__.py
deleted file mode 100644
index 2e12526c0..000000000
--- a/src/web/Gui/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Copyright (C) 2001-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-from Archive import Archive
-from Autoresponse import Autoresponse
-from Bounce import Bounce
-from Digest import Digest
-from General import General
-from Membership import Membership
-from NonDigest import NonDigest
-from Passwords import Passwords
-from Privacy import Privacy
-from Topics import Topics
-from Usenet import Usenet
-from Language import Language
-from ContentFilter import ContentFilter
-
-# Don't export this symbol outside the package
-del GUIBase
diff --git a/src/web/HTMLFormatter.py b/src/web/HTMLFormatter.py
deleted file mode 100644
index 594f4adea..000000000
--- a/src/web/HTMLFormatter.py
+++ /dev/null
@@ -1,437 +0,0 @@
-# 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/>.
-
-"""Routines for presentation of list-specific HTML text."""
-
-import re
-import time
-
-from mailman import Defaults
-from mailman import MemberAdaptor
-from mailman import Utils
-from mailman.configuration import config
-from mailman.htmlformat import *
-from mailman.i18n import _
-
-
-EMPTYSTRING = ''
-BR = '<br>'
-NL = '\n'
-COMMASPACE = ', '
-
-
-
-class HTMLFormatter:
- def GetMailmanFooter(self):
- ownertext = COMMASPACE.join([Utils.ObscureEmail(a, 1)
- for a in self.owner])
- # Remove the .Format() when htmlformat conversion is done.
- realname = self.real_name
- hostname = self.host_name
- listinfo_link = Link(self.GetScriptURL('listinfo'),
- realname).Format()
- owner_link = Link('mailto:' + self.GetOwnerEmail(), ownertext).Format()
- innertext = _('%(listinfo_link)s list run by %(owner_link)s')
- return Container(
- '<hr>',
- Address(
- Container(
- innertext,
- '<br>',
- Link(self.GetScriptURL('admin'),
- _('%(realname)s administrative interface')),
- _(' (requires authorization)'),
- '<br>',
- Link(Utils.ScriptURL('listinfo'),
- _('Overview of all %(hostname)s mailing lists')),
- '<p>', MailmanLogo()))).Format()
-
- def FormatUsers(self, digest, lang=None):
- if lang is None:
- lang = self.preferred_language
- conceal_sub = Defaults.ConcealSubscription
- people = []
- if digest:
- digestmembers = self.getDigestMemberKeys()
- for dm in digestmembers:
- if not self.getMemberOption(dm, conceal_sub):
- people.append(dm)
- num_concealed = len(digestmembers) - len(people)
- else:
- members = self.getRegularMemberKeys()
- for m in members:
- if not self.getMemberOption(m, conceal_sub):
- people.append(m)
- num_concealed = len(members) - len(people)
- if num_concealed == 1:
- concealed = _('<em>(1 private member not shown)</em>')
- elif num_concealed > 1:
- concealed = _(
- '<em>(%(num_concealed)d private members not shown)</em>')
- else:
- concealed = ''
- items = []
- people.sort()
- obscure = self.obscure_addresses
- for person in people:
- id = Utils.ObscureEmail(person)
- url = self.GetOptionsURL(person, obscure=obscure)
- if obscure:
- showing = Utils.ObscureEmail(person, for_text=1)
- else:
- showing = person
- got = Link(url, showing)
- if self.getDeliveryStatus(person) <> MemberAdaptor.ENABLED:
- got = Italic('(', got, ')')
- items.append(got)
- # Just return the .Format() so this works until I finish
- # converting everything to htmlformat...
- return concealed + UnorderedList(*tuple(items)).Format()
-
- def FormatOptionButton(self, option, value, user):
- if option == Defaults.DisableDelivery:
- optval = self.getDeliveryStatus(user) <> MemberAdaptor.ENABLED
- else:
- optval = self.getMemberOption(user, option)
- if optval == value:
- checked = ' CHECKED'
- else:
- checked = ''
- name = {
- Defaults.DontReceiveOwnPosts : 'dontreceive',
- Defaults.DisableDelivery : 'disablemail',
- Defaults.DisableMime : 'mime',
- Defaults.AcknowledgePosts : 'ackposts',
- Defaults.Digests : 'digest',
- Defaults.ConcealSubscription : 'conceal',
- Defaults.SuppressPasswordReminder : 'remind',
- Defaults.ReceiveNonmatchingTopics : 'rcvtopic',
- Defaults.DontReceiveDuplicates : 'nodupes',
- }[option]
- return '<input type=radio name="%s" value="%d"%s>' % (
- name, value, checked)
-
- def FormatDigestButton(self):
- if self.digest_is_default:
- checked = ' CHECKED'
- else:
- checked = ''
- return '<input type=radio name="digest" value="1"%s>' % checked
-
- def FormatDisabledNotice(self, user):
- status = self.getDeliveryStatus(user)
- reason = None
- info = self.getBounceInfo(user)
- if status == MemberAdaptor.BYUSER:
- reason = _('; it was disabled by you')
- elif status == MemberAdaptor.BYADMIN:
- reason = _('; it was disabled by the list administrator')
- elif status == MemberAdaptor.BYBOUNCE:
- date = time.strftime('%d-%b-%Y',
- time.localtime(Utils.midnight(info.date)))
- reason = _('''; it was disabled due to excessive bounces. The
- last bounce was received on %(date)s''')
- elif status == MemberAdaptor.UNKNOWN:
- reason = _('; it was disabled for unknown reasons')
- if reason:
- note = FontSize('+1', _(
- 'Note: your list delivery is currently disabled%(reason)s.'
- )).Format()
- link = Link('#disable', _('Mail delivery')).Format()
- mailto = Link('mailto:' + self.GetOwnerEmail(),
- _('the list administrator')).Format()
- return _('''<p>%(note)s
-
- <p>You may have disabled list delivery intentionally,
- or it may have been triggered by bounces from your email
- address. In either case, to re-enable delivery, change the
- %(link)s option below. Contact %(mailto)s if you have any
- questions or need assistance.''')
- elif info and info.score > 0:
- # Provide information about their current bounce score. We know
- # their membership is currently enabled.
- score = info.score
- total = self.bounce_score_threshold
- return _('''<p>We have received some recent bounces from your
- address. Your current <em>bounce score</em> is %(score)s out of a
- maximum of %(total)s. Please double check that your subscribed
- address is correct and that there are no problems with delivery to
- this address. Your bounce score will be automatically reset if
- the problems are corrected soon.''')
- else:
- return ''
-
- def FormatUmbrellaNotice(self, user, type):
- addr = self.GetMemberAdminEmail(user)
- if self.umbrella_list:
- return _("(Note - you are subscribing to a list of mailing lists, "
- "so the %(type)s notice will be sent to the admin address"
- " for your membership, %(addr)s.)<p>")
- else:
- return ""
-
- def FormatSubscriptionMsg(self):
- msg = ''
- also = ''
- if self.subscribe_policy == 1:
- msg += _('''You will be sent email requesting confirmation, to
- prevent others from gratuitously subscribing you.''')
- elif self.subscribe_policy == 2:
- msg += _("""This is a closed list, which means your subscription
- will be held for approval. You will be notified of the list
- moderator's decision by email.""")
- also = _('also ')
- elif self.subscribe_policy == 3:
- msg += _("""You will be sent email requesting confirmation, to
- prevent others from gratuitously subscribing you. Once
- confirmation is received, your request will be held for approval
- by the list moderator. You will be notified of the moderator's
- decision by email.""")
- also = _("also ")
- if msg:
- msg += ' '
- if self.private_roster == 1:
- msg += _('''This is %(also)sa private list, which means that the
- list of members is not available to non-members.''')
- elif self.private_roster:
- msg += _('''This is %(also)sa hidden list, which means that the
- list of members is available only to the list administrator.''')
- else:
- msg += _('''This is %(also)sa public list, which means that the
- list of members list is available to everyone.''')
- if self.obscure_addresses:
- msg += _(''' (but we obscure the addresses so they are not
- easily recognizable by spammers).''')
-
- if self.umbrella_list:
- sfx = self.umbrella_member_suffix
- msg += _("""<p>(Note that this is an umbrella list, intended to
- have only other mailing lists as members. Among other things,
- this means that your confirmation request will be sent to the
- `%(sfx)s' account for your address.)""")
- return msg
-
- def FormatUndigestButton(self):
- if self.digest_is_default:
- checked = ''
- else:
- checked = ' CHECKED'
- return '<input type=radio name="digest" value="0"%s>' % checked
-
- def FormatMimeDigestsButton(self):
- if self.mime_is_default_digest:
- checked = ' CHECKED'
- else:
- checked = ''
- return '<input type=radio name="mime" value="1"%s>' % checked
-
- def FormatPlainDigestsButton(self):
- if self.mime_is_default_digest:
- checked = ''
- else:
- checked = ' CHECKED'
- return '<input type=radio name="plain" value="1"%s>' % checked
-
- def FormatEditingOption(self, lang):
- if self.private_roster == 0:
- either = _('<b><i>either</i></b> ')
- else:
- either = ''
- realname = self.real_name
-
- text = (_('''To unsubscribe from %(realname)s, get a password reminder,
- or change your subscription options %(either)senter your subscription
- email address:
- <p><center> ''')
- + TextBox('email', size=30).Format()
- + ' '
- + SubmitButton('UserOptions',
- _('Unsubscribe or edit options')).Format()
- + Hidden('language', lang).Format()
- + '</center>')
- if self.private_roster == 0:
- text += _('''<p>... <b><i>or</i></b> select your entry from
- the subscribers list (see above).''')
- text += _(''' If you leave the field blank, you will be prompted for
- your email address''')
- return text
-
- def RestrictedListMessage(self, which, restriction):
- if not restriction:
- return ''
- elif restriction == 1:
- return _(
- '''(<i>%(which)s is only available to the list
- members.</i>)''')
- else:
- return _('''(<i>%(which)s is only available to the list
- administrator.</i>)''')
-
- def FormatRosterOptionForUser(self, lang):
- return self.RosterOption(lang).Format()
-
- def RosterOption(self, lang):
- container = Container()
- container.AddItem(Hidden('language', lang))
- if not self.private_roster:
- container.AddItem(_("Click here for the list of ")
- + self.real_name
- + _(" subscribers: "))
- container.AddItem(SubmitButton('SubscriberRoster',
- _("Visit Subscriber list")))
- else:
- if self.private_roster == 1:
- only = _('members')
- whom = _('Address:')
- else:
- only = _('the list administrator')
- whom = _('Admin address:')
- # Solicit the user and password.
- container.AddItem(
- self.RestrictedListMessage(_('The subscribers list'),
- self.private_roster)
- + _(" <p>Enter your ")
- + whom[:-1].lower()
- + _(" and password to visit"
- " the subscribers list: <p><center> ")
- + whom
- + " ")
- container.AddItem(self.FormatBox('roster-email'))
- container.AddItem(_("Password: ")
- + self.FormatSecureBox('roster-pw')
- + "&nbsp;&nbsp;")
- container.AddItem(SubmitButton('SubscriberRoster',
- _('Visit Subscriber List')))
- container.AddItem("</center>")
- return container
-
- def FormatFormStart(self, name, extra=''):
- base_url = self.GetScriptURL(name)
- if extra:
- full_url = "%s/%s" % (base_url, extra)
- else:
- full_url = base_url
- return ('<FORM Method=POST ACTION="%s">' % full_url)
-
- def FormatArchiveAnchor(self):
- return '<a href="%s">' % self.GetBaseArchiveURL()
-
- def FormatFormEnd(self):
- return '</FORM>'
-
- def FormatBox(self, name, size=20, value=''):
- return '<INPUT type="Text" name="%s" size="%d" value="%s">' % (
- name, size, value)
-
- def FormatSecureBox(self, name):
- return '<INPUT type="Password" name="%s" size="15">' % name
-
- def FormatButton(self, name, text='Submit'):
- return '<INPUT type="Submit" name="%s" value="%s">' % (name, text)
-
- def FormatReminder(self, lang):
- if self.send_reminders:
- return _('Once a month, your password will be emailed to you as'
- ' a reminder.')
- return ''
-
- def ParseTags(self, template, replacements, lang=None):
- if lang is None:
- charset = 'us-ascii'
- else:
- charset = Utils.GetCharSet(lang)
- text = Utils.maketext(template, raw=1, lang=lang, mlist=self)
- parts = re.split('(</?[Mm][Mm]-[^>]*>)', text)
- i = 1
- while i < len(parts):
- tag = parts[i].lower()
- if replacements.has_key(tag):
- repl = replacements[tag]
- if isinstance(repl, str):
- repl = unicode(repl, charset, 'replace')
- parts[i] = repl
- else:
- parts[i] = ''
- i = i + 2
- return EMPTYSTRING.join(parts)
-
- # This needs to wait until after the list is inited, so let's build it
- # when it's needed only.
- def GetStandardReplacements(self, lang=None):
- dmember_len = len(self.getDigestMemberKeys())
- member_len = len(self.getRegularMemberKeys())
- # If only one language is enabled for this mailing list, omit the
- # language choice buttons.
- if len(self.language_codes) == 1:
- listlangs = _(
- config.languages.get_description(self.preferred_language))
- else:
- listlangs = self.GetLangSelectBox(lang).Format()
- d = {
- '<mm-mailman-footer>' : self.GetMailmanFooter(),
- '<mm-list-name>' : self.real_name,
- '<mm-email-user>' : self._internal_name,
- '<mm-list-description>' : self.description,
- '<mm-list-info>' : BR.join(self.info.split(NL)),
- '<mm-form-end>' : self.FormatFormEnd(),
- '<mm-archive>' : self.FormatArchiveAnchor(),
- '</mm-archive>' : '</a>',
- '<mm-list-subscription-msg>' : self.FormatSubscriptionMsg(),
- '<mm-restricted-list-message>' : \
- self.RestrictedListMessage(_('The current archive'),
- self.archive_private),
- '<mm-num-reg-users>' : `member_len`,
- '<mm-num-digesters>' : `dmember_len`,
- '<mm-num-members>' : (`member_len + dmember_len`),
- '<mm-posting-addr>' : '%s' % self.GetListEmail(),
- '<mm-request-addr>' : '%s' % self.GetRequestEmail(),
- '<mm-owner>' : self.GetOwnerEmail(),
- '<mm-reminder>' : self.FormatReminder(self.preferred_language),
- '<mm-host>' : self.host_name,
- '<mm-list-langs>' : listlangs,
- }
- if config.IMAGE_LOGOS:
- d['<mm-favicon>'] = config.IMAGE_LOGOS + config.SHORTCUT_ICON
- return d
-
- def GetAllReplacements(self, lang=None):
- """
- returns standard replaces plus formatted user lists in
- a dict just like GetStandardReplacements.
- """
- if lang is None:
- lang = self.preferred_language
- d = self.GetStandardReplacements(lang)
- d.update({"<mm-regular-users>": self.FormatUsers(0, lang),
- "<mm-digest-users>": self.FormatUsers(1, lang)})
- return d
-
- def GetLangSelectBox(self, lang=None, varname='language'):
- if lang is None:
- lang = self.preferred_language
- # Figure out the available languages
- values = self.language_codes
- legend = [config.languages.get_description(code) for code in values]
- try:
- selected = values.index(lang)
- except ValueError:
- try:
- selected = values.index(self.preferred_language)
- except ValueError:
- selected = config.DEFAULT_SERVER_LANGUAGE
- # Return the widget
- return SelectOptions(varname, values, legend, selected)
diff --git a/src/web/__init__.py b/src/web/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/src/web/__init__.py
+++ /dev/null
diff --git a/src/web/htmlformat.py b/src/web/htmlformat.py
deleted file mode 100644
index 608d0e647..000000000
--- a/src/web/htmlformat.py
+++ /dev/null
@@ -1,670 +0,0 @@
-# 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/>.
-
-"""Library for program-based construction of an HTML documents.
-
-Encapsulate HTML formatting directives in classes that act as containers
-for python and, recursively, for nested HTML formatting objects.
-"""
-
-from Mailman import Defaults
-from Mailman import Utils
-from Mailman import version
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-SPACE = ' '
-EMPTYSTRING = ''
-NL = '\n'
-
-
-
-# Format an arbitrary object.
-def HTMLFormatObject(item, indent):
- "Return a presentation of an object, invoking their Format method if any."
- if hasattr(item, 'Format'):
- return item.Format(indent)
- if isinstance(item, basestring):
- return item
- return str(item)
-
-def CaseInsensitiveKeyedDict(d):
- result = {}
- for (k,v) in d.items():
- result[k.lower()] = v
- return result
-
-# Given references to two dictionaries, copy the second dictionary into the
-# first one.
-def DictMerge(destination, fresh_dict):
- for (key, value) in fresh_dict.items():
- destination[key] = value
-
-class Table:
- def __init__(self, **table_opts):
- self.cells = []
- self.cell_info = {}
- self.row_info = {}
- self.opts = table_opts
-
- def AddOptions(self, opts):
- DictMerge(self.opts, opts)
-
- # Sets all of the cells. It writes over whatever cells you had there
- # previously.
-
- def SetAllCells(self, cells):
- self.cells = cells
-
- # Add a new blank row at the end
- def NewRow(self):
- self.cells.append([])
-
- # Add a new blank cell at the end
- def NewCell(self):
- self.cells[-1].append('')
-
- def AddRow(self, row):
- self.cells.append(row)
-
- def AddCell(self, cell):
- self.cells[-1].append(cell)
-
- def AddCellInfo(self, row, col, **kws):
- kws = CaseInsensitiveKeyedDict(kws)
- if not self.cell_info.has_key(row):
- self.cell_info[row] = { col : kws }
- elif self.cell_info[row].has_key(col):
- DictMerge(self.cell_info[row], kws)
- else:
- self.cell_info[row][col] = kws
-
- def AddRowInfo(self, row, **kws):
- kws = CaseInsensitiveKeyedDict(kws)
- if not self.row_info.has_key(row):
- self.row_info[row] = kws
- else:
- DictMerge(self.row_info[row], kws)
-
- # What's the index for the row we just put in?
- def GetCurrentRowIndex(self):
- return len(self.cells)-1
-
- # What's the index for the col we just put in?
- def GetCurrentCellIndex(self):
- return len(self.cells[-1])-1
-
- def ExtractCellInfo(self, info):
- valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan',
- 'bgcolor']
- output = ''
-
- for (key, val) in info.items():
- if not key in valid_mods:
- continue
- if key == 'nowrap':
- output = output + ' NOWRAP'
- continue
- else:
- output = output + ' %s="%s"' % (key.upper(), val)
-
- return output
-
- def ExtractRowInfo(self, info):
- valid_mods = ['align', 'valign', 'bgcolor']
- output = ''
-
- for (key, val) in info.items():
- if not key in valid_mods:
- continue
- output = output + ' %s="%s"' % (key.upper(), val)
-
- return output
-
- def ExtractTableInfo(self, info):
- valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding',
- 'bgcolor']
-
- output = ''
-
- for (key, val) in info.items():
- if not key in valid_mods:
- continue
- if key == 'border' and val == None:
- output = output + ' BORDER'
- continue
- else:
- output = output + ' %s="%s"' % (key.upper(), val)
-
- return output
-
- def FormatCell(self, row, col, indent):
- try:
- my_info = self.cell_info[row][col]
- except:
- my_info = None
-
- output = '\n' + ' '*indent + '<td'
- if my_info:
- output = output + self.ExtractCellInfo(my_info)
- item = self.cells[row][col]
- item_format = HTMLFormatObject(item, indent+4)
- output = '%s>%s</td>' % (output, item_format)
- return output
-
- def FormatRow(self, row, indent):
- try:
- my_info = self.row_info[row]
- except:
- my_info = None
-
- output = '\n' + ' '*indent + '<tr'
- if my_info:
- output = output + self.ExtractRowInfo(my_info)
- output = output + '>'
-
- for i in range(len(self.cells[row])):
- output = output + self.FormatCell(row, i, indent + 2)
-
- output = output + '\n' + ' '*indent + '</tr>'
-
- return output
-
- def Format(self, indent=0):
- output = '\n' + ' '*indent + '<table'
- output = output + self.ExtractTableInfo(self.opts)
- output = output + '>'
-
- for i in range(len(self.cells)):
- output = output + self.FormatRow(i, indent + 2)
-
- output = output + '\n' + ' '*indent + '</table>\n'
-
- return output
-
-
-class Link:
- def __init__(self, href, text, target=None):
- self.href = href
- self.text = text
- self.target = target
-
- def Format(self, indent=0):
- texpr = ""
- if self.target != None:
- texpr = ' target="%s"' % self.target
- return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent),
- texpr,
- HTMLFormatObject(self.text, indent))
-
-class FontSize:
- """FontSize is being deprecated - use FontAttr(..., size="...") instead."""
- def __init__(self, size, *items):
- self.items = list(items)
- self.size = size
-
- def Format(self, indent=0):
- output = '<font size="%s">' % self.size
- for item in self.items:
- output = output + HTMLFormatObject(item, indent)
- output = output + '</font>'
- return output
-
-class FontAttr:
- """Present arbitrary font attributes."""
- def __init__(self, *items, **kw):
- self.items = list(items)
- self.attrs = kw
-
- def Format(self, indent=0):
- seq = []
- for k, v in self.attrs.items():
- seq.append('%s="%s"' % (k, v))
- output = '<font %s>' % SPACE.join(seq)
- for item in self.items:
- output = output + HTMLFormatObject(item, indent)
- output = output + '</font>'
- return output
-
-
-class Container:
- def __init__(self, *items):
- if not items:
- self.items = []
- else:
- self.items = items
-
- def AddItem(self, obj):
- self.items.append(obj)
-
- def Format(self, indent=0):
- output = []
- for item in self.items:
- output.append(HTMLFormatObject(item, indent))
- return EMPTYSTRING.join(output)
-
-
-class Label(Container):
- align = 'right'
-
- def __init__(self, *items):
- Container.__init__(self, *items)
-
- def Format(self, indent=0):
- return ('<div align="%s">' % self.align) + \
- Container.Format(self, indent) + \
- '</div>'
-
-
-# My own standard document template. YMMV.
-# something more abstract would be more work to use...
-
-class Document(Container):
- title = None
- language = None
- bgcolor = Defaults.WEB_BG_COLOR
- suppress_head = 0
-
- def set_language(self, lang=None):
- self.language = lang
-
- def set_bgcolor(self, color):
- self.bgcolor = color
-
- def SetTitle(self, title):
- self.title = title
-
- def Format(self, indent=0, **kws):
- charset = 'us-ascii'
- if self.language:
- charset = Utils.GetCharSet(self.language)
- output = ['Content-Type: text/html; charset=%s\n' % charset]
- if not self.suppress_head:
- kws.setdefault('bgcolor', self.bgcolor)
- tab = ' ' * indent
- output.extend([tab,
- '<HTML>',
- '<HEAD>'
- ])
- if config.IMAGE_LOGOS:
- output.append('<LINK REL="SHORTCUT ICON" HREF="%s">' %
- (config.IMAGE_LOGOS + config.SHORTCUT_ICON))
- # Hit all the bases
- output.append('<META http-equiv="Content-Type" '
- 'content="text/html; charset=%s">' % charset)
- if self.title:
- output.append('%s<TITLE>%s</TITLE>' % (tab, self.title))
- output.append('%s</HEAD>' % tab)
- quals = []
- # Default link colors
- if config.WEB_VLINK_COLOR:
- kws.setdefault('vlink', config.WEB_VLINK_COLOR)
- if config.WEB_ALINK_COLOR:
- kws.setdefault('alink', config.WEB_ALINK_COLOR)
- if config.WEB_LINK_COLOR:
- kws.setdefault('link', config.WEB_LINK_COLOR)
- for k, v in kws.items():
- quals.append('%s="%s"' % (k, v))
- output.append('%s<BODY %s>' % (tab, SPACE.join(quals)))
- # Always do this...
- output.append(Container.Format(self, indent))
- if not self.suppress_head:
- output.append('%s</BODY>' % tab)
- output.append('%s</HTML>' % tab)
- return NL.join(output).encode(charset, 'replace')
-
- def addError(self, errmsg, tag=None):
- if tag is None:
- tag = _('Error: ')
- self.AddItem(Header(3, Bold(FontAttr(
- _(tag), color=config.WEB_ERROR_COLOR, size='+2')).Format() +
- Italic(errmsg).Format()))
-
-
-class HeadlessDocument(Document):
- """Document without head section, for templates that provide their own."""
- suppress_head = 1
-
-
-class StdContainer(Container):
- def Format(self, indent=0):
- # If I don't start a new I ignore indent
- output = '<%s>' % self.tag
- output = output + Container.Format(self, indent)
- output = '%s</%s>' % (output, self.tag)
- return output
-
-
-class QuotedContainer(Container):
- def Format(self, indent=0):
- # If I don't start a new I ignore indent
- output = '<%s>%s</%s>' % (
- self.tag,
- Utils.websafe(Container.Format(self, indent)),
- self.tag)
- return output
-
-class Header(StdContainer):
- def __init__(self, num, *items):
- self.items = items
- self.tag = 'h%d' % num
-
-class Address(StdContainer):
- tag = 'address'
-
-class Underline(StdContainer):
- tag = 'u'
-
-class Bold(StdContainer):
- tag = 'strong'
-
-class Italic(StdContainer):
- tag = 'em'
-
-class Preformatted(QuotedContainer):
- tag = 'pre'
-
-class Subscript(StdContainer):
- tag = 'sub'
-
-class Superscript(StdContainer):
- tag = 'sup'
-
-class Strikeout(StdContainer):
- tag = 'strike'
-
-class Center(StdContainer):
- tag = 'center'
-
-class Form(Container):
- def __init__(self, action='', method='POST', encoding=None, *items):
- apply(Container.__init__, (self,) + items)
- self.action = action
- self.method = method
- self.encoding = encoding
-
- def set_action(self, action):
- self.action = action
-
- def Format(self, indent=0):
- spaces = ' ' * indent
- encoding = ''
- if self.encoding:
- encoding = 'enctype="%s"' % self.encoding
- output = '\n%s<FORM action="%s" method="%s" %s>\n' % (
- spaces, self.action, self.method, encoding)
- output = output + Container.Format(self, indent+2)
- output = '%s\n%s</FORM>\n' % (output, spaces)
- return output
-
-
-class InputObj:
- def __init__(self, name, ty, value, checked, **kws):
- self.name = name
- self.type = ty
- self.value = value
- self.checked = checked
- self.kws = kws
-
- def Format(self, indent=0):
- output = ['<INPUT name="%s" type="%s" value="%s"' %
- (self.name, self.type, self.value)]
- for item in self.kws.items():
- output.append('%s="%s"' % item)
- if self.checked:
- output.append('CHECKED')
- output.append('>')
- return SPACE.join(output)
-
-
-class SubmitButton(InputObj):
- def __init__(self, name, button_text):
- InputObj.__init__(self, name, "SUBMIT", button_text, checked=0)
-
-class PasswordBox(InputObj):
- def __init__(self, name, value='', size=Defaults.TEXTFIELDWIDTH):
- InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size)
-
-class TextBox(InputObj):
- def __init__(self, name, value='', size=Defaults.TEXTFIELDWIDTH):
- InputObj.__init__(self, name, "TEXT", value, checked=0, size=size)
-
-class Hidden(InputObj):
- def __init__(self, name, value=''):
- InputObj.__init__(self, name, 'HIDDEN', value, checked=0)
-
-class TextArea:
- def __init__(self, name, text='', rows=None, cols=None, wrap='soft',
- readonly=0):
- self.name = name
- self.text = text
- self.rows = rows
- self.cols = cols
- self.wrap = wrap
- self.readonly = readonly
-
- def Format(self, indent=0):
- output = '<TEXTAREA NAME=%s' % self.name
- if self.rows:
- output += ' ROWS=%s' % self.rows
- if self.cols:
- output += ' COLS=%s' % self.cols
- if self.wrap:
- output += ' WRAP=%s' % self.wrap
- if self.readonly:
- output += ' READONLY'
- output += '>%s</TEXTAREA>' % self.text
- return output
-
-class FileUpload(InputObj):
- def __init__(self, name, rows=None, cols=None, **kws):
- apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws)
-
-class RadioButton(InputObj):
- def __init__(self, name, value, checked=0, **kws):
- apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws)
-
-class CheckBox(InputObj):
- def __init__(self, name, value, checked=0, **kws):
- apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws)
-
-class VerticalSpacer:
- def __init__(self, size=10):
- self.size = size
- def Format(self, indent=0):
- output = '<spacer type="vertical" height="%d">' % self.size
- return output
-
-class WidgetArray:
- Widget = None
-
- def __init__(self, name, button_names, checked, horizontal, values):
- self.name = name
- self.button_names = button_names
- self.checked = checked
- self.horizontal = horizontal
- self.values = values
- assert len(values) == len(button_names)
- # Don't assert `checked' because for RadioButtons it is a scalar while
- # for CheckedBoxes it is a vector. Subclasses will assert length.
-
- def ischecked(self, i):
- raise NotImplemented
-
- def Format(self, indent=0):
- t = Table(cellspacing=5)
- items = []
- for i, name, value in zip(range(len(self.button_names)),
- self.button_names,
- self.values):
- ischecked = (self.ischecked(i))
- item = self.Widget(self.name, value, ischecked).Format() + name
- items.append(item)
- if not self.horizontal:
- t.AddRow(items)
- items = []
- if self.horizontal:
- t.AddRow(items)
- return t.Format(indent)
-
-class RadioButtonArray(WidgetArray):
- Widget = RadioButton
-
- def __init__(self, name, button_names, checked=None, horizontal=1,
- values=None):
- if values is None:
- values = range(len(button_names))
- # BAW: assert checked is a scalar...
- WidgetArray.__init__(self, name, button_names, checked, horizontal,
- values)
-
- def ischecked(self, i):
- return self.checked == i
-
-class CheckBoxArray(WidgetArray):
- Widget = CheckBox
-
- def __init__(self, name, button_names, checked=None, horizontal=0,
- values=None):
- if checked is None:
- checked = [0] * len(button_names)
- else:
- assert len(checked) == len(button_names)
- if values is None:
- values = range(len(button_names))
- WidgetArray.__init__(self, name, button_names, checked, horizontal,
- values)
-
- def ischecked(self, i):
- return self.checked[i]
-
-class UnorderedList(Container):
- def Format(self, indent=0):
- spaces = ' ' * indent
- output = '\n%s<ul>\n' % spaces
- for item in self.items:
- output = output + '%s<li>%s\n' % \
- (spaces, HTMLFormatObject(item, indent + 2))
- output = output + '%s</ul>\n' % spaces
- return output
-
-class OrderedList(Container):
- def Format(self, indent=0):
- spaces = ' ' * indent
- output = '\n%s<ol>\n' % spaces
- for item in self.items:
- output = output + '%s<li>%s\n' % \
- (spaces, HTMLFormatObject(item, indent + 2))
- output = output + '%s</ol>\n' % spaces
- return output
-
-class DefinitionList(Container):
- def Format(self, indent=0):
- spaces = ' ' * indent
- output = '\n%s<dl>\n' % spaces
- for dt, dd in self.items:
- output = output + '%s<dt>%s\n<dd>%s\n' % \
- (spaces, HTMLFormatObject(dt, indent+2),
- HTMLFormatObject(dd, indent+2))
- output = output + '%s</dl>\n' % spaces
- return output
-
-
-
-# Logo constants
-#
-# These are the URLs which the image logos link to. The Mailman home page now
-# points at the gnu.org site instead of the www.list.org mirror.
-
-PYTHON_URL = 'http://www.python.org/'
-GNU_URL = 'http://www.gnu.org/'
-
-# The names of the image logo files. These are concatentated onto
-# config.IMAGE_LOGOS (not urljoined).
-DELIVERED_BY = 'mailman.jpg'
-PYTHON_POWERED = 'PythonPowered.png'
-GNU_HEAD = 'gnu-head-tiny.jpg'
-
-
-def MailmanLogo():
- t = Table(border=0, width='100%')
- if config.IMAGE_LOGOS:
- def logo(file):
- return config.IMAGE_LOGOS + file
- mmlink = '<img src="%s" alt="Delivered by Mailman" border=0>' \
- '<br>version %s' % (logo(DELIVERED_BY), version.VERSION)
- pylink = '<img src="%s" alt="Python Powered" border=0>' % \
- logo(PYTHON_POWERED)
- gnulink = '<img src="%s" alt="GNU\'s Not Unix" border=0>' % \
- logo(GNU_HEAD)
- t.AddRow([mmlink, pylink, gnulink])
- else:
- # use only textual links
- version = version.VERSION
- mmlink = Link(config.MAILMAN_URL,
- _('Delivered by Mailman<br>version %(version)s'))
- pylink = Link(PYTHON_URL, _('Python Powered'))
- gnulink = Link(GNU_URL, _("Gnu's Not Unix"))
- t.AddRow([mmlink, pylink, gnulink])
- return t
-
-
-class SelectOptions:
- def __init__(self, varname, values, legend,
- selected=0, size=1, multiple=None):
- self.varname = varname
- self.values = values
- self.legend = legend
- self.size = size
- self.multiple = multiple
- # we convert any type to tuple, commas are needed
- if not multiple:
- if isinstance(selected, int):
- self.selected = (selected,)
- elif isinstance(selected, tuple):
- self.selected = (selected[0],)
- elif isinstance(selected, list):
- self.selected = (selected[0],)
- else:
- self.selected = (0,)
-
- def Format(self, indent=0):
- spaces = " " * indent
- items = min( len(self.values), len(self.legend) )
-
- # jcrey: If there is no argument, we return nothing to avoid errors
- if items == 0:
- return ""
-
- text = "\n" + spaces + "<Select name=\"%s\"" % self.varname
- if self.size > 1:
- text = text + " size=%d" % self.size
- if self.multiple:
- text = text + " multiple"
- text = text + ">\n"
-
- for i in range(items):
- if i in self.selected:
- checked = " Selected"
- else:
- checked = ""
-
- opt = " <option value=\"%s\"%s> %s </option>" % (
- self.values[i], checked, self.legend[i])
- text = text + spaces + opt + "\n"
-
- return text + spaces + '</Select>'