diff options
Diffstat (limited to 'src')
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([' ', ' ']) - 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> <br>') - if config.OWNERS_CAN_DELETE_THEIR_OWN_LISTS: - otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'), - _('Delete this mailing list')).Format() + - _(' (requires confirmation)<br> <br>')) - otherlinks.AddItem(Link('%s/logout' % adminurl, - # BAW: What I really want is a blank line, but - # adding an 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([' ', ' ']) - # 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 = ' '*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([' ', ' ']) - 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((_('<blank line>'), - _('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 = ' ' * 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() + - ' ' + - _('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() + - ' ' + - _('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() + \ - ' ' + _('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() + - ' ' + - _('Preserve messages for the site administrator') - ]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - left.AddRow([ - CheckBox('senderforward-' + qsender, 1).Format() + - ' ' + - _('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() + - ' ' + - _("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() + - ' ' + - _('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() + - ' ' + - _("""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([' ', ' ']) - 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([' ', Bold(_('Size:')), str(size) + _(' bytes')]) - if reason: - reason = _(reason) - else: - reason = _('not available') - t.AddRow([' ', Bold(_('Reason:')), reason]) - # Include the date we received the message, if available - when = msgdata.get('received_time') - if when: - t.AddRow([' ', 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=' '*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([' ', - CheckBox('preserve-%d' % id, 'on', 0).Format() + - ' ' + _('Preserve message for site administrator') - ]) - t.AddRow([' ', - CheckBox('forward-%d' % id, 'on', 0).Format() + - ' ' + _('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'<\1>', 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([' ', ' ']) - 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 = '<undetermined>' - - # 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 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('<', '<') - 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 <br> - 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'<\1>', 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 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 Management...') - - def GetConfigSubCategories(self, category): - if category == 'members': - return [('list', _('Membership List')), - ('add', _('Mass Subscription')), - ('remove', _('Mass 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 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 rules')), - ('sender', _('Sender filters')), - ('recipient', _('Recipient filters')), - ('spam', _('Spam 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<->News 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') - + " ") - 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>' |
