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