summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbwarsaw2006-07-16 22:03:56 +0000
committerbwarsaw2006-07-16 22:03:56 +0000
commit7958b4d57dd75cb25894da523440c4dde78a0fa1 (patch)
tree829382c297bf2e84bfe84ec4718bd7f49cd95c29
parent2863de361a5a597006d7ce57dc58f9c538021855 (diff)
downloadmailman-7958b4d57dd75cb25894da523440c4dde78a0fa1.tar.gz
mailman-7958b4d57dd75cb25894da523440c4dde78a0fa1.tar.zst
mailman-7958b4d57dd75cb25894da523440c4dde78a0fa1.zip
Add a framework for easier use of alternative MemberAdaptor implementations.
Also add an experimental (and currently non-functioning) SQLAlchemy implementation. The MemberAdaptor.py interface has been updated in a couple of ways. First, a "transaction interface" has been added so that Mailman can properly sync with the member adaptor. Newly supported methods are load(), lock(), save(), and unlock() and these correspond to methods in the MailList object. Second, __init__() is officially documented to take a MailList instance and nothing else. Third, some of the existing docstrings were incorrect w.r.t. the OldStyleMemberships implementation (such as rasing BadPasswordError in some cases). Most of these should not be the responsibility of the MemberAdaptor, so the docstrings have been updated. Test cases have been added and a new Defaults.py.in variable called MEMBER_ADAPTOR_CLASS has been added which names the class to use. Of course OldStyleMemberships are named by default. There's also a SQLALCHEMY_ENGINE_URL variable for use with the experimental member adaptor. Fix a bug in Configuration where if the etc/mailman.cfg file wasn't found and the mm_cfg.py file was used as a fallback, it would blow away the original namespace copied from Defaults.py.in. This wasn't a problem until we started adding additional names to that namespace, such as 'add_domain'.
-rw-r--r--Mailman/Defaults.py.in21
-rw-r--r--Mailman/MailList.py22
-rw-r--r--Mailman/MemberAdaptor.py57
-rw-r--r--Mailman/OldStyleMemberships.py15
-rw-r--r--Mailman/SAMemberships.py330
-rw-r--r--Mailman/configuration.py2
-rw-r--r--Mailman/testing/base.py9
-rw-r--r--Mailman/testing/emailbase.py1
-rw-r--r--misc/mailman.cfg.sample33
9 files changed, 440 insertions, 50 deletions
diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in
index e843bc249..0c5a6f648 100644
--- a/Mailman/Defaults.py.in
+++ b/Mailman/Defaults.py.in
@@ -97,6 +97,27 @@ HTML_TO_PLAIN_TEXT_COMMAND = '/usr/bin/lynx -dump %(filename)s'
#####
+# Database options
+#####
+
+# Specify the name of the membership adaptor class that implements the
+# MemberAdaptor interface you want to use. The first is traditional
+# pickle-based adapter that was standard for Mailman 2.1 and earlier.
+MEMBER_ADAPTOR_CLASS = 'Mailman.OldStyleMemberships.OldStyleMemberships'
+
+# This is the SQLAlchemy-based adaptor which lets you store all membership
+# data in any of several supported RDBMs.
+#MEMBER_ADAPTOR_CLASS = 'Mailman.SAMemberships.SAMemberships'
+
+# If you choose the SAMemberships adaptor, set this variable to specify the
+# connection url to the backend database engine. Specify the placeholder
+# $listdir for the directory that list data is stored in, and $listname for
+# the name of the mailing list.
+SQLALCHEMY_ENGINE_URL = 'sqlite:///$listdir/members.db'
+
+
+
+#####
# Virtual domains
#####
diff --git a/Mailman/MailList.py b/Mailman/MailList.py
index 953ac02d7..c834ab9c6 100644
--- a/Mailman/MailList.py
+++ b/Mailman/MailList.py
@@ -68,10 +68,10 @@ from Mailman import Gui
from Mailman import i18n
from Mailman import MemberAdaptor
from Mailman import Message
-from Mailman.OldStyleMemberships import OldStyleMemberships
_ = i18n._
+DOT = '.'
EMPTYSTRING = ''
OR = '|'
@@ -89,7 +89,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
#
# A MailList object's basic Python object model support
#
- def __init__(self, name=None, lock=1):
+ def __init__(self, name=None, lock=True):
# No timeout by default. If you want to timeout, open the list
# unlocked, then lock explicitly.
#
@@ -99,8 +99,13 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
baseclass.__init__(self)
# Initialize volatile attributes
self.InitTempVars(name)
- # Default membership adaptor class
- self._memberadaptor = OldStyleMemberships(self)
+ # Attach a membership adaptor instance.
+ parts = config.MEMBER_ADAPTOR_CLASS.split(DOT)
+ adaptor_class = parts.pop()
+ adaptor_module = DOT.join(parts)
+ __import__(adaptor_module)
+ mod = sys.modules[adaptor_module]
+ self._memberadaptor = getattr(mod, adaptor_class)(self)
# This extension mechanism allows list-specific overrides of any
# method (well, except __init__(), InitTempVars(), and InitVars()
# I think). Note that fullpath() will return None when we're creating
@@ -157,6 +162,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
#
def Lock(self, timeout=0):
self.__lock.lock(timeout)
+ self._memberadaptor.lock()
# Must reload our database for consistency. Watch out for lists that
# don't exist.
try:
@@ -166,7 +172,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
raise
def Unlock(self):
- self.__lock.unlock(unconditionally=1)
+ self.__lock.unlock(unconditionally=True)
+ self._memberadaptor.unlock()
def Locked(self):
return self.__lock.locked()
@@ -280,9 +287,10 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
lifetime=config.LIST_LOCK_LIFETIME)
# XXX FIXME Sometimes name is fully qualified, sometimes it's not.
if name and '@' in name:
- self._internal_name, email_host = name.split('@', 1)
+ self._internal_name, self.host_name = name.split('@', 1)
else:
self._internal_name = name
+ self.host_name = config.DEFAULT_EMAIL_HOST
if name:
self._full_path = os.path.join(config.LIST_DATA_DIR, name)
else:
@@ -571,6 +579,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
# the lock (which is a serious problem!). TBD: do we need to be more
# defensive?
self.__lock.refresh()
+ self._memberadaptor.save()
# copy all public attributes to serializable dictionary
dict = {}
for key, value in self.__dict__.items():
@@ -639,6 +648,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
fqdn_listname = self.fqdn_listname
if not Utils.list_exists(fqdn_listname):
raise Errors.MMUnknownListError
+ self._memberadaptor.load()
# We first try to load config.pck, which contains the up-to-date
# version of the database. If that fails, perhaps because it's
# corrupted or missing, we'll try to load the backup file
diff --git a/Mailman/MemberAdaptor.py b/Mailman/MemberAdaptor.py
index bcaf1ace0..2b5d248c5 100644
--- a/Mailman/MemberAdaptor.py
+++ b/Mailman/MemberAdaptor.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2001-2003 by the Free Software Foundation, Inc.
+# Copyright (C) 2001-2006 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
@@ -12,7 +12,8 @@
#
# 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.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
"""This is an interface to list-specific membership information.
@@ -56,6 +57,47 @@ BYBOUNCE = 4 # disabled by bounces
class MemberAdaptor:
+ def __init__(self, mlist):
+ """Create the adaptor, attached to the given mailing list."""
+ raise NotImplementedError
+
+ #
+ # Transaction interface
+ #
+ def load(self):
+ """Called when the mailing list data is loaded.
+
+ The adaptor should refresh/clear all in-memory objects. The reason is
+ that other processes may have changed the database since the last time
+ the adaptor has been accessed.
+ """
+
+ def lock(self):
+ """Called when the mailing list is locked.
+
+ This should correspond to a database 'begin'.
+ """
+ raise NotImplementedError
+
+ def save(self):
+ """Called when a locked mailing list is saved.
+
+ This should correspond to a database 'commit' except that the adaptor
+ should record that the database has been saved, and this flag should
+ be checked in any subsequent unlock() calls.
+ """
+ raise NotImplementedError
+
+ def unlock(self):
+ """Called when a locked mailing list is unlocked.
+
+ Generally, this can be no-op'd if save() was previously called, but if
+ not, then a database 'rollback' should be issued. There is no
+ explicit transaction rollback operation in the MailList API, but
+ processes will unlock without saving to mean the same thing.
+ """
+ raise NotImplementedError
+
#
# The readable interface
#
@@ -181,6 +223,9 @@ class MemberAdaptor:
def getDeliveryStatusChangeTime(self, member):
"""Return the time of the last disabled delivery status change.
+ Time is returned in float seconds since the epoch. XXX this should be
+ a Python datetime.
+
If the current delivery status is ENABLED, the status change time will
be zero. If member is not a member of the list, raise
NotAMemberError.
@@ -254,7 +299,7 @@ class MemberAdaptor:
"""
raise NotImplementedError
- def changeMemberAddress(self, memberkey, newaddress, nodelete=0):
+ def changeMemberAddress(self, memberkey, newaddress, nodelete=False):
"""Change the address for the member KEY.
memberkey will be a KEY, not an LCE. newaddress should be the
@@ -273,8 +318,6 @@ class MemberAdaptor:
"""Set the password for member LCE/KEY.
If member does not refer to a valid member, raise NotAMemberError.
- Also raise BadPasswordError if the password is illegal (e.g. too
- short or easily guessed via a dictionary attack).
"""
raise NotImplementedError
@@ -282,8 +325,6 @@ class MemberAdaptor:
"""Set the language for the member LCE/KEY.
If member does not refer to a valid member, raise NotAMemberError.
- Also raise BadLanguageError if the language is invalid (e.g. the list
- is not configured to support the given language).
"""
raise NotImplementedError
@@ -294,8 +335,6 @@ class MemberAdaptor:
Default.py, and value is a boolean.
If member does not refer to a valid member, raise NotAMemberError.
- Also raise BadOptionError if the flag does not refer to a valid
- option.
"""
raise NotImplementedError
diff --git a/Mailman/OldStyleMemberships.py b/Mailman/OldStyleMemberships.py
index 04f3d1a7c..8961c2825 100644
--- a/Mailman/OldStyleMemberships.py
+++ b/Mailman/OldStyleMemberships.py
@@ -47,6 +47,17 @@ class OldStyleMemberships(MemberAdaptor.MemberAdaptor):
self.__mlist = mlist
#
+ # Transaction interface
+ #
+
+ # These are all no-op'd because the data is all attached to and managed by
+ # the MailList object.
+ def load(self): pass
+ def lock(self): pass
+ def save(self): pass
+ def unlock(self): pass
+
+ #
# Read interface
#
def getMembers(self):
@@ -271,6 +282,10 @@ class OldStyleMemberships(MemberAdaptor.MemberAdaptor):
# toggling the Digests flag, then we need to move their entry from
# mlist.members to mlist.digest_members or vice versa. Blarg. Do
# this before the flag setting below in case it fails.
+ #
+ # XXX Adaptors should not be doing these semantic integrity checks,
+ # but for backward compatibility I'm not changing this. New adaptors
+ # should not mimic this behavior.
if flag == mm_cfg.Digests:
if value:
# Be sure the list supports digest delivery
diff --git a/Mailman/SAMemberships.py b/Mailman/SAMemberships.py
new file mode 100644
index 000000000..dec76fe58
--- /dev/null
+++ b/Mailman/SAMemberships.py
@@ -0,0 +1,330 @@
+# Copyright (C) 2006 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.
+
+"""An experimental SQLAlchemy-based membership adaptor."""
+
+# XXX THIS FILE DOES NOT YET WORK!
+
+import os
+import re
+import time
+
+from sqlalchemy import *
+from string import Template
+
+from Mailman import Defaults
+from Mailman import Errors
+from Mailman import MemberAdaptor
+from Mailman import Utils
+from Mailman.configuration import config
+
+NUL = '\0'
+
+
+
+# Python classes representing the data in the SQLAlchemy database. These will
+# be associated with tables via object mappers.
+
+class Member(object):
+ def __init__(self, mlist,
+ address, realname=None, password=None,
+ digests_p=False, language=None):
+ self.lckey = address.lower()
+ self.address = address
+ self.realname = realname
+ self.password = password or Utils.MakeRandomPassword()
+ self.language = language or mlist.preferred_language
+ self.digests_p = digests_p
+ self.options = mlist.new_member_options
+ self.topics = ''
+ self.status = MemberAdaptor.ENABLED
+ # XXX This should really be a datetime
+ self.disable_time = 0
+
+
+
+_table = None
+_metadata = None
+_mapper = None
+
+def create_table():
+ global _table, _metadata, _mapper
+
+ if _table:
+ return
+ _metadata = MetaData('table metadata')
+ _table = Table(
+ 'members', _metadata,
+ Column('member_id', Integer, primary_key=True),
+ Column('lckey', Unicode(), index=True, nullable=False),
+ Column('address', Unicode(), index=True, nullable=False),
+ Column('realname', Unicode()),
+ Column('password', Unicode()),
+ Column('language', String(2)),
+ Column('digest', Boolean),
+ Column('options', Integer),
+ Column('status', Integer),
+ Column('disable_time', Float),
+ )
+ _mapper = mapper(Member, _table)
+
+
+
+class SAMemberships(MemberAdaptor.MemberAdaptor):
+ def __init__(self, mlist):
+ self._mlist = mlist
+ self._metadata = None
+ self._session = None
+ self._txn = None
+
+ def _connect(self):
+ create_table()
+ # We cannot connect in the __init__() because our adaptor requires the
+ # fqdn_listname to exist. In MailList.Create() that won't be the case.
+ #
+ # Calculate the engine url, expanding placeholder variables.
+ engine_url = Template(config.SQLALCHEMY_ENGINE_URL).substitute(
+ {'listname' : self._mlist.fqdn_listname,
+ 'listdir' : os.path.join(config.LIST_DATA_DIR,
+ self._mlist.fqdn_listname),
+ })
+ print 'engine_url:', engine_url
+ self._engine = create_engine(engine_url)
+ self._session = create_session(bind_to=self._engine)
+ self._session.bind_table(_table, self._engine)
+ self._session.bind_mapper(_mapper, self._engine)
+ # XXX There must be a better way to figure out whether the tables need
+ # to be created or not.
+ try:
+ _table.create()
+ except exceptions.SQLError:
+ pass
+
+ #
+ # The transaction interface
+ #
+
+ def load(self):
+ if self._session is None:
+ self._connect()
+ assert self._txn is None
+ self._session.clear()
+ self._txn = self._session.create_transaction()
+
+ def lock(self):
+ pass
+
+ def save(self):
+ # When a MailList is first Create()'d, the load() callback doesn't get
+ # called, so there will be no transaction.
+ if self._txn:
+ self._txn.commit()
+ self._txn = None
+
+ def unlock(self):
+ if self._txn is not None:
+ # The MailList has not been saved, but it is being unlocked, so
+ # throw away all pending changes.
+ self._txn.rollback()
+ self._txn = None
+
+ #
+ # The readable interface
+ #
+
+ def getMembers(self):
+ return [m.lckey for m in self._session.query(Member).select_by()]
+
+ def getRegularMemberKeys(self):
+ query = self._session.query(Member)
+ return [m.lckey for m in query.select(Member.c.digests_p == False)]
+
+ def getDigestMemberKeys(self):
+ query = self._session.query(Member)
+ return [m.lckey for m in query.select(Member.c.digests_p == True)]
+
+ def _get_member(self, member):
+ members = self._session.query(Member).select_by(lckey=member.lower())
+ if not members:
+ return None
+ assert len(members) == 1
+ return members[0]
+
+ def _get_member_strict(self, member):
+ member_obj = self._get_member(member)
+ if not member_obj:
+ raise Errors.NotAMemberError(member)
+ return member_obj
+
+ def isMember(self, member):
+ return bool(self._get_member(member))
+
+ def getMemberKey(self, member):
+ self._get_member_strict(member)
+ return member.lower()
+
+ def getMemberCPAddress(self, member):
+ return self._get_member(member).address
+
+ def getMemberCPAddresses(self, members):
+ query = self._session.query(Member)
+ return [user.address for user in query.select(
+ in_(Member.c.lckey, [m.lower() for m in members]))]
+
+ def getMemberPassword(self, member):
+ return self._get_member_strict(member).password
+
+ def authenticateMember(self, member, response):
+ return self._get_member_strict(member).password == response
+
+ def getMemberLanguage(self, member):
+ member = self._get_member(member)
+ if member and member.language in self._mlist.GetAvailableLanguages():
+ return member.language
+ return self._mlist.preferred_language
+
+ def getMemberOption(self, member, flag):
+ return bool(self._get_member_strict(member).options & flag)
+
+ def getMemberName(self, member):
+ return self._get_member_strict(member).realname
+
+ def getMemberTopics(self, member):
+ topics = self._get_member_strict(member).topics
+ if not topics:
+ return []
+ return topics.split(NUL)
+
+ def getDeliveryStatus(self, member):
+ return self._get_member_strict(member).status
+
+ def getDeliveryStatusChangeTime(self, member):
+ member = self._get_member_strict(member)
+ if member.status == MemberAdaptor.ENABLED:
+ return 0
+ return member.disable_time
+
+ def getDeliveryStatusMembers(self, status=(MemberAdaptor.UNKNOWN,
+ MemberAdaptor.BYUSER,
+ MemberAdaptor.BYADMIN,
+ MemberAdaptor.BYBOUNCE)):
+ query = self._session.query(Member)
+ return [user.lckey for user in query.select(
+ in_(Member.c.status, status))]
+
+ def getBouncingMembers(self):
+ "XXX"
+
+ def getBounceInfo(self):
+ "XXX"
+
+ #
+ # The writable interface
+ #
+
+ def addNewMember(self, member, **kws):
+ assert self._mlist.Locked()
+ if self.isMember(member):
+ raise Errors.MMAlreadyAMember(member)
+ try:
+ new_member = Member(self._mlist, member, **kws)
+ self._session.save(new_member)
+ self._session.flush()
+ except TypeError:
+ # Transform exception to API specification
+ raise ValueError
+
+ def removeMember(self, memberkey):
+ assert self._mlist.Locked()
+ member = self._get_member_strict(memberkey)
+ self._session.delete(member)
+
+ def changeMemberAddress(self, memberkey, newaddress, nodelete=False):
+ assert self._mlist.Locked()
+ member = self._get_member_strict(memberkey)
+ # First, add the new member from the previous data
+ self.addNewMember(newaddress, member.realname, member.password,
+ member.digests_p, member.language)
+ new_member = self._get_member(newaddress)
+ assert new_member
+ new_member.options = member.options
+ new_member.topics = member.topics
+ new_member.status = MemberAdaptor.ENABLED
+ new_member.disable_time = 0
+ if not nodelete:
+ self._session.delete(member)
+
+ def setMemberPassword(self, member, password):
+ assert self._mlist.Locked()
+ self._get_member_strict(member).password = password
+
+ def setMemberLanguage(self, member, language):
+ assert self._mlist.Locked()
+ self._get_member_strict(member).language = language
+
+ def setMemberOption(self, member, flag, value):
+ assert self._mlist.Locked()
+ member = self._get_member_strict(member)
+ # XXX the OldStyleMemberships adaptor will raise CantDigestError,
+ # MustDigestError, AlreadyReceivingDigests, and
+ # AlreadyReceivingRegularDeliveries in certain cases depending on the
+ # configuration of the mailing list and the member's delivery status.
+ # These semantics are not defined in the API so to keep things simple,
+ # I am not reproducing them here. Ideally, adaptors should not be
+ # doing semantic integrity checks, but I'm also not going to change
+ # the OldStyleMemberships adaptor.
+ #
+ # We still need to handle digests differently, because they aren't
+ # really represented as a unique flag in the options bitfield.
+ if flag == Defaults.Digests:
+ member.digests_p = bool(value)
+ else:
+ if value:
+ member.options |= flag
+ else:
+ member.options &= ~flag
+
+ def setMemberName(self, member, realname):
+ assert self._mlist.Locked()
+ self._get_member_strict(member).realname = realname
+
+ def setMemberTopics(self, member, topics):
+ assert self._mlist.Locked()
+ # For simplicity, we represent a user's topics of interest as a
+ # null-joined string, which will be split properly by the accessor.
+ if not topics:
+ topics = None
+ else:
+ topics = NUL.join(topics)
+ self._get_member_strict(member).topics = topics
+
+ def setDeliveryStatus(self, member, status):
+ assert status in (MemberAdaptor.ENABLED, MemberAdaptor.UNKNOWN,
+ MemberAdaptor.BYUSER, MemberAdaptor.BYADMIN,
+ MemberAdaptor.BYBOUNCE)
+ assert self._mlist.Locked()
+ member = self._get_member_strict(member)
+ if status == MemberAdaptor.ENABLED:
+ # XXX zap bounce info
+ disable_time = 0
+ else:
+ disable_time = time.time()
+ member.disable_time = disable_time
+ member.status = status
+
+ def setBounceInfo(self, member, info):
+ "XXX"
diff --git a/Mailman/configuration.py b/Mailman/configuration.py
index 1f1c52f19..6ee94b911 100644
--- a/Mailman/configuration.py
+++ b/Mailman/configuration.py
@@ -58,7 +58,7 @@ class Configuration(object):
raise
# The file didn't exist, so try mm_cfg.py
from Mailman import mm_cfg
- ns = mm_cfg.__dict__.copy()
+ ns.update(mm_cfg.__dict__)
# Pull out the defaults
PREFIX = ns['PREFIX']
VAR_PREFIX = ns['VAR_PREFIX']
diff --git a/Mailman/testing/base.py b/Mailman/testing/base.py
index 589ab0abb..a65c62bdd 100644
--- a/Mailman/testing/base.py
+++ b/Mailman/testing/base.py
@@ -37,6 +37,9 @@ PERMISSIONS = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
class TestBase(unittest.TestCase):
def _configure(self, fp):
+## print >> fp, \
+## "MEMBER_ADAPTOR_CLASS = 'Mailman.SAMemberships.SAMemberships'"
+## config.MEMBER_ADAPTOR_CLASS = 'Mailman.SAMemberships.SAMemberships'
print >> fp, 'add_domain("example.com", "www.example.com")'
# Only add this domain once to the current process
if 'example.com' not in config.domains:
@@ -66,6 +69,11 @@ class TestBase(unittest.TestCase):
mlist = MailList.MailList()
mlist.Create('_xtest@example.com', 'owner@example.com', 'xxxxx')
mlist.Save()
+ # We need to reload the mailing list to ensure that the member
+ # adaptors are all sync'd up. This isn't strictly necessary with the
+ # OldStyleMemberships adaptor, but it may be required for other
+ # adaptors
+ mlist.Load()
# This leaves the list in a locked state
self._mlist = mlist
@@ -83,3 +91,4 @@ class TestBase(unittest.TestCase):
os.unlink(dir)
elif os.path.isdir(dir):
shutil.rmtree(dir)
+ os.unlink(self._config)
diff --git a/Mailman/testing/emailbase.py b/Mailman/testing/emailbase.py
index 48cb163e2..53b915abb 100644
--- a/Mailman/testing/emailbase.py
+++ b/Mailman/testing/emailbase.py
@@ -86,7 +86,6 @@ class EmailBase(TestBase):
else:
raise
TestBase.tearDown(self)
- os.remove(self._config)
def _readmsg(self):
global MSGTEXT
diff --git a/misc/mailman.cfg.sample b/misc/mailman.cfg.sample
index 4cc4ec280..dcf38e313 100644
--- a/misc/mailman.cfg.sample
+++ b/misc/mailman.cfg.sample
@@ -31,36 +31,3 @@ To use this, copy this file to $VAR_PREFIX/etc/mailman.cfg
Mailman's installation procedure will never overwrite mailman.cfg.
"""
-# -*- python -*-
-
-# Copyright (C) 2006 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.
-
-"""This module contains your site-specific settings.
-
-Use this to override the default settings in Mailman/Defaults.py. You only
-need to include those settings that you want to change, and unlike the old
-mm_cfg.py file, you do /not/ need to import Defaults. Its variables will
-automatically be available in this module's namespace.
-
-You should consult Defaults.py though for a complete listing of configuration
-variables that you can change.
-
-To use this, copy this file to $VAR_PREFIX/etc/mailman.cfg
-
-Mailman's installation procedure will never overwrite mailman.cfg.
-"""