summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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.
-"""