summaryrefslogtreecommitdiff
path: root/Mailman
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman')
-rw-r--r--Mailman/Cgi/admin.py5
-rw-r--r--Mailman/Cgi/listinfo.py5
-rw-r--r--Mailman/Cgi/options.py2
-rw-r--r--Mailman/Commands/cmd_lists.py6
-rw-r--r--Mailman/Defaults.py.in45
-rw-r--r--Mailman/Errors.py30
-rw-r--r--Mailman/Gui/Language.py2
-rw-r--r--Mailman/Handlers/CleanseDKIM.py9
-rw-r--r--Mailman/Handlers/Scrubber.py24
-rw-r--r--Mailman/ListAdmin.py219
-rw-r--r--Mailman/LockFile.py10
-rw-r--r--Mailman/MailList.py215
-rw-r--r--Mailman/Makefile.in2
-rw-r--r--Mailman/Pending.py23
-rw-r--r--Mailman/Queue/LMTPRunner.py3
-rw-r--r--Mailman/Queue/MaildirRunner.py6
-rw-r--r--Mailman/Queue/Runner.py36
-rw-r--r--Mailman/Queue/Switchboard.py14
-rw-r--r--Mailman/Utils.py11
-rw-r--r--Mailman/bin/bumpdigests.py3
-rw-r--r--Mailman/bin/change_pw.py7
-rwxr-xr-xMailman/bin/checkdbs.py2
-rw-r--r--Mailman/bin/disabled.py3
-rw-r--r--Mailman/bin/export.py3
-rw-r--r--Mailman/bin/find_member.py6
-rw-r--r--Mailman/bin/gate_news.py2
-rw-r--r--Mailman/bin/genaliases.py3
-rw-r--r--Mailman/bin/list_lists.py5
-rw-r--r--Mailman/bin/list_owners.py3
-rw-r--r--Mailman/bin/nightly_gzip.py3
-rw-r--r--Mailman/bin/senddigests.py3
-rw-r--r--Mailman/bin/update.py128
-rw-r--r--Mailman/bin/withlist.py4
-rw-r--r--Mailman/configuration.py1
-rw-r--r--Mailman/constants.py44
-rw-r--r--Mailman/database/Makefile.in23
-rw-r--r--Mailman/database/__init__.py40
-rw-r--r--Mailman/database/dbcontext.py76
-rw-r--r--Mailman/database/listmanager.py81
-rw-r--r--Mailman/database/model/Makefile.in71
-rw-r--r--Mailman/database/model/__init__.py96
-rw-r--r--Mailman/database/model/address.py46
-rw-r--r--Mailman/database/model/language.py (renamed from Mailman/database/address.py)14
-rw-r--r--Mailman/database/model/mailinglist.py275
-rw-r--r--Mailman/database/model/profile.py46
-rw-r--r--Mailman/database/model/roster.py51
-rw-r--r--Mailman/database/model/rosterset.py41
-rw-r--r--Mailman/database/model/user.py50
-rw-r--r--Mailman/database/model/version.py25
-rw-r--r--Mailman/database/tables/Makefile.in71
-rw-r--r--Mailman/database/tables/__init__.py0
-rw-r--r--Mailman/database/tables/addresses.py45
-rw-r--r--Mailman/database/tables/languages.py (renamed from Mailman/database/languages.py)6
-rw-r--r--Mailman/database/tables/listdata.py (renamed from Mailman/database/listdata.py)22
-rw-r--r--Mailman/database/tables/profiles.py77
-rw-r--r--Mailman/database/tables/rosters.py59
-rw-r--r--Mailman/database/tables/versions.py (renamed from Mailman/database/version.py)4
-rw-r--r--Mailman/database/types.py40
-rw-r--r--Mailman/database/usermanager.py91
-rw-r--r--Mailman/docs/Makefile.in82
-rw-r--r--Mailman/docs/__init__.py0
-rw-r--r--Mailman/docs/addresses.txt144
-rw-r--r--Mailman/docs/mlist-addresses.txt85
-rw-r--r--Mailman/docs/mlist-rosters.txt118
-rw-r--r--Mailman/docs/use-listmanager.txt124
-rw-r--r--Mailman/docs/use-usermanager.txt100
-rw-r--r--Mailman/docs/users.txt180
-rw-r--r--Mailman/enum.py132
-rw-r--r--Mailman/ext/Makefile.in71
-rw-r--r--Mailman/ext/__init__.py0
-rw-r--r--Mailman/initialize.py13
-rw-r--r--Mailman/interfaces/Makefile.in71
-rw-r--r--Mailman/interfaces/__init__.py46
-rw-r--r--Mailman/interfaces/address.py44
-rw-r--r--Mailman/interfaces/listmanager.py61
-rw-r--r--Mailman/interfaces/mailinglist.py28
-rw-r--r--Mailman/interfaces/manager.py57
-rw-r--r--Mailman/interfaces/mlistdigests.py66
-rw-r--r--Mailman/interfaces/mlistemail.py78
-rw-r--r--Mailman/interfaces/mlistid.py46
-rw-r--r--Mailman/interfaces/mlistrequest.py29
-rw-r--r--Mailman/interfaces/mlistrosters.py96
-rw-r--r--Mailman/interfaces/mliststats.py38
-rw-r--r--Mailman/interfaces/mlistweb.py39
-rw-r--r--Mailman/interfaces/permissions.py28
-rw-r--r--Mailman/interfaces/profile.py53
-rw-r--r--Mailman/interfaces/roster.py42
-rw-r--r--Mailman/interfaces/rosterset.py47
-rw-r--r--Mailman/interfaces/user.py59
-rw-r--r--Mailman/interfaces/usermanager.py82
-rw-r--r--Mailman/loginit.py3
-rw-r--r--Mailman/passwords.py2
-rw-r--r--Mailman/testing/inmemory.py92
-rw-r--r--Mailman/testing/test_address.py30
-rw-r--r--Mailman/testing/test_enum.py125
-rw-r--r--Mailman/testing/test_mlist_addresses.py28
-rw-r--r--Mailman/testing/test_mlist_rosters.py30
-rw-r--r--Mailman/testing/test_use_listmanager.py28
-rw-r--r--Mailman/testing/test_use_usermanager.py28
-rw-r--r--Mailman/testing/test_user.py30
-rw-r--r--Mailman/testing/testing.cfg.in1
101 files changed, 3753 insertions, 940 deletions
diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py
index ae9850fba..553f26656 100644
--- a/Mailman/Cgi/admin.py
+++ b/Mailman/Cgi/admin.py
@@ -182,10 +182,7 @@ def admin_overview(msg=''):
bgcolor=mm_cfg.WEB_HEADER_COLOR)
# Skip any mailing list that isn't advertised.
advertised = []
- listnames = list(Utils.list_names())
- listnames.sort()
-
- for name in listnames:
+ for name in sorted(config.list_manager.names):
mlist = MailList.MailList(name, lock=False)
if mlist.advertised:
if hostname not in mlist.web_page_url:
diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py
index b43d10f91..13689b767 100644
--- a/Mailman/Cgi/listinfo.py
+++ b/Mailman/Cgi/listinfo.py
@@ -82,10 +82,7 @@ def listinfo_overview(msg=''):
# Skip any mailing lists that isn't advertised.
advertised = []
- listnames = list(Utils.list_names())
- listnames.sort()
-
- for name in listnames:
+ for name in sorted(config.list_manager.names):
mlist = MailList.MailList(name, lock=False)
if mlist.advertised:
if hostname not in mlist.web_page_url:
diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py
index f357bcf15..29f05cf7b 100644
--- a/Mailman/Cgi/options.py
+++ b/Mailman/Cgi/options.py
@@ -895,7 +895,7 @@ def loginpage(mlist, doc, user, lang):
def lists_of_member(mlist, user):
hostname = mlist.host_name
onlists = []
- for listname in Utils.list_names():
+ for listname in config.list_manager.names:
# The current list will always handle things in the mainline
if listname == mlist.internal_name():
continue
diff --git a/Mailman/Commands/cmd_lists.py b/Mailman/Commands/cmd_lists.py
index 10e8aad71..6d23d4745 100644
--- a/Mailman/Commands/cmd_lists.py
+++ b/Mailman/Commands/cmd_lists.py
@@ -21,8 +21,8 @@
"""
from Mailman import mm_cfg
-from Mailman import Utils
from Mailman.MailList import MailList
+from Mailman.configuration import config
from Mailman.i18n import _
@@ -43,10 +43,8 @@ def process(res, args):
return STOP
hostname = mlist.host_name
res.results.append(_('Public mailing lists at %(hostname)s:'))
- lists = Utils.list_names()
- lists.sort()
i = 1
- for listname in lists:
+ for listname in sorted(config.list_manager.names):
if listname == mlist.internal_name():
xlist = mlist
else:
diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in
index 7008be8f9..87d5db125 100644
--- a/Mailman/Defaults.py.in
+++ b/Mailman/Defaults.py.in
@@ -26,6 +26,9 @@
import os
+from munepy import Enum
+
+
def seconds(s): return s
def minutes(m): return m * 60
def hours(h): return h * 60 * 60
@@ -79,7 +82,7 @@ SITE_OWNER_ADDRESS = 'changeme@example.com'
DEFAULT_HOST_NAME = None
DEFAULT_URL = None
-HOME_PAGE = 'index.html'
+HOME_PAGE = 'index.html'
# 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
@@ -104,6 +107,11 @@ PASSWORD_SCHEME = 'ssha'
# Database options
#####
+# Initialization function for creating the IListManager, IUserManager, and
+# IMessageManager objects, as a Python dotted name. This function must take
+# zero arguments.
+MANAGERS_INIT_FUNCTION = 'Mailman.database.initialize'
+
# 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
@@ -467,6 +475,15 @@ NNTP_REWRITE_DUPLICATE_HEADERS = [
('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
+
# All `normal' messages which are delivered to the entire list membership go
# through this pipeline of handler modules. Lists themselves can override the
# global pipeline by defining a `pipeline' attribute.
@@ -525,6 +542,8 @@ OWNER_PIPELINE = [
# - 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
@@ -664,9 +683,9 @@ VERP_DELIVERY_INTERVAL = 0
# friendly Subject: on the message, but requires cooperation from the MTA.
# Format is like VERP_FORMAT above, but with the following substitutions:
#
-# %(addr)s -- the list-confirm mailbox will be set here
-# %(cookie)s -- the confirmation cookie will be set here
-VERP_CONFIRM_FORMAT = '%(addr)s+%(cookie)s'
+# $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
@@ -1295,6 +1314,24 @@ ReceiveNonmatchingTopics = 64
Moderate = 128
DontReceiveDuplicates = 256
+
+class DeliveryMode(Enum):
+ # Non-digest delivery
+ Regular = 1
+ # Digest delivery modes
+ MIME = 2
+ Plain = 3
+
+
+class DeliveryStatus(Enum):
+ Enabled = 0
+ # Disabled reason
+ Unknown = 1
+ ByUser = 2
+ ByAdmin = 3
+ ByBounce = 4
+
+
# A mapping between short option tags and their flag
OPTINFO = {'hide' : ConcealSubscription,
'nomail' : DisableDelivery,
diff --git a/Mailman/Errors.py b/Mailman/Errors.py
index 10081f08d..6b5abac5a 100644
--- a/Mailman/Errors.py
+++ b/Mailman/Errors.py
@@ -212,3 +212,33 @@ class BadPasswordSchemeError(PasswordError):
def __str__(self):
return 'A bad password scheme was given: %s' % self.scheme_name
+
+
+
+class UserError(MailmanError):
+ """A general user-related error occurred."""
+
+
+class RosterError(UserError):
+ """A roster-related error occurred."""
+
+
+class RosterExistsError(RosterError):
+ """The named roster already exists."""
+
+
+
+class AddressError(MailmanError):
+ """A general address-related error occurred."""
+
+
+class ExistingAddressError(AddressError):
+ """The given email address already exists."""
+
+
+class AddressAlreadyLinkedError(AddressError):
+ """The address is already linked to a user."""
+
+
+class AddressNotLinkedError(AddressError):
+ """The address is not linked to the user."""
diff --git a/Mailman/Gui/Language.py b/Mailman/Gui/Language.py
index 8f73a8b3f..7b7433232 100644
--- a/Mailman/Gui/Language.py
+++ b/Mailman/Gui/Language.py
@@ -23,7 +23,7 @@ from Mailman import Utils
from Mailman import i18n
from Mailman import mm_cfg
from Mailman.Gui.GUIBase import GUIBase
-from Mailman.database.languages import Language
+from Mailman.database.tables.languages import Language
_ = i18n._
diff --git a/Mailman/Handlers/CleanseDKIM.py b/Mailman/Handlers/CleanseDKIM.py
index 8abdbb972..9dee4fcb0 100644
--- a/Mailman/Handlers/CleanseDKIM.py
+++ b/Mailman/Handlers/CleanseDKIM.py
@@ -25,9 +25,12 @@ and it will also give the MTA the opportunity to regenerate valid keys
originating at the Mailman server for the outgoing message.
"""
+from Mailman.configuration import config
+
def process(mlist, msg, msgdata):
- del msg['domainkey-signature']
- del msg['dkim-signature']
- del msg['authentication-results']
+ if config.REMOVE_DKIM_HEADERS:
+ del msg['domainkey-signature']
+ del msg['dkim-signature']
+ del msg['authentication-results']
diff --git a/Mailman/Handlers/Scrubber.py b/Mailman/Handlers/Scrubber.py
index a7a825852..4c53b11ac 100644
--- a/Mailman/Handlers/Scrubber.py
+++ b/Mailman/Handlers/Scrubber.py
@@ -174,7 +174,19 @@ def process(mlist, msg, msgdata=None):
if ctype == 'text/plain':
# We need to choose a charset for the scrubbed message, so we'll
# arbitrarily pick the charset of the first text/plain part in the
- # message. Also get the RFC 3676 stuff from this part.
+ # message.
+ #
+ # Also get the RFC 3676 stuff from this part. This seems to
+ # work okay for scrub_nondigest. It will also work as far as
+ # scrubbing messages for the archive is concerned, but Pipermail
+ # doesn't pay any attention to the RFC 3676 parameters. The plain
+ # format digest is going to be a disaster in any case as some of
+ # messages will be format="flowed" and some not. ToDigest creates
+ # its own Content-Type: header for the plain digest which won't
+ # have RFC 3676 parameters. If the message Content-Type: headers
+ # are retained for display in the digest, the parameters will be
+ # there for information, but not for the MUA. This is the best we
+ # can do without having get_payload() process the parameters.
if charset is None:
charset = part.get_content_charset(lcset)
format = part.get_param('format')
@@ -318,7 +330,8 @@ URL: %(url)s
partcharset = part.get_content_charset('us-ascii')
try:
t = unicode(t, partcharset, 'replace')
- except (UnicodeError, LookupError, ValueError, TypeError):
+ except (UnicodeError, LookupError, ValueError, TypeError,
+ AssertionError):
# What is the cause to come this exception now ?
# Replace funny characters. We use errors='replace'.
u = unicode(t, 'ascii', 'replace')
@@ -331,6 +344,13 @@ URL: %(url)s
charsets.append(partcharset)
# Now join the text and set the payload
sep = _('-------------- next part --------------\n')
+ # The i18n separator is in the list's charset. Coerce it to the
+ # message charset.
+ try:
+ s = unicode(sep, lcset, 'replace')
+ sep = s.encode(charset, 'replace')
+ except (UnicodeError, LookupError, ValueError):
+ pass
rept = sep.join(text)
# Replace entire message with text and scrubbed notice.
# Try with message charsets and utf-8
diff --git a/Mailman/ListAdmin.py b/Mailman/ListAdmin.py
index bcef7d2c9..cadf0146e 100644
--- a/Mailman/ListAdmin.py
+++ b/Mailman/ListAdmin.py
@@ -24,6 +24,8 @@ Pending subscriptions which are requiring a user's confirmation are handled
elsewhere.
"""
+from __future__ import with_statement
+
import os
import time
import email
@@ -71,104 +73,99 @@ class ListAdmin:
self.next_request_id = 1
def InitTempVars(self):
- self.__db = None
- self.__filename = os.path.join(self.fullpath(), 'request.pck')
+ self._db = None
+ self._filename = os.path.join(self.full_path, 'request.pck')
- def __opendb(self):
- if self.__db is None:
+ def _opendb(self):
+ if self._db is None:
assert self.Locked()
try:
- fp = open(self.__filename)
- try:
- self.__db = cPickle.load(fp)
- finally:
- fp.close()
+ with open(self._filename) as fp:
+ self._db = cPickle.load(fp)
except IOError, e:
- if e.errno <> errno.ENOENT: raise
- self.__db = {}
+ if e.errno <> errno.ENOENT:
+ raise
+ self._db = {}
# put version number in new database
- self.__db['version'] = IGN, config.REQUESTS_FILE_SCHEMA_VERSION
+ self._db['version'] = IGN, config.REQUESTS_FILE_SCHEMA_VERSION
- def __closedb(self):
- if self.__db is not None:
+ def _closedb(self):
+ if self._db is not None:
assert self.Locked()
# Save the version number
- self.__db['version'] = IGN, config.REQUESTS_FILE_SCHEMA_VERSION
+ self._db['version'] = IGN, config.REQUESTS_FILE_SCHEMA_VERSION
# Now save a temp file and do the tmpfile->real file dance. BAW:
# should we be as paranoid as for the config.pck file? Should we
# use pickle?
- tmpfile = self.__filename + '.tmp'
- fp = open(tmpfile, 'w')
- try:
- cPickle.dump(self.__db, fp, 1)
+ tmpfile = self._filename + '.tmp'
+ with open(tmpfile, 'w') as fp:
+ cPickle.dump(self._db, fp, 1)
fp.flush()
os.fsync(fp.fileno())
- finally:
- fp.close()
- self.__db = None
+ self._db = None
# Do the dance
- os.rename(tmpfile, self.__filename)
+ os.rename(tmpfile, self._filename)
- def __nextid(self):
+ @property
+ def _next_id(self):
assert self.Locked()
while True:
+ missing = object()
next = self.next_request_id
self.next_request_id += 1
- if not self.__db.has_key(next):
- break
- return next
+ if self._db.setdefault(next, missing) is missing:
+ yield next
def SaveRequestsDb(self):
- self.__closedb()
+ self._closedb()
def NumRequestsPending(self):
- self.__opendb()
+ self._opendb()
# Subtract one for the version pseudo-entry
- return len(self.__db) - 1
+ return len(self._db) - 1
- def __getmsgids(self, rtype):
- self.__opendb()
- ids = [k for k, (op, data) in self.__db.items() if op == rtype]
- ids.sort()
+ def _getmsgids(self, rtype):
+ self._opendb()
+ ids = sorted([k for k, (op, data) in self._db.items() if op == rtype])
return ids
def GetHeldMessageIds(self):
- return self.__getmsgids(HELDMSG)
+ return self._getmsgids(HELDMSG)
def GetSubscriptionIds(self):
- return self.__getmsgids(SUBSCRIPTION)
+ return self._getmsgids(SUBSCRIPTION)
def GetUnsubscriptionIds(self):
- return self.__getmsgids(UNSUBSCRIPTION)
+ return self._getmsgids(UNSUBSCRIPTION)
def GetRecord(self, id):
- self.__opendb()
- type, data = self.__db[id]
+ self._opendb()
+ type, data = self._db[id]
return data
def GetRecordType(self, id):
- self.__opendb()
- type, data = self.__db[id]
+ self._opendb()
+ type, data = self._db[id]
return type
def HandleRequest(self, id, value, comment=None, preserve=None,
forward=None, addr=None):
- self.__opendb()
- rtype, data = self.__db[id]
+ self._opendb()
+ rtype, data = self._db[id]
if rtype == HELDMSG:
- status = self.__handlepost(data, value, comment, preserve,
- forward, addr)
+ status = self._handlepost(data, value, comment, preserve,
+ forward, addr)
elif rtype == UNSUBSCRIPTION:
- status = self.__handleunsubscription(data, value, comment)
+ status = self._handleunsubscription(data, value, comment)
else:
assert rtype == SUBSCRIPTION
- status = self.__handlesubscription(data, value, comment)
+ status = self._handlesubscription(data, value, comment)
if status <> DEFER:
# BAW: Held message ids are linked to Pending cookies, allowing
# the user to cancel their post before the moderator has approved
# it. We should probably remove the cookie associated with this
# id, but we have no way currently of correlating them. :(
- del self.__db[id]
+ del self._db[id]
def HoldMessage(self, msg, reason, msgdata={}):
# Make a copy of msgdata so that subsequent changes won't corrupt the
@@ -176,9 +173,9 @@ class ListAdmin:
# not be relevant when the message is resurrected.
msgdata = msgdata.copy()
# assure that the database is open for writing
- self.__opendb()
+ self._opendb()
# get the next unique id
- id = self.__nextid()
+ id = self._next_id
# get the message sender
sender = msg.get_sender()
# calculate the file name for the message text and write it to disk
@@ -187,8 +184,7 @@ class ListAdmin:
else:
ext = 'txt'
filename = 'heldmsg-%s-%d.%s' % (self.internal_name(), id, ext)
- fp = open(os.path.join(config.DATA_DIR, filename), 'w')
- try:
+ with open(os.path.join(config.DATA_DIR, filename), 'w') as fp:
if config.HOLD_MESSAGES_AS_PICKLES:
cPickle.dump(msg, fp, 1)
else:
@@ -196,8 +192,6 @@ class ListAdmin:
g(msg, 1)
fp.flush()
os.fsync(fp.fileno())
- finally:
- fp.close()
# save the information to the request database. for held message
# entries, each record in the database will be of the following
# format:
@@ -211,10 +205,10 @@ class ListAdmin:
#
msgsubject = msg.get('subject', _('(no subject)'))
data = time.time(), sender, msgsubject, reason, filename, msgdata
- self.__db[id] = (HELDMSG, data)
+ self._db[id] = (HELDMSG, data)
return id
- def __handlepost(self, record, value, comment, preserve, forward, addr):
+ def _handlepost(self, record, value, comment, preserve, forward, addr):
# For backwards compatibility with pre 2.0beta3
ptime, sender, subject, reason, filename, msgdata = record
path = os.path.join(config.DATA_DIR, filename)
@@ -225,24 +219,19 @@ class ListAdmin:
spamfile = DASH.join(parts)
# Preserve the message as plain text, not as a pickle
try:
- fp = open(path)
+ with open(path) as fp:
+ msg = cPickle.load(fp)
except IOError, e:
- if e.errno <> errno.ENOENT: raise
+ if e.errno <> errno.ENOENT:
+ raise
return LOST
- try:
- msg = cPickle.load(fp)
- finally:
- fp.close()
# Save the plain text to a .msg file, not a .pck file
outpath = os.path.join(config.SPAM_DIR, spamfile)
head, ext = os.path.splitext(outpath)
outpath = head + '.msg'
- outfp = open(outpath, 'w')
- try:
+ with open(outpath, 'w') as outfp:
g = Generator(outfp)
g(msg, 1)
- finally:
- outfp.close()
# Now handle updates to the database
rejection = None
fp = None
@@ -256,7 +245,8 @@ class ListAdmin:
try:
msg = readMessage(path)
except IOError, e:
- if e.errno <> errno.ENOENT: raise
+ if e.errno <> errno.ENOENT:
+ raise
return LOST
msg = readMessage(path)
msgdata['approved'] = 1
@@ -281,7 +271,7 @@ class ListAdmin:
elif value == config.REJECT:
# Rejected
rejection = 'Refused'
- self.__refuse(_('Posting of your message titled "%(subject)s"'),
+ self._refuse(_('Posting of your message titled "%(subject)s"'),
sender, comment or _('[No reason given]'),
lang=self.getMemberLanguage(sender))
else:
@@ -297,7 +287,8 @@ class ListAdmin:
try:
copy = readMessage(path)
except IOError, e:
- if e.errno <> errno.ENOENT: raise
+ if e.errno <> errno.ENOENT:
+ raise
raise Errors.LostHeldMessage(path)
# It's possible the addr is a comma separated list of addresses.
addrs = getaddresses([addr])
@@ -354,9 +345,9 @@ class ListAdmin:
def HoldSubscription(self, addr, fullname, password, digest, lang):
# Assure that the database is open for writing
- self.__opendb()
+ self._opendb()
# Get the next unique id
- id = self.__nextid()
+ id = self._next_id
# Save the information to the request database. for held subscription
# entries, each record in the database will be one of the following
# format:
@@ -367,7 +358,7 @@ class ListAdmin:
# the digest flag
# the user's preferred language
data = time.time(), addr, fullname, password, digest, lang
- self.__db[id] = (SUBSCRIPTION, data)
+ self._db[id] = (SUBSCRIPTION, data)
#
# TBD: this really shouldn't go here but I'm not sure where else is
# appropriate.
@@ -399,7 +390,7 @@ class ListAdmin:
elif value == config.DISCARD:
pass
elif value == config.REJECT:
- self.__refuse(_('Subscription request'), addr,
+ self._refuse(_('Subscription request'), addr,
comment or _('[No reason given]'),
lang=lang)
else:
@@ -413,16 +404,16 @@ class ListAdmin:
pass
# TBD: disgusting hack: ApprovedAddMember() can end up closing
# the request database.
- self.__opendb()
+ self._opendb()
return REMOVE
def HoldUnsubscription(self, addr):
# Assure the database is open for writing
- self.__opendb()
+ self._opendb()
# Get the next unique id
- id = self.__nextid()
+ id = self._next_id
# All we need to do is save the unsubscribing address
- self.__db[id] = (UNSUBSCRIPTION, addr)
+ self._db[id] = (UNSUBSCRIPTION, addr)
log.info('%s: held unsubscription request from %s',
self.internal_name(), addr)
# Possibly notify the administrator of the hold
@@ -444,14 +435,14 @@ class ListAdmin:
self.preferred_language)
msg.send(self, **{'tomoderators': 1})
- def __handleunsubscription(self, record, value, comment):
+ def _handleunsubscription(self, record, value, comment):
addr = record
if value == config.DEFER:
return DEFER
elif value == config.DISCARD:
pass
elif value == config.REJECT:
- self.__refuse(_('Unsubscription request'), addr, comment)
+ self._refuse(_('Unsubscription request'), addr, comment)
else:
assert value == config.UNSUBSCRIBE
try:
@@ -461,7 +452,7 @@ class ListAdmin:
pass
return REMOVE
- def __refuse(self, request, recip, comment, origmsg=None, lang=None):
+ def _refuse(self, request, recip, comment, origmsg=None, lang=None):
# As this message is going to the requestor, try to set the language
# to his/her language choice, if they are a member. Otherwise use the
# list's preferred language.
@@ -492,82 +483,16 @@ class ListAdmin:
subject, text, lang)
msg.send(self)
- def _UpdateRecords(self):
- # Subscription records have changed since MM2.0.x. In that family,
- # the records were of length 4, containing the request time, the
- # address, the password, and the digest flag. In MM2.1a2, they grew
- # an additional language parameter at the end. In MM2.1a4, they grew
- # a fullname slot after the address. This semi-public method is used
- # by the update script to coerce all subscription records to the
- # latest MM2.1 format.
- #
- # Held message records have historically either 5 or 6 items too.
- # These always include the requests time, the sender, subject, default
- # rejection reason, and message text. When of length 6, it also
- # includes the message metadata dictionary on the end of the tuple.
- #
- # In Mailman 2.1.5 we converted these files to pickles.
- filename = os.path.join(self.fullpath(), 'request.db')
- try:
- fp = open(filename)
- try:
- self.__db = marshal.load(fp)
- finally:
- fp.close()
- os.unlink(filename)
- except IOError, e:
- if e.errno <> errno.ENOENT: raise
- filename = os.path.join(self.fullpath(), 'request.pck')
- try:
- fp = open(filename)
- try:
- self.__db = cPickle.load(fp)
- finally:
- fp.close()
- except IOError, e:
- if e.errno <> errno.ENOENT: raise
- self.__db = {}
- for id, (op, info) in self.__db.items():
- if op == SUBSCRIPTION:
- if len(info) == 4:
- # pre-2.1a2 compatibility
- when, addr, passwd, digest = info
- fullname = ''
- lang = self.preferred_language
- elif len(info) == 5:
- # pre-2.1a4 compatibility
- when, addr, passwd, digest, lang = info
- fullname = ''
- else:
- assert len(info) == 6, 'Unknown subscription record layout'
- continue
- # Here's the new layout
- self.__db[id] = when, addr, fullname, passwd, digest, lang
- elif op == HELDMSG:
- if len(info) == 5:
- when, sender, subject, reason, text = info
- msgdata = {}
- else:
- assert len(info) == 6, 'Unknown held msg record layout'
- continue
- # Here's the new layout
- self.__db[id] = when, sender, subject, reason, text, msgdata
- # All done
- self.__closedb()
-
def readMessage(path):
# For backwards compatibility, we must be able to read either a flat text
# file or a pickle.
ext = os.path.splitext(path)[1]
- fp = open(path)
- try:
+ with open(path) as fp:
if ext == '.txt':
msg = email.message_from_file(fp, Message.Message)
else:
assert ext == '.pck'
msg = cPickle.load(fp)
- finally:
- fp.close()
return msg
diff --git a/Mailman/LockFile.py b/Mailman/LockFile.py
index b240ce5a4..d83cef7e2 100644
--- a/Mailman/LockFile.py
+++ b/Mailman/LockFile.py
@@ -321,6 +321,16 @@ class LockFile:
if self._owned:
self.finalize()
+ # Python 2.5 context manager protocol support.
+ def __enter__(self):
+ self.lock()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.unlock()
+ # Don't suppress any exception that might have occurred.
+ return False
+
# Use these only if you're transfering ownership to a child process across
# a fork. Use at your own risk, but it should be race-condition safe.
# _transfer_to() is called in the parent, passing in the pid of the child.
diff --git a/Mailman/MailList.py b/Mailman/MailList.py
index cb310dd74..343040157 100644
--- a/Mailman/MailList.py
+++ b/Mailman/MailList.py
@@ -36,8 +36,10 @@ 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
@@ -49,7 +51,8 @@ from Mailman import Version
from Mailman import database
from Mailman.UserDesc import UserDesc
from Mailman.configuration import config
-from Mailman.database.languages import Language
+from Mailman.database.tables.languages import Language
+from Mailman.interfaces import *
# Base classes
from Mailman import Pending
@@ -89,84 +92,49 @@ slog = logging.getLogger('mailman.subscribe')
class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
Archiver, Digester, SecurityManager, Bouncer, GatewayManager,
Autoresponder, TopicMgr, Pending.Pending):
- def __new__(cls, *args, **kws):
- # Search positional and keyword arguments to find the name of the
- # existing list that is being opened, with the latter taking
- # precedence. If no name can be found, then make sure there are no
- # arguments, otherwise it's an error.
- if 'name' in kws:
- listname = kws.pop('name')
- elif not args:
- if not kws:
- # We're probably being created from the ORM layer, so just let
- # the super class do its thing.
- return super(MailList, cls).__new__(cls, *args, **kws)
- raise ValueError("'name' argument required'")
- else:
- listname = args[0]
- fqdn_listname = Utils.fqdn_listname(listname)
- listname, hostname = Utils.split_listname(fqdn_listname)
- mlist = database.find_list(listname, hostname)
- if not mlist:
- raise Errors.MMUnknownListError(fqdn_listname)
- return mlist
- #
- # A MailList object's basic Python object model support
- #
- def __init__(self, name=None, lock=True, check_version=True):
- # No timeout by default. If you want to timeout, open the list
- # unlocked, then lock explicitly.
- #
+ 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 volatile attributes
- self.InitTempVars(name, check_version)
- # 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
- # the list, which will only happen when name is None.
- if name is None:
- return
- filename = os.path.join(self.fullpath(), 'extend.py')
- dict = {}
+ self.InitTempVars()
+ # Give the extension mechanism a chance to process this list.
try:
- execfile(filename, dict)
- except IOError, e:
- # Ignore missing files, but log other errors
- if e.errno == errno.ENOENT:
- pass
- else:
- elog.error('IOError reading list extension: %s', e)
+ from Mailman.ext import init_mlist
+ except ImportError:
+ pass
else:
- func = dict.get('extend')
- if func:
- func(self)
- if lock:
- # This will load the database.
- self.Lock()
- else:
- self.Load(name, check_version)
+ init_mlist(self)
def __getattr__(self, name):
+ missing = object()
if name.startswith('_'):
return super(MailList, self).__getattr__(name)
- # Because we're using delegation, we want to be sure that attribute
- # access to a delegated member function gets passed to the
- # sub-objects. This of course imposes a specific name resolution
- # order.
- try:
- return getattr(self._memberadaptor, name)
- except AttributeError:
- for guicomponent in self._gui:
- try:
- return getattr(guicomponent, name)
- except AttributeError:
- pass
- else:
- raise AttributeError(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
+ # Delegate to the member adapter next.
+ obj = getattr(self._memberadaptor, 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):
if self.Locked():
@@ -212,43 +180,63 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
#
# Useful accessors
#
- def internal_name(self):
- return self._internal_name
-
- def fullpath(self):
+ @property
+ def full_path(self):
return self._full_path
+
+
+ # IMailingListIdentity
+
@property
def fqdn_listname(self):
- return '%s@%s' % (self._internal_name, self.host_name)
+ return Utils.fqdn_listname(self._data.list_name, self._data.host_name)
+
+
+
+ # IMailingListAddresses
+
+ @property
+ def posting_address(self):
+ return self.fqdn_listname
@property
- def no_reply_address(self):
+ def noreply_address(self):
return '%s@%s' % (config.NO_REPLY_ADDRESS, self.host_name)
- def getListAddress(self, extra=None):
- if extra is None:
- return self.fqdn_listname
- return '%s-%s@%s' % (self.internal_name(), extra, self.host_name)
+ @property
+ def owner_address(self):
+ return '%s-owner@%s' % (self.list_name, self.host_name)
+
+ @property
+ def request_address(self):
+ return '%s-request@%s' % (self.list_name, self.host_name)
- # For backwards compatibility
- def GetBouncesEmail(self):
- return self.getListAddress('bounces')
+ @property
+ def bounces_address(self):
+ return '%s-bounces@%s' % (self.list_name, self.host_name)
- def GetOwnerEmail(self):
- return self.getListAddress('owner')
+ @property
+ def join_address(self):
+ return '%s-join@%s' % (self.list_name, self.host_name)
- def GetRequestEmail(self, cookie=''):
- if config.VERP_CONFIRMATIONS and cookie:
- return self.GetConfirmEmail(cookie)
- else:
- return self.getListAddress('request')
+ @property
+ def leave_address(self):
+ return '%s-leave@%s' % (self.list_name, self.host_name)
+
+ @property
+ def subscribe_address(self):
+ return '%s-subscribe@%s' % (self.list_name, self.host_name)
+
+ @property
+ def unsubscribe_address(self):
+ return '%s-unsubscribe@%s' % (self.list_name, self.host_name)
- def GetConfirmEmail(self, cookie):
- return config.VERP_CONFIRM_FORMAT % {
- 'addr' : '%s-confirm' % self.internal_name(),
- 'cookie': cookie,
- } + '@' + self.host_name
+ def confirm_address(self, cookie):
+ local_part = Template(config.VERP_CONFIRM_FORMAT).safe_substitute(
+ address = '%s-confirm' % self.list_name,
+ cookie = cookie)
+ return '%s@%s' % (local_part, self.host_name)
def GetConfirmJoinSubject(self, listname, cookie):
if config.VERP_CONFIRMATIONS and cookie:
@@ -303,12 +291,11 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
user = Utils.ObscureEmail(user)
return '%s/%s' % (url, urllib.quote(user.lower()))
-
#
# Instance and subcomponent initialization
#
- def InitTempVars(self, name, check_version=True):
+ def InitTempVars(self):
"""Set transient variables of this and inherited classes."""
# Because of the semantics of the database layer, it's possible that
# this method gets called more than once on an existing object. For
@@ -325,27 +312,9 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
__import__(adaptor_module)
mod = sys.modules[adaptor_module]
self._memberadaptor = getattr(mod, adaptor_class)(self)
- # The timestamp is set whenever we load the state from disk. If our
- # timestamp is newer than the modtime of the config.pck file, we don't
- # need to reload, otherwise... we do.
- self._timestamp = 0
- self._make_lock(name or '<site>')
- # XXX FIXME Sometimes name is fully qualified, sometimes it's not.
- if name:
- if '@' in name:
- self._internal_name, self.host_name = name.split('@', 1)
- self._full_path = os.path.join(config.LIST_DATA_DIR, name)
- else:
- self._internal_name = name
- self.host_name = config.DEFAULT_EMAIL_HOST
- if check_version:
- self._full_path = os.path.join(config.LIST_DATA_DIR,
- name + '@' +
- self.host_name)
- else:
- self._full_path = os.path.join(config.LIST_DATA_DIR, name)
- else:
- self._full_path = ''
+ self._make_lock(self.fqdn_listname)
+ self._full_path = os.path.join(config.LIST_DATA_DIR,
+ self.fqdn_listname)
# Only one level of mixin inheritance allowed
for baseclass in self.__class__.__bases__:
if hasattr(baseclass, 'InitTempVars'):
@@ -597,18 +566,9 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
self.SaveRequestsDb()
self.CheckHTMLArchiveDir()
- def Load(self, fqdn_listname=None, check_version=True):
- if fqdn_listname is None:
- fqdn_listname = self.fqdn_listname
- if not Utils.list_exists(fqdn_listname):
- raise Errors.MMUnknownListError(fqdn_listname)
+ def Load(self):
database.load(self)
self._memberadaptor.load()
- if check_version:
- # XXX for now disable version checks. We'll fold this into schema
- # updates eventually.
- #self.CheckVersion(dict)
- self.CheckValues()
@@ -623,7 +583,6 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
self.InitVars()
# Then reload the database (but don't recurse). Force a reload even
# if we have the most up-to-date state.
- self._timestamp = 0
self.Load(self.fqdn_listname, check_version=False)
# We must hold the list lock in order to update the schema
waslocked = self.Locked()
@@ -955,7 +914,7 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
self.setMemberName(addr, name)
if not globally:
return
- for listname in Utils.list_names():
+ for listname in config.list_manager.names:
# Don't bother with ourselves
if listname == self.internal_name():
continue
@@ -1047,7 +1006,7 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin,
# oldaddr is a member.
if not globally:
return
- for listname in Utils.list_names():
+ for listname in config.list_manager.names:
# Don't bother with ourselves
if listname == self.internal_name():
continue
diff --git a/Mailman/Makefile.in b/Mailman/Makefile.in
index 43536ec22..e71d86ce8 100644
--- a/Mailman/Makefile.in
+++ b/Mailman/Makefile.in
@@ -44,7 +44,7 @@ SHELL= /bin/sh
MODULES= $(srcdir)/*.py
SUBDIRS= Cgi Archiver Handlers Bouncers Queue MTA Gui Commands \
- bin database testing
+ bin database docs ext interfaces testing
# Modes for directories and executables created by the install
# process. Default to group-writable directories but
diff --git a/Mailman/Pending.py b/Mailman/Pending.py
index f5794453d..1d133e018 100644
--- a/Mailman/Pending.py
+++ b/Mailman/Pending.py
@@ -17,6 +17,8 @@
"""Track pending actions which require confirmation."""
+from __future__ import with_statement
+
import os
import sha
import time
@@ -51,7 +53,7 @@ _default = object()
class Pending:
def InitTempVars(self):
- self.__pendfile = os.path.join(self.fullpath(), 'pending.pck')
+ self._pendfile = os.path.join(self.full_path, 'pending.pck')
def pend_new(self, op, *content, **kws):
"""Create a new entry in the pending database, returning cookie for it.
@@ -87,14 +89,12 @@ class Pending:
def __load(self):
try:
- fp = open(self.__pendfile)
+ with open(self._pendfile) as fp:
+ return cPickle.load(fp)
except IOError, e:
- if e.errno <> errno.ENOENT: raise
+ if e.errno <> errno.ENOENT:
+ raise
return {'evictions': {}}
- try:
- return cPickle.load(fp)
- finally:
- fp.close()
def __save(self, db):
evictions = db['evictions']
@@ -112,15 +112,12 @@ class Pending:
if not db.has_key(cookie):
del evictions[cookie]
db['version'] = config.PENDING_FILE_SCHEMA_VERSION
- tmpfile = '%s.tmp.%d.%d' % (self.__pendfile, os.getpid(), now)
- fp = open(tmpfile, 'w')
- try:
+ tmpfile = '%s.tmp.%d.%d' % (self._pendfile, os.getpid(), now)
+ with open(tmpfile, 'w') as fp:
cPickle.dump(db, fp)
fp.flush()
os.fsync(fp.fileno())
- finally:
- fp.close()
- os.rename(tmpfile, self.__pendfile)
+ os.rename(tmpfile, self._pendfile)
def pend_confirm(self, cookie, expunge=True):
"""Return data for cookie, or None if not found.
diff --git a/Mailman/Queue/LMTPRunner.py b/Mailman/Queue/LMTPRunner.py
index 0d2214641..81c912653 100644
--- a/Mailman/Queue/LMTPRunner.py
+++ b/Mailman/Queue/LMTPRunner.py
@@ -48,7 +48,6 @@ import asyncore
from email.utils import parseaddr
-from Mailman import Utils
from Mailman.Message import Message
from Mailman.Queue.Runner import Runner
from Mailman.Queue.sbcache import get_switchboard
@@ -122,7 +121,7 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
# since the set of mailing lists could have changed. However, on
# a big site this could be fairly expensive, so we may need to
# cache this in some way.
- listnames = Utils.list_names()
+ listnames = set(config.list_manager.names)
# Parse the message data. XXX Should we reject the message
# immediately if it has defects? Usually only spam has defects.
msg = email.message_from_string(data, Message)
diff --git a/Mailman/Queue/MaildirRunner.py b/Mailman/Queue/MaildirRunner.py
index 012aacc4a..ff193bf22 100644
--- a/Mailman/Queue/MaildirRunner.py
+++ b/Mailman/Queue/MaildirRunner.py
@@ -56,7 +56,6 @@ import logging
from email.Parser import Parser
from email.Utils import parseaddr
-from Mailman import Utils
from Mailman.Message import Message
from Mailman.Queue.Runner import Runner
from Mailman.Queue.sbcache import get_switchboard
@@ -101,9 +100,8 @@ class MaildirRunner(Runner):
self._parser = Parser(Message)
def _oneloop(self):
- # Refresh this each time through the list. BAW: could be too
- # expensive.
- listnames = Utils.list_names()
+ # Refresh this each time through the list.
+ listnames = list(config.list_manager.names)
# Cruise through all the files currently in the new/ directory
try:
files = os.listdir(self._dir)
diff --git a/Mailman/Queue/Runner.py b/Mailman/Queue/Runner.py
index 4b782b3e3..2045022fa 100644
--- a/Mailman/Queue/Runner.py
+++ b/Mailman/Queue/Runner.py
@@ -92,16 +92,16 @@ class Runner:
# Ask the switchboard for the message and metadata objects
# associated with this filebase.
msg, msgdata = self._switchboard.dequeue(filebase)
- except email.Errors.MessageParseError, e:
- # It's possible to get here if the message was stored in the
- # pickle in plain text, and the metadata had a _parsemsg key
- # that was true, /and/ if the message had some bogosity in
- # it. It's almost always going to be spam or bounced spam.
- # There's not much we can do (and we didn't even get the
- # metadata, so just log the exception and continue.
+ except Exception, e:
+ # This used to just catch email.Errors.MessageParseError,
+ # but other problems can occur in message parsing, e.g.
+ # ValueError, and exceptions can occur in unpickling too.
+ # We don't want the runner to die, so we just log and skip
+ # this entry, but preserve it for analysis.
self._log(e)
- log.error('Ignoring unparseable message: %s', filebase)
- self._switchboard.finish(filebase)
+ log.error('Skipping and preserving unparseable message: %s',
+ filebase)
+ self._switchboard.finish(filebase, preserve=True)
continue
try:
self._onefile(msg, msgdata)
@@ -116,9 +116,21 @@ class Runner:
self._log(e)
# Put a marker in the metadata for unshunting
msgdata['whichq'] = self._switchboard.whichq()
- new_filebase = self._shunt.enqueue(msg, msgdata)
- log.error('SHUNTING: %s', new_filebase)
- self._switchboard.finish(filebase)
+ # It is possible that shunting can throw an exception, e.g. a
+ # permissions problem or a MemoryError due to a really large
+ # message. Try to be graceful.
+ try:
+ new_filebase = self._shunt.enqueue(msg, msgdata)
+ log.error('SHUNTING: %s', new_filebase)
+ self._switchboard.finish(filebase)
+ except Exception, e:
+ # The message wasn't successfully shunted. Log the
+ # exception and try to preserve the original queue entry
+ # for possible analysis.
+ self._log(e)
+ log.error('SHUNTING FAILED, preserving original entry: %s',
+ filebase)
+ self._switchboard.finish(filebase, preserve=True)
# Other work we want to do each time through the loop
Utils.reap(self._kids, once=True)
self._doperiodic()
diff --git a/Mailman/Queue/Switchboard.py b/Mailman/Queue/Switchboard.py
index fabb1d099..6f5cd6222 100644
--- a/Mailman/Queue/Switchboard.py
+++ b/Mailman/Queue/Switchboard.py
@@ -149,12 +149,20 @@ class Switchboard:
msg = email.message_from_string(msg, Message.Message)
return msg, data
- def finish(self, filebase):
+ def finish(self, filebase, preserve=False):
bakfile = os.path.join(self.__whichq, filebase + '.bak')
try:
- os.unlink(bakfile)
+ if preserve:
+ psvfile = os.path.join(config.SHUNTQUEUE_DIR,
+ filebase + '.psv')
+ # Create the directory if it doesn't yet exist.
+ Utils.makedirs(config.SHUNTQUEUE_DIR, 0770)
+ os.rename(bakfile, psvfile)
+ else:
+ os.unlink(bakfile)
except EnvironmentError, e:
- elog.exception('Failed to unlink backup file: %s', bakfile)
+ elog.exception('Failed to unlink/preserve backup file: %s',
+ bakfile)
def files(self, extension='.pck'):
times = {}
diff --git a/Mailman/Utils.py b/Mailman/Utils.py
index afb2f1fd5..27a61567e 100644
--- a/Mailman/Utils.py
+++ b/Mailman/Utils.py
@@ -40,7 +40,6 @@ from email.Errors import HeaderParseError
from string import ascii_letters, digits, whitespace
from Mailman import Errors
-from Mailman import database
from Mailman import passwords
from Mailman.SafeDict import SafeDict
from Mailman.configuration import config
@@ -66,13 +65,13 @@ log = logging.getLogger('mailman.error')
def list_exists(fqdn_listname):
"""Return true iff list `fqdn_listname' exists."""
listname, hostname = split_listname(fqdn_listname)
- return bool(database.find_list(listname, hostname))
+ return bool(config.list_manager.find_list(listname, hostname))
def list_names():
"""Return the fqdn names of all lists in default list directory."""
return ['%s@%s' % (listname, hostname)
- for listname, hostname in database.get_list_names()]
+ for listname, hostname in config.list_manager.get_list_names()]
def split_listname(listname):
@@ -81,8 +80,10 @@ def split_listname(listname):
return listname, config.DEFAULT_EMAIL_HOST
-def fqdn_listname(listname):
- return AT.join(split_listname(listname))
+def fqdn_listname(listname, hostname=None):
+ if hostname is None:
+ return AT.join(split_listname(listname))
+ return AT.join((listname, hostname))
diff --git a/Mailman/bin/bumpdigests.py b/Mailman/bin/bumpdigests.py
index 602ecd763..94a010950 100644
--- a/Mailman/bin/bumpdigests.py
+++ b/Mailman/bin/bumpdigests.py
@@ -20,7 +20,6 @@ import optparse
from Mailman import Errors
from Mailman import MailList
-from Mailman import Utils
from Mailman import Version
from Mailman.configuration import config
from Mailman.i18n import _
@@ -52,7 +51,7 @@ def main():
opts, args, parser = parseargs()
config.load(opts.config)
- listnames = set(args or Utils.list_names())
+ listnames = set(args or config.list_manager.names)
if not listnames:
print _('Nothing to do.')
sys.exit(0)
diff --git a/Mailman/bin/change_pw.py b/Mailman/bin/change_pw.py
index 57080d4fa..3830b76ae 100644
--- a/Mailman/bin/change_pw.py
+++ b/Mailman/bin/change_pw.py
@@ -114,13 +114,10 @@ def main():
# Cull duplicates
domains = set(opts.domains)
- if opts.all:
- listnames = set(Utils.list_names())
- else:
- listnames = set(opts.listnames)
+ listnames = set(config.list_manager.names if opts.all else opts.listnames)
if domains:
- for name in Utils.list_names():
+ for name in config.list_manager.names:
mlist = openlist(name)
if mlist.host_name in domains:
listnames.add(name)
diff --git a/Mailman/bin/checkdbs.py b/Mailman/bin/checkdbs.py
index 1423421c9..691417780 100755
--- a/Mailman/bin/checkdbs.py
+++ b/Mailman/bin/checkdbs.py
@@ -132,7 +132,7 @@ def main():
i18n.set_language(config.DEFAULT_SERVER_LANGUAGE)
- for name in Utils.list_names():
+ for name in config.list_manager.names:
# The list must be locked in order to open the requests database
mlist = MailList.MailList(name)
try:
diff --git a/Mailman/bin/disabled.py b/Mailman/bin/disabled.py
index 36999793b..81916e7d9 100644
--- a/Mailman/bin/disabled.py
+++ b/Mailman/bin/disabled.py
@@ -23,7 +23,6 @@ from Mailman import Errors
from Mailman import MailList
from Mailman import MemberAdaptor
from Mailman import Pending
-from Mailman import Utils
from Mailman import Version
from Mailman import loginit
from Mailman.Bouncer import _BounceInfo
@@ -122,7 +121,7 @@ def main():
elog = logging.getLogger('mailman.error')
blog = logging.getLogger('mailman.bounce')
- listnames = set(opts.listnames or Utils.list_names())
+ listnames = set(opts.listnames or config.list_manager.names)
who = tuple(opts.who)
msg = _('[disabled by periodic sweep and cull, no message available]')
diff --git a/Mailman/bin/export.py b/Mailman/bin/export.py
index b0b5519ef..42b5a17b0 100644
--- a/Mailman/bin/export.py
+++ b/Mailman/bin/export.py
@@ -31,7 +31,6 @@ from xml.sax.saxutils import escape
from Mailman import Defaults
from Mailman import Errors
from Mailman import MemberAdaptor
-from Mailman import Utils
from Mailman import Version
from Mailman.MailList import MailList
from Mailman.configuration import config
@@ -308,7 +307,7 @@ def main():
listname = '%s@%s' % (listname, config.DEFAULT_EMAIL_HOST)
listnames.append(listname)
else:
- listnames = Utils.list_names()
+ listnames = config.list_manager.names
dumper.dump(listnames)
dumper.close()
finally:
diff --git a/Mailman/bin/find_member.py b/Mailman/bin/find_member.py
index a0116cd03..187345bfe 100644
--- a/Mailman/bin/find_member.py
+++ b/Mailman/bin/find_member.py
@@ -21,7 +21,6 @@ import optparse
from Mailman import Errors
from Mailman import MailList
-from Mailman import Utils
from Mailman import Version
from Mailman.configuration import config
from Mailman.i18n import _
@@ -79,9 +78,8 @@ def main():
parser, opts, args = parseargs()
config.load(opts.config)
- if not opts.listnames:
- opts.listnames = Utils.list_names()
- includes = set(listname.lower() for listname in opts.listnames)
+ listnames = opts.listnames or config.list_manager.names
+ includes = set(listname.lower() for listname in listnames)
excludes = set(listname.lower() for listname in opts.excludes)
listnames = includes - excludes
diff --git a/Mailman/bin/gate_news.py b/Mailman/bin/gate_news.py
index 71cf060d8..491660f2f 100644
--- a/Mailman/bin/gate_news.py
+++ b/Mailman/bin/gate_news.py
@@ -164,7 +164,7 @@ def poll_newsgroup(mlist, conn, first, last, glock):
def process_lists(glock):
- for listname in Utils.list_names():
+ for listname in config.list_manager.names:
glock.refresh()
# Open the list unlocked just to check to see if it is gating news to
# mail. If not, we're done with the list. Otherwise, lock the list
diff --git a/Mailman/bin/genaliases.py b/Mailman/bin/genaliases.py
index 4ce8160fd..239f88c91 100644
--- a/Mailman/bin/genaliases.py
+++ b/Mailman/bin/genaliases.py
@@ -21,7 +21,6 @@ import sys
import optparse
from Mailman import MailList
-from Mailman import Utils
from Mailman import Version
from Mailman.configuration import config
from Mailman.i18n import _
@@ -68,7 +67,7 @@ def main():
lock.lock()
# Group lists by virtual hostname
mlists = {}
- for listname in Utils.list_names():
+ for listname in config.list_manager.names:
mlist = MailList.MailList(listname, lock=False)
mlists.setdefault(mlist.host_name, []).append(mlist)
try:
diff --git a/Mailman/bin/list_lists.py b/Mailman/bin/list_lists.py
index b4ca44366..14a57f002 100644
--- a/Mailman/bin/list_lists.py
+++ b/Mailman/bin/list_lists.py
@@ -19,7 +19,6 @@ import optparse
from Mailman import Defaults
from Mailman import MailList
-from Mailman import Utils
from Mailman import Version
from Mailman.i18n import _
from Mailman.initialize import initialize
@@ -66,12 +65,10 @@ def main():
parser, opts, args = parseargs()
initialize(opts.config)
- names = list(Utils.list_names())
- names.sort()
mlists = []
longest = 0
- for n in names:
+ for n in sorted(config.list_manager.names):
mlist = MailList.MailList(n, lock=False)
if opts.advertised and not mlist.advertised:
continue
diff --git a/Mailman/bin/list_owners.py b/Mailman/bin/list_owners.py
index b9f12047d..5bc01eeee 100644
--- a/Mailman/bin/list_owners.py
+++ b/Mailman/bin/list_owners.py
@@ -18,7 +18,6 @@
import sys
import optparse
-from Mailman import Utils
from Mailman import Version
from Mailman.MailList import MailList
from Mailman.configuration import config
@@ -55,7 +54,7 @@ def main():
parser, opts, args = parseargs()
config.load(opts.config)
- listnames = args or Utils.list_names()
+ listnames = set(args or config.list_manager.names)
bylist = {}
for listname in listnames:
diff --git a/Mailman/bin/nightly_gzip.py b/Mailman/bin/nightly_gzip.py
index 4bd9271bc..efd054293 100644
--- a/Mailman/bin/nightly_gzip.py
+++ b/Mailman/bin/nightly_gzip.py
@@ -25,7 +25,6 @@ except ImportError:
sys.exit(0)
from Mailman import MailList
-from Mailman import Utils
from Mailman import Version
from Mailman.configuration import config
from Mailman.i18n import _
@@ -86,7 +85,7 @@ def main():
return
# Process all the specified lists
- for listname in set(args or Utils.list_names()):
+ for listname in set(args or config.list_manager.names):
mlist = MailList.MailList(listname, lock=False)
if not mlist.archive:
continue
diff --git a/Mailman/bin/senddigests.py b/Mailman/bin/senddigests.py
index 96e3cfabd..a2c7089df 100644
--- a/Mailman/bin/senddigests.py
+++ b/Mailman/bin/senddigests.py
@@ -19,7 +19,6 @@ import sys
import optparse
from Mailman import MailList
-from Mailman import Utils
from Mailman import Version
from Mailman.i18n import _
from Mailman.initialize import initialize
@@ -59,7 +58,7 @@ def main():
opts, args, parser = parseargs()
initialize(opts.config)
- for listname in set(opts.listnames or Utils.list_names()):
+ for listname in set(opts.listnames or config.list_manager.names):
mlist = MailList.MailList(listname, lock=False)
if mlist.digest_send_periodic:
mlist.Lock()
diff --git a/Mailman/bin/update.py b/Mailman/bin/update.py
index 281f40420..93bb0021b 100644
--- a/Mailman/bin/update.py
+++ b/Mailman/bin/update.py
@@ -242,10 +242,6 @@ def dolist(listname):
for addr in noinfo.keys():
mlist.setDeliveryStatus(addr, ENABLED)
- # Update the held requests database
- print _("""Updating the held requests database.""")
- mlist._UpdateRecords()
-
mbox_dir = make_varabs('archives/private/%s.mbox' % (listname))
mbox_file = make_varabs('archives/private/%s.mbox/%s' % (listname,
listname))
@@ -538,121 +534,6 @@ def dequeue(filebase):
-def update_pending():
- file20 = os.path.join(config.DATA_DIR, 'pending_subscriptions.db')
- file214 = os.path.join(config.DATA_DIR, 'pending.pck')
- db = None
- # Try to load the Mailman 2.0 file
- try:
- fp = open(file20)
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- else:
- print _('Updating Mailman 2.0 pending_subscriptions.db database')
- db = marshal.load(fp)
- # Convert to the pre-Mailman 2.1.5 format
- db = Pending._update(db)
- if db is None:
- # Try to load the Mailman 2.1.x where x < 5, file
- try:
- fp = open(file214)
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- else:
- print _('Updating Mailman 2.1.4 pending.pck database')
- db = cPickle.load(fp)
- if db is None:
- print _('Nothing to do.')
- return
- # Now upgrade the database to the 2.1.5 format. Each list now has its own
- # pending.pck file, but only the RE_ENABLE operation actually recorded the
- # listname in the request. For the SUBSCRIPTION, UNSUBSCRIPTION, and
- # CHANGE_OF_ADDRESS operations, we know the address of the person making
- # the request so we can repend this request just for the lists the person
- # is a member of. For the HELD_MESSAGE operation, we can check the list's
- # requests.pck file for correlation. Evictions will take care of any
- # misdirected pendings.
- reenables_by_list = {}
- addrops_by_address = {}
- holds_by_id = {}
- subs_by_address = {}
- for key, val in db.items():
- if key in ('evictions', 'version'):
- continue
- try:
- op = val[0]
- data = val[1:]
- except (IndexError, ValueError):
- print _('Ignoring bad pended data: $key: $val')
- continue
- if op in (Pending.UNSUBSCRIPTION, Pending.CHANGE_OF_ADDRESS):
- # data[0] is the address being unsubscribed
- addrops_by_address.setdefault(data[0], []).append((key, val))
- elif op == Pending.SUBSCRIPTION:
- # data[0] is a UserDesc object
- addr = data[0].address
- subs_by_address.setdefault(addr, []).append((key, val))
- elif op == Pending.RE_ENABLE:
- # data[0] is the mailing list's internal name
- reenables_by_list.setdefault(data[0], []).append((key, val))
- elif op == Pending.HELD_MESSAGE:
- # data[0] is the hold id. There better only be one entry per id
- id = data[0]
- if holds_by_id.has_key(id):
- print _('WARNING: Ignoring duplicate pending ID: $id.')
- else:
- holds_by_id[id] = (key, val)
- # Now we have to lock every list and re-pend all the appropriate
- # requests. Note that this will reset all the expiration dates, but that
- # should be fine.
- for listname in Utils.list_names():
- mlist = MailList.MailList(listname)
- # This is not the most efficient way to do this because it loads and
- # saves the pending.pck file each time. :(
- try:
- for cookie, data in reenables_by_list.get(listname, []):
- mlist.pend_repend(cookie, data)
- for id, (cookie, data) in holds_by_id.items():
- try:
- rec = mlist.GetRecord(id)
- except KeyError:
- # Not for this list
- pass
- else:
- mlist.pend_repend(cookie, data)
- del holds_by_id[id]
- for addr, recs in subs_by_address.items():
- # We shouldn't have a subscription confirmation if the address
- # is already a member of the mailing list.
- if mlist.isMember(addr):
- continue
- for cookie, data in recs:
- mlist.pend_repend(cookie, data)
- for addr, recs in addrops_by_address.items():
- # We shouldn't have unsubscriptions or change of address
- # requests for addresses which aren't members of the list.
- if not mlist.isMember(addr):
- continue
- for cookie, data in recs:
- mlist.pend_repend(cookie, data)
- mlist.Save()
- finally:
- mlist.Unlock()
- try:
- os.unlink(file20)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- try:
- os.unlink(file214)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
-
-
-
def main():
parser, opts, args = parseargs()
initialize(opts.config)
@@ -682,8 +563,7 @@ Exiting.""")
'scripts/mailowner', 'mail/wrapper', 'Mailman/pythonlib',
'cgi-bin/archives', 'Mailman/MailCommandHandler'):
remove_old_sources(mod)
- listnames = Utils.list_names()
- if not listnames:
+ if not config.list_manager.names:
print _('no lists == nothing to do, exiting')
return
# For people with web archiving, make sure the directories
@@ -695,7 +575,7 @@ If your archives are big, this could take a minute or two...""")
os.path.walk("%s/public_html/archives" % config.PREFIX,
archive_path_fixer, "")
print _('done')
- for listname in listnames:
+ for listname in config.list_manager.names:
# With 2.2.0a0, all list names grew an @domain suffix. If you find a
# list without that, move it now.
if not '@' in listname:
@@ -732,10 +612,6 @@ If your archives are big, this could take a minute or two...""")
mlist.Unlock()
os.unlink(wmfile)
print _('- usenet watermarks updated and gate_watermarks removed')
- # In Mailman 2.1, the pending database format and file name changed, but
- # in Mailman 2.1.5 it changed again. This should update all existing
- # files to the 2.1.5 format.
- update_pending()
# In Mailman 2.1, the qfiles directory has a different structure and a
# different content. Also, in Mailman 2.1.5 we collapsed the message
# files from separate .msg (pickled Message objects) and .db (marshalled
diff --git a/Mailman/bin/withlist.py b/Mailman/bin/withlist.py
index b108e4a18..ac1ab0aac 100644
--- a/Mailman/bin/withlist.py
+++ b/Mailman/bin/withlist.py
@@ -22,7 +22,6 @@ import optparse
from Mailman import Errors
from Mailman import MailList
-from Mailman import Utils
from Mailman import Version
from Mailman import interact
from Mailman.configuration import config
@@ -235,7 +234,8 @@ def main():
r = None
if opts.all:
- r = [do_list(listname, args, func) for listname in Utils.list_names()]
+ r = [do_list(listname, args, func)
+ for listname in config.list_manager.names]
elif dolist:
listname = args.pop(0).lower().strip()
r = do_list(listname, args, func)
diff --git a/Mailman/configuration.py b/Mailman/configuration.py
index a0d35e483..3247204b3 100644
--- a/Mailman/configuration.py
+++ b/Mailman/configuration.py
@@ -97,6 +97,7 @@ class Configuration(object):
self.DATA_DIR = datadir = os.path.join(VAR_PREFIX, 'data')
self.ETC_DIR = etcdir = os.path.join(VAR_PREFIX, 'etc')
self.SPAM_DIR = os.path.join(VAR_PREFIX, 'spam')
+ self.EXT_DIR = os.path.join(VAR_PREFIX, 'ext')
self.WRAPPER_DIR = os.path.join(EXEC_PREFIX, 'mail')
self.BIN_DIR = os.path.join(PREFIX, 'bin')
self.SCRIPTS_DIR = os.path.join(PREFIX, 'scripts')
diff --git a/Mailman/constants.py b/Mailman/constants.py
new file mode 100644
index 000000000..852704364
--- /dev/null
+++ b/Mailman/constants.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2006-2007 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.
+
+"""Various constants and enumerations."""
+
+from munepy import Enum
+
+
+
+class DeliveryMode(Enum):
+ # Regular (i.e. non-digest) delivery
+ regular = 1
+ # Plain text digest delivery
+ plaintext_digests = 2
+ # MIME digest delivery
+ mime_digests = 3
+ # Summary digests
+ summary_digests = 4
+
+
+
+class DeliveryStatus(Enum):
+ # Delivery is enabled
+ enabled = 1
+ # Delivery was disabled by the user
+ by_user = 2
+ # Delivery was disabled due to bouncing addresses
+ by_bounces = 3
+ # Delivery was disabled by an administrator or moderator
+ by_moderator = 4
diff --git a/Mailman/database/Makefile.in b/Mailman/database/Makefile.in
index 8936d791b..56fcb3f29 100644
--- a/Mailman/database/Makefile.in
+++ b/Mailman/database/Makefile.in
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
+# Copyright (C) 2007 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
@@ -42,6 +42,7 @@ PACKAGEDIR= $(prefix)/Mailman/database
SHELL= /bin/sh
MODULES= *.py
+SUBDIRS= tables model
# Modes for directories and executables created by the install
# process. Default to group-writable directories but
@@ -55,17 +56,37 @@ INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
# Rules
all:
+ for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE)); \
+ done
install:
for f in $(MODULES); \
do \
$(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
done
+ for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) DESTDIR=$(DESTDIR) install); \
+ done
finish:
+ @for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) DESTDIR=$(DESTDIR) finish); \
+ done
clean:
+ for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) clean); \
+ done
distclean:
-rm *.pyc
-rm Makefile
+ for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) distclean); \
+ done
diff --git a/Mailman/database/__init__.py b/Mailman/database/__init__.py
index 808ad9fd0..6c6312d0a 100644
--- a/Mailman/database/__init__.py
+++ b/Mailman/database/__init__.py
@@ -15,31 +15,35 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-# This module exposes the higher level interface methods that the rest of
-# Mailman should use. It essentially hides the dbcontext and the SQLAlchemy
-# session from all other code. The preferred way to use these methods is:
-#
-# from Mailman import database
-# database.add_list(foo)
+from __future__ import with_statement
import os
+from elixir import objectstore
+
+from Mailman.database.listmanager import ListManager
+from Mailman.database.usermanager import UserManager
+
+__all__ = [
+ 'initialize',
+ 'flush',
+ ]
+
+
def initialize():
- from Mailman import database
from Mailman.LockFile import LockFile
from Mailman.configuration import config
- from Mailman.database.dbcontext import dbcontext
+ from Mailman.database import model
# Serialize this so we don't get multiple processes trying to create the
# database at the same time.
lockfile = os.path.join(config.LOCK_DIR, '<dbcreatelock>')
- lock = LockFile(lockfile)
- lock.lock()
- try:
- dbcontext.connect()
- finally:
- lock.unlock()
- for attr in dir(dbcontext):
- if attr.startswith('api_'):
- exposed_name = attr[4:]
- setattr(database, exposed_name, getattr(dbcontext, attr))
+ with LockFile(lockfile):
+ model.initialize()
+ config.list_manager = ListManager()
+ config.user_manager = UserManager()
+ flush()
+
+
+def flush():
+ objectstore.flush()
diff --git a/Mailman/database/dbcontext.py b/Mailman/database/dbcontext.py
index 5421fb6f2..eaf87be93 100644
--- a/Mailman/database/dbcontext.py
+++ b/Mailman/database/dbcontext.py
@@ -16,19 +16,17 @@
# USA.
import os
+import sys
import logging
import weakref
-from sqlalchemy import BoundMetaData, create_session
+from elixir import create_all, metadata, objectstore
+from sqlalchemy import create_engine
from string import Template
from urlparse import urlparse
from Mailman import Version
from Mailman.configuration import config
-from Mailman.database import address
-from Mailman.database import languages
-from Mailman.database import listdata
-from Mailman.database import version
from Mailman.database.txnsupport import txn
@@ -39,19 +37,9 @@ class MlistRef(weakref.ref):
self.fqdn_listname = mlist.fqdn_listname
-class Tables(object):
- def bind(self, table, attrname=None):
- if attrname is None:
- attrname = table.name.lower()
- setattr(self, attrname, table)
-
-
class DBContext(object):
def __init__(self):
- self.tables = Tables()
- self.metadata = None
- self.session = None
# Special transaction used only for MailList.Lock() .Save() and
# .Unlock() interface.
self._mlist_txns = {}
@@ -75,37 +63,30 @@ class DBContext(object):
# engines, and yes, we could have chmod'd the file after the fact, but
# half dozen and all...
self._touch(url)
- self.metadata = BoundMetaData(url)
- self.metadata.engine.echo = config.SQLALCHEMY_ECHO
- # Create all the table objects, and then let SA conditionally create
- # them if they don't yet exist. NOTE: this order matters!
- for module in (languages, address, listdata, version):
- module.make_table(self.metadata, self.tables)
- self.metadata.create_all()
- # Validate schema version, updating if necessary (XXX)
- r = self.tables.version.select(
- self.tables.version.c.component=='schema').execute()
- row = r.fetchone()
- if row is None:
+ engine = create_engine(url)
+ engine.echo = config.SQLALCHEMY_ECHO
+ metadata.connect(engine)
+ # Load and create the Elixir active records. This works by
+ # side-effect.
+ import Mailman.database.model
+ create_all()
+ # Validate schema version.
+ v = Mailman.database.model.Version.get_by(component='schema')
+ if not v:
# Database has not yet been initialized
- self.tables.version.insert().execute(
+ v = Mailman.database.model.Version(
component='schema',
version=Version.DATABASE_SCHEMA_VERSION)
- elif row.version <> Version.DATABASE_SCHEMA_VERSION:
+ objectstore.flush()
+ elif v.version <> Version.DATABASE_SCHEMA_VERSION:
# XXX Update schema
- raise SchemaVersionMismatchError(row.version)
- self.session = create_session()
-
- def close(self):
- self.session.close()
- self.session = None
+ raise SchemaVersionMismatchError(v.version)
def _touch(self, url):
parts = urlparse(url)
- # XXX Python 2.5; use parts.scheme and parts.path
- if parts[0] <> 'sqlite':
+ if parts.scheme <> 'sqlite':
return
- path = os.path.normpath(parts[2])
+ path = os.path.normpath(parts.path)
fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666)
# Ignore errors
if fd > 0:
@@ -114,7 +95,7 @@ class DBContext(object):
# Cooperative method for use with @txn decorator
def _withtxn(self, meth, *args, **kws):
try:
- txn = self.session.create_transaction()
+ txn = objectstore.session.current.create_transaction()
rtn = meth(*args, **kws)
except:
txn.rollback()
@@ -133,7 +114,7 @@ class DBContext(object):
# Don't try to re-lock a list
if mlist.fqdn_listname in self._mlist_txns:
return
- txn = self.session.create_transaction()
+ txn = objectstore.session.current.create_transaction()
mref = MlistRef(mlist, self._unlock_mref)
# If mlist.host_name is changed, its fqdn_listname attribute will no
# longer match, so its transaction will not get committed when the
@@ -155,7 +136,7 @@ class DBContext(object):
def api_load(self, mlist):
# Mark the MailList object such that future attribute accesses will
# refresh from the database.
- self.session.expire(mlist)
+ objectstore.session.current.expire(mlist)
def api_save(self, mlist):
# When dealing with MailLists, .Save() will always be followed by
@@ -172,29 +153,22 @@ class DBContext(object):
@txn
def api_add_list(self, mlist):
- self.session.save(mlist)
+ objectstore.session.current.save(mlist)
@txn
def api_remove_list(self, mlist):
- self.session.delete(mlist)
+ objectstore.session.current.delete(mlist)
@txn
def api_find_list(self, listname, hostname):
from Mailman.MailList import MailList
- q = self.session.query(MailList)
+ q = objectstore.session.current.query(MailList)
mlists = q.select_by(list_name=listname, host_name=hostname)
assert len(mlists) <= 1, 'Duplicate mailing lists!'
if mlists:
return mlists[0]
return None
- @txn
- def api_get_list_names(self):
- table = self.tables.listdata
- results = table.select().execute()
- return [(row[table.c.list_name], row[table.c.host_name])
- for row in results.fetchall()]
-
dbcontext = DBContext()
diff --git a/Mailman/database/listmanager.py b/Mailman/database/listmanager.py
new file mode 100644
index 000000000..de8abbd58
--- /dev/null
+++ b/Mailman/database/listmanager.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2007 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.
+
+"""SQLAlchemy/Elixir based provider of IListManager."""
+
+import weakref
+
+from elixir import *
+from zope.interface import implements
+
+from Mailman import Errors
+from Mailman.Utils import split_listname, fqdn_listname
+from Mailman.configuration import config
+from Mailman.database.model import MailingList
+from Mailman.interfaces import IListManager
+
+
+
+class ListManager(object):
+ implements(IListManager)
+
+ def __init__(self):
+ self._objectmap = weakref.WeakKeyDictionary()
+
+ def create(self, fqdn_listname):
+ listname, hostname = split_listname(fqdn_listname)
+ mlist = MailingList.get_by(list_name=listname,
+ host_name=hostname)
+ if mlist:
+ raise Errors.MMListAlreadyExistsError(fqdn_listname)
+ mlist = MailingList(fqdn_listname)
+ # Wrap the database model object in an application MailList object and
+ # return the latter. Keep track of the wrapper so we can clean it up
+ # when we're done with it.
+ from Mailman.MailList import MailList
+ wrapper = MailList(mlist)
+ self._objectmap[mlist] = wrapper
+ return wrapper
+
+ def delete(self, mlist):
+ # Delete the wrapped backing data. XXX It's kind of icky to reach
+ # into the MailList object this way.
+ mlist._data.delete_rosters()
+ mlist._data.delete()
+ mlist._data = None
+
+ def get(self, fqdn_listname):
+ listname, hostname = split_listname(fqdn_listname)
+ mlist = MailingList.get_by(list_name=listname,
+ host_name=hostname)
+ if not mlist:
+ raise Errors.MMUnknownListError(fqdn_listname)
+ from Mailman.MailList import MailList
+ wrapper = self._objectmap.setdefault(mlist, MailList(mlist))
+ return wrapper
+
+ @property
+ def mailing_lists(self):
+ # Don't forget, the MailingList objects that this class manages must
+ # be wrapped in a MailList object as expected by this interface.
+ for fqdn_listname in self.names:
+ yield self.get(fqdn_listname)
+
+ @property
+ def names(self):
+ for mlist in MailingList.select():
+ yield fqdn_listname(mlist.list_name, mlist.host_name)
diff --git a/Mailman/database/model/Makefile.in b/Mailman/database/model/Makefile.in
new file mode 100644
index 000000000..2db8ce45e
--- /dev/null
+++ b/Mailman/database/model/Makefile.in
@@ -0,0 +1,71 @@
+# Copyright (C) 2007 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.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+DESTDIR=
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+PACKAGEDIR= $(prefix)/Mailman/database/model
+SHELL= /bin/sh
+
+MODULES= *.py
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+
+# Rules
+
+all:
+
+install:
+ for f in $(MODULES); \
+ do \
+ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm *.pyc
+ -rm Makefile
diff --git a/Mailman/database/model/__init__.py b/Mailman/database/model/__init__.py
new file mode 100644
index 000000000..11ca11f89
--- /dev/null
+++ b/Mailman/database/model/__init__.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2007 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.
+
+__all__ = [
+ 'Address',
+ 'Language',
+ 'MailingList',
+ 'Profile',
+ 'Roster',
+ 'RosterSet',
+ 'User',
+ 'Version',
+ ]
+
+import os
+import sys
+import elixir
+
+from sqlalchemy import create_engine
+from string import Template
+from urlparse import urlparse
+
+import Mailman.Version
+
+elixir.delay_setup = True
+
+from Mailman import constants
+from Mailman.Errors import SchemaVersionMismatchError
+from Mailman.configuration import config
+from Mailman.database.model.address import Address
+from Mailman.database.model.language import Language
+from Mailman.database.model.mailinglist import MailingList
+from Mailman.database.model.profile import Profile
+from Mailman.database.model.roster import Roster
+from Mailman.database.model.rosterset import RosterSet
+from Mailman.database.model.user import User
+from Mailman.database.model.version import Version
+
+
+
+def initialize():
+ # Calculate the engine url
+ url = Template(config.SQLALCHEMY_ENGINE_URL).safe_substitute(config.paths)
+ # XXX By design of SQLite, database file creation does not honor
+ # umask. See their ticket #1193:
+ # http://www.sqlite.org/cvstrac/tktview?tn=1193,31
+ #
+ # This sucks for us because the mailman.db file /must/ be group writable,
+ # however even though we guarantee our umask is 002 here, it still gets
+ # created without the necessary g+w permission, due to SQLite's policy.
+ # This should only affect SQLite engines because its the only one that
+ # creates a little file on the local file system. This kludges around
+ # their bug by "touch"ing the database file before SQLite has any chance
+ # to create it, thus honoring the umask and ensuring the right
+ # permissions. We only try to do this for SQLite engines, and yes, we
+ # could have chmod'd the file after the fact, but half dozen and all...
+ touch(url)
+ engine = create_engine(url)
+ engine.echo = config.SQLALCHEMY_ECHO
+ elixir.metadata.connect(engine)
+ elixir.setup_all()
+ # Validate schema version.
+ v = Version.get_by(component='schema')
+ if not v:
+ # Database has not yet been initialized
+ v = Version(component='schema',
+ version=Mailman.Version.DATABASE_SCHEMA_VERSION)
+ elixir.objectstore.flush()
+ elif v.version <> Mailman.Version.DATABASE_SCHEMA_VERSION:
+ # XXX Update schema
+ raise SchemaVersionMismatchError(v.version)
+
+
+def touch(url):
+ parts = urlparse(url)
+ if parts.scheme <> 'sqlite':
+ return
+ path = os.path.normpath(parts.path)
+ fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT, 0666)
+ # Ignore errors
+ if fd > 0:
+ os.close(fd)
diff --git a/Mailman/database/model/address.py b/Mailman/database/model/address.py
new file mode 100644
index 000000000..53d5016e5
--- /dev/null
+++ b/Mailman/database/model/address.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2006-2007 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.
+
+from elixir import *
+from email.utils import formataddr
+from zope.interface import implements
+
+from Mailman.interfaces import IAddress
+
+
+ROSTER_KIND = 'Mailman.database.model.roster.Roster'
+USER_KIND = 'Mailman.database.model.user.User'
+
+
+class Address(Entity):
+ implements(IAddress)
+
+ has_field('address', Unicode)
+ has_field('real_name', Unicode)
+ has_field('verified', Boolean)
+ has_field('registered_on', DateTime)
+ has_field('validated_on', DateTime)
+ # Relationships
+ has_and_belongs_to_many('rosters', of_kind=ROSTER_KIND)
+ belongs_to('user', of_kind=USER_KIND)
+
+ def __str__(self):
+ return formataddr((self.real_name, self.address))
+
+ def __repr__(self):
+ return '<Address: %s [%s]>' % (
+ str(self), ('verified' if self.verified else 'not verified'))
diff --git a/Mailman/database/address.py b/Mailman/database/model/language.py
index 672a366b1..3597a128d 100644
--- a/Mailman/database/address.py
+++ b/Mailman/database/model/language.py
@@ -15,16 +15,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Email addresses."""
+from elixir import *
-from sqlalchemy import *
-
-
-def make_table(metadata, tables):
- table = Table(
- 'Address', metadata,
- Column('address_id', Integer, primary_key=True),
- Column('address', Unicode(4096)),
- )
- tables.bind(table)
+class Language(Entity):
+ has_field('code', Unicode)
diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py
new file mode 100644
index 000000000..28e2c11dc
--- /dev/null
+++ b/Mailman/database/model/mailinglist.py
@@ -0,0 +1,275 @@
+# Copyright (C) 2006-2007 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.
+
+from elixir import *
+from zope.interface import implements
+
+from Mailman.Utils import fqdn_listname, split_listname
+from Mailman.configuration import config
+from Mailman.interfaces import *
+
+
+
+class MailingList(Entity):
+ implements(
+ IMailingList,
+ IMailingListAddresses,
+ IMailingListIdentity,
+ IMailingListRosters,
+ )
+
+ # List identity
+ has_field('list_name', Unicode),
+ has_field('host_name', Unicode),
+ # Attributes not directly modifiable via the web u/i
+ has_field('web_page_url', Unicode),
+ has_field('admin_member_chunksize', Integer),
+ # Attributes which are directly modifiable via the web u/i. The more
+ # complicated attributes are currently stored as pickles, though that
+ # will change as the schema and implementation is developed.
+ has_field('next_request_id', Integer),
+ has_field('next_digest_number', Integer),
+ has_field('admin_responses', PickleType),
+ has_field('postings_responses', PickleType),
+ has_field('request_responses', PickleType),
+ has_field('digest_last_sent_at', Float),
+ has_field('one_last_digest', PickleType),
+ has_field('volume', Integer),
+ has_field('last_post_time', Float),
+ # OldStyleMemberships attributes, temporarily stored as pickles.
+ has_field('bounce_info', PickleType),
+ has_field('delivery_status', PickleType),
+ has_field('digest_members', PickleType),
+ has_field('language', PickleType),
+ has_field('members', PickleType),
+ has_field('passwords', PickleType),
+ has_field('topics_userinterest', PickleType),
+ has_field('user_options', PickleType),
+ has_field('usernames', PickleType),
+ # Attributes which are directly modifiable via the web u/i. The more
+ # complicated attributes are currently stored as pickles, though that
+ # will change as the schema and implementation is developed.
+ has_field('accept_these_nonmembers', PickleType),
+ has_field('acceptable_aliases', PickleType),
+ has_field('admin_immed_notify', Boolean),
+ has_field('admin_notify_mchanges', Boolean),
+ has_field('administrivia', Boolean),
+ has_field('advertised', Boolean),
+ has_field('anonymous_list', Boolean),
+ has_field('archive', Boolean),
+ has_field('archive_private', Boolean),
+ has_field('archive_volume_frequency', Integer),
+ has_field('autorespond_admin', Boolean),
+ has_field('autorespond_postings', Boolean),
+ has_field('autorespond_requests', Integer),
+ has_field('autoresponse_admin_text', Unicode),
+ has_field('autoresponse_graceperiod', Integer),
+ has_field('autoresponse_postings_text', Unicode),
+ has_field('autoresponse_request_text', Unicode),
+ has_field('ban_list', PickleType),
+ has_field('bounce_info_stale_after', Integer),
+ has_field('bounce_matching_headers', Unicode),
+ has_field('bounce_notify_owner_on_disable', Boolean),
+ has_field('bounce_notify_owner_on_removal', Boolean),
+ has_field('bounce_processing', Boolean),
+ has_field('bounce_score_threshold', Integer),
+ has_field('bounce_unrecognized_goes_to_list_owner', Boolean),
+ has_field('bounce_you_are_disabled_warnings', Integer),
+ has_field('bounce_you_are_disabled_warnings_interval', Integer),
+ has_field('collapse_alternatives', Boolean),
+ has_field('convert_html_to_plaintext', Boolean),
+ has_field('default_member_moderation', Boolean),
+ has_field('description', Unicode),
+ has_field('digest_footer', Unicode),
+ has_field('digest_header', Unicode),
+ has_field('digest_is_default', Boolean),
+ has_field('digest_send_periodic', Boolean),
+ has_field('digest_size_threshhold', Integer),
+ has_field('digest_volume_frequency', Integer),
+ has_field('digestable', Boolean),
+ has_field('discard_these_nonmembers', PickleType),
+ has_field('emergency', Boolean),
+ has_field('encode_ascii_prefixes', Boolean),
+ has_field('filter_action', Integer),
+ has_field('filter_content', Boolean),
+ has_field('filter_filename_extensions', PickleType),
+ has_field('filter_mime_types', PickleType),
+ has_field('first_strip_reply_to', Boolean),
+ has_field('forward_auto_discards', Boolean),
+ has_field('gateway_to_mail', Boolean),
+ has_field('gateway_to_news', Boolean),
+ has_field('generic_nonmember_action', Integer),
+ has_field('goodbye_msg', Unicode),
+ has_field('header_filter_rules', PickleType),
+ has_field('hold_these_nonmembers', PickleType),
+ has_field('include_list_post_header', Boolean),
+ has_field('include_rfc2369_headers', Boolean),
+ has_field('info', Unicode),
+ has_field('linked_newsgroup', Unicode),
+ has_field('max_days_to_hold', Integer),
+ has_field('max_message_size', Integer),
+ has_field('max_num_recipients', Integer),
+ has_field('member_moderation_action', Boolean),
+ has_field('member_moderation_notice', Unicode),
+ has_field('mime_is_default_digest', Boolean),
+ has_field('mod_password', Unicode),
+ has_field('msg_footer', Unicode),
+ has_field('msg_header', Unicode),
+ has_field('new_member_options', Integer),
+ has_field('news_moderation', Boolean),
+ has_field('news_prefix_subject_too', Boolean),
+ has_field('nntp_host', Unicode),
+ has_field('nondigestable', Boolean),
+ has_field('nonmember_rejection_notice', Unicode),
+ has_field('obscure_addresses', Boolean),
+ has_field('pass_filename_extensions', PickleType),
+ has_field('pass_mime_types', PickleType),
+ has_field('password', Unicode),
+ has_field('personalize', Integer),
+ has_field('post_id', Integer),
+ has_field('preferred_language', Unicode),
+ has_field('private_roster', Boolean),
+ has_field('real_name', Unicode),
+ has_field('reject_these_nonmembers', PickleType),
+ has_field('reply_goes_to_list', Boolean),
+ has_field('reply_to_address', Unicode),
+ has_field('require_explicit_destination', Boolean),
+ has_field('respond_to_post_requests', Boolean),
+ has_field('scrub_nondigest', Boolean),
+ has_field('send_goodbye_msg', Boolean),
+ has_field('send_reminders', Boolean),
+ has_field('send_welcome_msg', Boolean),
+ has_field('subject_prefix', Unicode),
+ has_field('subscribe_auto_approval', PickleType),
+ has_field('subscribe_policy', Integer),
+ has_field('topics', PickleType),
+ has_field('topics_bodylines_limit', Integer),
+ has_field('topics_enabled', Boolean),
+ has_field('umbrella_list', Boolean),
+ has_field('umbrella_member_suffix', Unicode),
+ has_field('unsubscribe_policy', Integer),
+ has_field('welcome_msg', Unicode),
+ # Indirect relationships
+ has_field('owner_rosterset', Unicode),
+ has_field('moderator_rosterset', Unicode),
+ # Relationships
+## has_and_belongs_to_many(
+## 'available_languages',
+## of_kind='Mailman.database.model.languages.Language')
+
+ def __init__(self, fqdn_listname):
+ super(MailingList, self).__init__()
+ listname, hostname = split_listname(fqdn_listname)
+ self.list_name = listname
+ self.host_name = hostname
+ # Create two roster sets, one for the owners and one for the
+ # moderators. MailingLists are connected to RosterSets indirectly, in
+ # order to preserve the ability to store user data and list data in
+ # different databases.
+ name = fqdn_listname + ' owners'
+ self.owner_rosterset = name
+ roster = config.user_manager.create_roster(name)
+ config.user_manager.create_rosterset(name).add(roster)
+ name = fqdn_listname + ' moderators'
+ self.moderator_rosterset = name
+ roster = config.user_manager.create_roster(name)
+ config.user_manager.create_rosterset(name).add(roster)
+
+ def delete_rosters(self):
+ listname = fqdn_listname(self.list_name, self.host_name)
+ # Delete the list owner roster and roster set.
+ name = listname + ' owners'
+ roster = config.user_manager.get_roster(name)
+ assert roster, 'Missing roster: %s' % name
+ config.user_manager.delete_roster(roster)
+ rosterset = config.user_manager.get_rosterset(name)
+ assert rosterset, 'Missing roster set: %s' % name
+ config.user_manager.delete_rosterset(rosterset)
+ name = listname + ' moderators'
+ roster = config.user_manager.get_roster(name)
+ assert roster, 'Missing roster: %s' % name
+ config.user_manager.delete_roster(roster)
+ rosterset = config.user_manager.get_rosterset(name)
+ assert rosterset, 'Missing roster set: %s' % name
+ config.user_manager.delete_rosterset(rosterset)
+
+ # IMailingListRosters
+
+ @property
+ def owners(self):
+ for user in _collect_users(self.owner_rosterset):
+ yield user
+
+ @property
+ def moderators(self):
+ for user in _collect_users(self.moderator_rosterset):
+ yield user
+
+ @property
+ def administrators(self):
+ for user in _collect_users(self.owner_rosterset,
+ self.moderator_rosterset):
+ yield user
+
+ @property
+ def owner_rosters(self):
+ rosterset = config.user_manager.get_rosterset(self.owner_rosterset)
+ for roster in rosterset.rosters:
+ yield roster
+
+ @property
+ def moderator_rosters(self):
+ rosterset = config.user_manager.get_rosterset(self.moderator_rosterset)
+ for roster in rosterset.rosters:
+ yield roster
+
+ def add_owner_roster(self, roster):
+ rosterset = config.user_manager.get_rosterset(self.owner_rosterset)
+ rosterset.add(roster)
+
+ def delete_owner_roster(self, roster):
+ rosterset = config.user_manager.get_rosterset(self.owner_rosterset)
+ rosterset.delete(roster)
+
+ def add_moderator_roster(self, roster):
+ rosterset = config.user_manager.get_rosterset(self.moderator_rosterset)
+ rosterset.add(roster)
+
+ def delete_moderator_roster(self, roster):
+ rosterset = config.user_manager.get_rosterset(self.moderator_rosterset)
+ rosterset.delete(roster)
+
+
+
+def _collect_users(*rosterset_names):
+ users = set()
+ for name in rosterset_names:
+ # We have to indirectly look up the roster set's name in the user
+ # manager. This is how we enforce separation between the list manager
+ # and the user manager storages.
+ rosterset = config.user_manager.get_rosterset(name)
+ assert rosterset is not None, 'No RosterSet named: %s' % name
+ for roster in rosterset.rosters:
+ # Rosters collect addresses. It's not required that an address is
+ # linked to a user, but it must be the case that all addresses on
+ # the owner roster are linked to a user. Get the user that's
+ # linked to each address and add it to the set.
+ for address in roster.addresses:
+ user = config.user_manager.get_user(address.address)
+ assert user is not None, 'Unlinked address: ' + address.address
+ users.add(user)
+ return users
diff --git a/Mailman/database/model/profile.py b/Mailman/database/model/profile.py
new file mode 100644
index 000000000..49e108728
--- /dev/null
+++ b/Mailman/database/model/profile.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2006-2007 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.
+
+from elixir import *
+from email.utils import formataddr
+from zope.interface import implements
+
+from Mailman.constants import DeliveryMode
+from Mailman.database.types import EnumType
+from Mailman.interfaces import IProfile
+
+
+class Profile(Entity):
+ implements(IProfile)
+
+ has_field('acknowledge_posts', Boolean)
+ has_field('hide_address', Boolean)
+ has_field('preferred_language', Unicode)
+ has_field('receive_list_copy', Boolean)
+ has_field('receive_own_postings', Boolean)
+ has_field('delivery_mode', EnumType)
+ # Relationships
+ belongs_to('user', of_kind='Mailman.database.model.user.User')
+
+ def __init__(self):
+ super(Profile, self).__init__()
+ self.acknowledge_posts = False
+ self.hide_address = True
+ self.preferred_language = 'en'
+ self.receive_list_copy = True
+ self.receive_own_postings = True
+ self.delivery_mode = DeliveryMode.regular
diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py
new file mode 100644
index 000000000..bf8447433
--- /dev/null
+++ b/Mailman/database/model/roster.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2006-2007 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.
+
+from elixir import *
+from zope.interface import implements
+
+from Mailman.Errors import ExistingAddressError
+from Mailman.interfaces import IRoster
+
+
+ADDRESS_KIND = 'Mailman.database.model.address.Address'
+ROSTERSET_KIND = 'Mailman.database.model.rosterset.RosterSet'
+
+
+class Roster(Entity):
+ implements(IRoster)
+
+ has_field('name', Unicode)
+ # Relationships
+ has_and_belongs_to_many('addresses', of_kind=ADDRESS_KIND)
+ has_and_belongs_to_many('roster_set', of_kind=ROSTERSET_KIND)
+
+ def create(self, email_address, real_name=None):
+ """See IRoster"""
+ from Mailman.database.model.address import Address
+ addr = Address.get_by(address=email_address)
+ if addr:
+ raise ExistingAddressError(email_address)
+ addr = Address(address=email_address, real_name=real_name)
+ # Make sure all the expected links are made, including to the null
+ # (i.e. everyone) roster.
+ self.addresses.append(addr)
+ addr.rosters.append(self)
+ null_roster = Roster.get_by(name='')
+ null_roster.addresses.append(addr)
+ addr.rosters.append(null_roster)
+ return addr
diff --git a/Mailman/database/model/rosterset.py b/Mailman/database/model/rosterset.py
new file mode 100644
index 000000000..f84b52c15
--- /dev/null
+++ b/Mailman/database/model/rosterset.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2006-2007 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.
+
+from elixir import *
+from zope.interface import implements
+
+from Mailman.interfaces import IRosterSet
+
+ROSTER_KIND = 'Mailman.database.model.roster.Roster'
+
+
+
+# Internal implementation of roster sets for use with mailing lists. These
+# are owned by the user storage.
+class RosterSet(Entity):
+ implements(IRosterSet)
+
+ has_field('name', Unicode)
+ has_and_belongs_to_many('rosters', of_kind=ROSTER_KIND)
+
+ def add(self, roster):
+ if roster not in self.rosters:
+ self.rosters.append(roster)
+
+ def delete(self, roster):
+ if roster in self.rosters:
+ self.rosters.remove(roster)
diff --git a/Mailman/database/model/user.py b/Mailman/database/model/user.py
new file mode 100644
index 000000000..be634b9df
--- /dev/null
+++ b/Mailman/database/model/user.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2006-2007 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.
+
+from elixir import *
+from email.utils import formataddr
+from zope.interface import implements
+
+from Mailman import Errors
+from Mailman.database.model import Address
+from Mailman.interfaces import IUser
+
+
+class User(Entity):
+ implements(IUser)
+
+ has_field('real_name', Unicode)
+ has_field('password', Unicode)
+ # Relationships
+ has_one('profile', of_kind='Mailman.database.model.profile.Profile')
+ has_many('addresses', of_kind='Mailman.database.model.address.Address')
+
+ def link(self, address):
+ if address.user is not None:
+ raise Errors.AddressAlreadyLinkedError(address)
+ address.user = self
+ self.addresses.append(address)
+
+ def unlink(self, address):
+ if address.user is None:
+ raise Errors.AddressNotLinkedError(address)
+ address.user = None
+ self.addresses.remove(address)
+
+ def controls(self, address):
+ found = Address.get_by(address=address.address)
+ return bool(found and found.user is self)
diff --git a/Mailman/database/model/version.py b/Mailman/database/model/version.py
new file mode 100644
index 000000000..e22e8ae11
--- /dev/null
+++ b/Mailman/database/model/version.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2007 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.
+
+from elixir import *
+
+
+class Version(Entity):
+ with_fields(
+ component = Field(String),
+ version = Field(Integer),
+ )
diff --git a/Mailman/database/tables/Makefile.in b/Mailman/database/tables/Makefile.in
new file mode 100644
index 000000000..dd99e125f
--- /dev/null
+++ b/Mailman/database/tables/Makefile.in
@@ -0,0 +1,71 @@
+# Copyright (C) 2007 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.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+DESTDIR=
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+PACKAGEDIR= $(prefix)/Mailman/database/tables
+SHELL= /bin/sh
+
+MODULES= *.py
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+
+# Rules
+
+all:
+
+install:
+ for f in $(MODULES); \
+ do \
+ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm *.pyc
+ -rm Makefile
diff --git a/Mailman/database/tables/__init__.py b/Mailman/database/tables/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/Mailman/database/tables/__init__.py
diff --git a/Mailman/database/tables/addresses.py b/Mailman/database/tables/addresses.py
new file mode 100644
index 000000000..922984646
--- /dev/null
+++ b/Mailman/database/tables/addresses.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2006-2007 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.
+
+"""Email addresses."""
+
+from sqlalchemy import *
+
+
+
+class Address(object):
+ pass
+
+
+def make_table(metadata, tables):
+ address_table = Table(
+ 'Addresses', metadata,
+ Column('address_id', Integer, primary_key=True),
+ Column('profile_id', Integer, ForeignKey('Profiles.profile_id')),
+ Column('address', Unicode),
+ Column('verified', Boolean),
+ Column('bounce_info', PickleType),
+ )
+ # Associate Rosters
+ address_rosters_table = Table(
+ 'AddressRoster', metadata,
+ Column('roster_id', Integer, ForeignKey('Rosters.roster_id')),
+ Column('address_id', Integer, ForeignKey('Addresses.address_id')),
+ )
+ mapper(Address, address_table)
+ tables.bind(address_table)
+ tables.bind(address_rosters_table, 'address_rosters')
diff --git a/Mailman/database/languages.py b/Mailman/database/tables/languages.py
index 6032a67df..fa10974b7 100644
--- a/Mailman/database/languages.py
+++ b/Mailman/database/tables/languages.py
@@ -37,16 +37,16 @@ class Language(object):
def make_table(metadata, tables):
language_table = Table(
- 'Language', metadata,
+ 'Languages', metadata,
# Two letter language code
Column('language_id', Integer, primary_key=True),
Column('code', Unicode),
)
# Associate List
available_languages_table = Table(
- 'AvailableLanguage', metadata,
+ 'AvailableLanguages', metadata,
Column('list_id', Integer, ForeignKey('Listdata.list_id')),
- Column('language_id', Integer, ForeignKey('Language.language_id')),
+ Column('language_id', Integer, ForeignKey('Languages.language_id')),
)
mapper(Language, language_table)
tables.bind(language_table)
diff --git a/Mailman/database/listdata.py b/Mailman/database/tables/listdata.py
index 9f537b229..fff396980 100644
--- a/Mailman/database/listdata.py
+++ b/Mailman/database/tables/listdata.py
@@ -29,6 +29,12 @@ def make_table(metadata, tables):
Column('list_name', Unicode),
Column('web_page_url', Unicode),
Column('admin_member_chunksize', Integer),
+ # Foreign keys - XXX ondelete='all, delete=orphan' ??
+ Column('owner', Integer, ForeignKey('RosterSets.rosterset_id')),
+ Column('moderator', Integer, ForeignKey('RosterSets.rosterset_id')),
+ # Attributes which are directly modifiable via the web u/i. The more
+ # complicated attributes are currently stored as pickles, though that
+ # will change as the schema and implementation is developed.
Column('next_request_id', Integer),
Column('next_digest_number', Integer),
Column('admin_responses', PickleType),
@@ -116,7 +122,6 @@ def make_table(metadata, tables):
Column('member_moderation_notice', Unicode),
Column('mime_is_default_digest', Boolean),
Column('mod_password', Unicode),
- Column('moderator', PickleType),
Column('msg_footer', Unicode),
Column('msg_header', Unicode),
Column('new_member_options', Integer),
@@ -126,7 +131,6 @@ def make_table(metadata, tables):
Column('nondigestable', Boolean),
Column('nonmember_rejection_notice', Unicode),
Column('obscure_addresses', Boolean),
- Column('owner', PickleType),
Column('pass_filename_extensions', PickleType),
Column('pass_mime_types', PickleType),
Column('password', Unicode),
@@ -157,14 +161,18 @@ def make_table(metadata, tables):
)
# Avoid circular imports
from Mailman.MailList import MailList
- from Mailman.database.languages import Language
+ from Mailman.database.tables.languages import Language
+ from Mailman.database.tables.rosters import RosterSet
# We need to ensure MailList.InitTempVars() is called whenever a MailList
# instance is created from a row. Use a mapper extension for this.
- props = dict(available_languages=
- relation(Language,
- secondary=tables.available_languages,
- lazy=False))
+ props = dict(
+ # listdata* <-> language*
+ available_languages= relation(Language,
+ secondary=tables.available_languages,
+ lazy=False))
mapper(MailList, table,
+ # The mapper extension ensures MailList.InitTempVars() is called
+ # whenever a MailList instance is created from a row.
extension=MailListMapperExtension(),
properties=props)
tables.bind(table)
diff --git a/Mailman/database/tables/profiles.py b/Mailman/database/tables/profiles.py
new file mode 100644
index 000000000..9f65bdf03
--- /dev/null
+++ b/Mailman/database/tables/profiles.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2007 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.
+
+"""Mailman user profile information."""
+
+from sqlalchemy import *
+
+from Mailman import Defaults
+
+
+
+class Profile(object):
+ pass
+
+
+
+# Both of these Enum types are stored in the database as integers, and
+# converted back into their enums on retrieval.
+
+class DeliveryModeType(types.TypeDecorator):
+ impl = types.Integer
+
+ def convert_bind_param(self, value, engine):
+ return int(value)
+
+ def convert_result_value(self, value, engine):
+ return Defaults.DeliveryMode(value)
+
+
+class DeliveryStatusType(types.TypeDecorator):
+ impl = types.Integer
+
+ def convert_bind_param(self, value, engine):
+ return int(value)
+
+ def convert_result_value(self, value, engine):
+ return Defaults.DeliveryStatus(value)
+
+
+
+def make_table(metadata, tables):
+ table = Table(
+ 'Profiles', metadata,
+ Column('profile_id', Integer, primary_key=True),
+ # OldStyleMemberships attributes, temporarily stored as pickles.
+ Column('ack', Boolean),
+ Column('delivery_mode', DeliveryModeType),
+ Column('delivery_status', DeliveryStatusType),
+ Column('hide', Boolean),
+ Column('language', Unicode),
+ Column('nodupes', Boolean),
+ Column('nomail', Boolean),
+ Column('notmetoo', Boolean),
+ Column('password', Unicode),
+ Column('realname', Unicode),
+ Column('topics', PickleType),
+ )
+ # Avoid circular references
+ from Mailman.database.tables.addresses import Address
+ # profile -> address*
+ props = dict(addresses=relation(Address, cascade='all, delete-orphan'))
+ mapper(Profile, table, properties=props)
+ tables.bind(table)
diff --git a/Mailman/database/tables/rosters.py b/Mailman/database/tables/rosters.py
new file mode 100644
index 000000000..eea0cbb39
--- /dev/null
+++ b/Mailman/database/tables/rosters.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2007 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.
+
+"""Collections of email addresses.
+
+Rosters contain email addresses. RosterSets contain Rosters. Most attributes
+on the listdata table take RosterSets so that it's easy to compose just about
+any combination of addresses.
+"""
+
+from sqlalchemy import *
+
+from Mailman.database.tables.addresses import Address
+
+
+
+class Roster(object):
+ pass
+
+
+class RosterSet(object):
+ pass
+
+
+
+def make_table(metadata, tables):
+ table = Table(
+ 'Rosters', metadata,
+ Column('roster_id', Integer, primary_key=True),
+ )
+ # roster* <-> address*
+ props = dict(addresses=
+ relation(Address,
+ secondary=tables.address_rosters,
+ lazy=False))
+ mapper(Roster, table, properties=props)
+ tables.bind(table)
+ table = Table(
+ 'RosterSets', metadata,
+ Column('rosterset_id', Integer, primary_key=True),
+ )
+ # rosterset -> roster*
+ props = dict(rosters=relation(Roster, cascade='all, delete=orphan'))
+ mapper(RosterSet, table, properties=props)
+ tables.bind(table)
diff --git a/Mailman/database/version.py b/Mailman/database/tables/versions.py
index 57c50b0ef..09fd21bf7 100644
--- a/Mailman/database/version.py
+++ b/Mailman/database/tables/versions.py
@@ -23,9 +23,9 @@ from sqlalchemy import *
def make_table(metadata, tables):
table = Table(
- 'Version', metadata,
+ 'Versions', metadata,
Column('version_id', Integer, primary_key=True),
- Column('component', String(20)),
+ Column('component', String),
Column('version', Integer),
)
tables.bind(table)
diff --git a/Mailman/database/types.py b/Mailman/database/types.py
new file mode 100644
index 000000000..00ad29559
--- /dev/null
+++ b/Mailman/database/types.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2007 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.
+
+import sys
+
+from sqlalchemy import types
+
+
+
+# SQLAlchemy custom type for storing enums in the database.
+class EnumType(types.TypeDecorator):
+ # Enums can be stored as strings of the form:
+ # full.path.to.Enum:intval
+ impl = types.String
+
+ def convert_bind_param(self, value, engine):
+ return '%s:%s.%d' % (value.enumclass.__module__,
+ value.enumclass.__name__,
+ int(value))
+
+ def convert_result_value(self, value, engine):
+ path, intvalue = value.rsplit(':', 1)
+ modulename, classname = intvalue.rsplit('.', 1)
+ __import__(modulename)
+ cls = getattr(sys.modules[modulename], classname)
+ return cls[int(intvalue)]
diff --git a/Mailman/database/usermanager.py b/Mailman/database/usermanager.py
new file mode 100644
index 000000000..97a740803
--- /dev/null
+++ b/Mailman/database/usermanager.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2007 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.
+
+"""SQLAlchemy/Elixir based provider of IUserManager."""
+
+from __future__ import with_statement
+
+import os
+
+from elixir import *
+from zope.interface import implements
+
+from Mailman import Errors
+from Mailman.LockFile import LockFile
+from Mailman.configuration import config
+from Mailman.database.model import *
+from Mailman.interfaces import IUserManager
+
+
+
+class UserManager(object):
+ implements(IUserManager)
+
+ def __init__(self):
+ # Create the null roster if it does not already exist. It's more
+ # likely to exist than not so try to get it before creating it.
+ lockfile = os.path.join(config.LOCK_DIR, '<umgrcreatelock>')
+ with LockFile(lockfile):
+ roster = self.get_roster('')
+ if roster is None:
+ self.create_roster('')
+ objectstore.flush()
+
+ def create_roster(self, name):
+ roster = Roster.get_by(name=name)
+ if roster:
+ raise Errors.RosterExistsError(name)
+ return Roster(name=name)
+
+ def get_roster(self, name):
+ return Roster.get_by(name=name)
+
+ def delete_roster(self, roster):
+ roster.delete()
+
+ @property
+ def rosters(self):
+ for roster in Roster.select():
+ yield roster
+
+ def create_rosterset(self, name):
+ return RosterSet(name=name)
+
+ def delete_rosterset(self, rosterset):
+ rosterset.delete()
+
+ def get_rosterset(self, name):
+ return RosterSet.get_by(name=name)
+
+ def create_user(self):
+ user = User()
+ # Users always have a profile
+ user.profile = Profile()
+ user.profile.user = user
+ return user
+
+ def delete_user(self, user):
+ user.delete()
+
+ @property
+ def users(self):
+ for user in User.select():
+ yield user
+
+ def get_user(self, address):
+ found = Address.get_by(address=address)
+ return found and found.user
diff --git a/Mailman/docs/Makefile.in b/Mailman/docs/Makefile.in
new file mode 100644
index 000000000..0662d8a3e
--- /dev/null
+++ b/Mailman/docs/Makefile.in
@@ -0,0 +1,82 @@
+# Copyright (C) 1998-2007 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.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+DESTDIR=
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+PACKAGEDIR= $(prefix)/Mailman/docs
+SHELL= /bin/sh
+
+OTHERFILES= *.txt
+MODULES= *.py
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+# Directories make should decend into
+SUBDIRS=
+
+# Rules
+
+all:
+
+install:
+ for f in $(MODULES) $(OTHERFILES); \
+ do \
+ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
+ done
+ for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) DESTDIR=$(DESTDIR) install); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm *.pyc
+ -rm Makefile
+ @for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) distclean); \
+ done
diff --git a/Mailman/docs/__init__.py b/Mailman/docs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/Mailman/docs/__init__.py
diff --git a/Mailman/docs/addresses.txt b/Mailman/docs/addresses.txt
new file mode 100644
index 000000000..a8cf9f655
--- /dev/null
+++ b/Mailman/docs/addresses.txt
@@ -0,0 +1,144 @@
+Email addresses and rosters
+===========================
+
+Addresses represent email address, and nothing more. Some addresses are tied
+to users that Mailman knows about. For example, a list member is a user that
+the system knows about, but a non-member posting from a brand new email
+address is a counter-example.
+
+
+Creating a roster
+-----------------
+
+Email address objects are tied to rosters, and rosters are tied to the user
+manager. To get things started, access the global user manager and create a
+new roster.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> mgr = config.user_manager
+ >>> roster_1 = mgr.create_roster('roster-1')
+ >>> sorted(roster_1.addresses)
+ []
+
+
+Creating addresses
+------------------
+
+Creating a simple email address object is straight forward.
+
+ >>> addr_1 = roster_1.create('aperson@example.com')
+ >>> flush()
+ >>> addr_1.address
+ 'aperson@example.com'
+ >>> addr_1.real_name is None
+ True
+
+You can also create an email address object with a real name.
+
+ >>> addr_2 = roster_1.create('bperson@example.com', 'Barney Person')
+ >>> addr_2.address
+ 'bperson@example.com'
+ >>> addr_2.real_name
+ 'Barney Person'
+
+You can also iterate through all the addresses on a roster.
+
+ >>> sorted(addr.address for addr in roster_1.addresses)
+ ['aperson@example.com', 'bperson@example.com']
+
+You can create another roster and add a bunch of existing addresses to the
+second roster.
+
+ >>> roster_2 = mgr.create_roster('roster-2')
+ >>> flush()
+ >>> sorted(roster_2.addresses)
+ []
+ >>> for address in roster_1.addresses:
+ ... roster_2.addresses.append(address)
+ >>> roster_2.create('cperson@example.com', 'Charlie Person')
+ <Address: Charlie Person <cperson@example.com> [not verified]>
+ >>> sorted(addr.address for addr in roster_2.addresses)
+ ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
+
+The first roster hasn't been affected.
+
+ >>> sorted(addr.address for addr in roster_1.addresses)
+ ['aperson@example.com', 'bperson@example.com']
+
+
+Removing addresses
+------------------
+
+You can remove an address from a roster just by deleting it.
+
+ >>> for addr in roster_1.addresses:
+ ... if addr.address == 'aperson@example.com':
+ ... break
+ >>> addr.address
+ 'aperson@example.com'
+ >>> roster_1.addresses.remove(addr)
+ >>> sorted(addr.address for addr in roster_1.addresses)
+ ['bperson@example.com']
+
+Again, this doesn't affect the other rosters.
+
+ >>> sorted(addr.address for addr in roster_2.addresses)
+ ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
+
+
+Registration and validation
+---------------------------
+
+Addresses have two dates, the date the address was registered on and the date
+the address was validated on. Neither date isset by default.
+
+ >>> addr = roster_1.create('dperson@example.com', 'David Person')
+ >>> addr.registered_on is None
+ True
+ >>> addr.validated_on is None
+ True
+
+The registered date takes a Python datetime object.
+
+ >>> from datetime import datetime
+ >>> addr.registered_on = datetime(2007, 5, 8, 22, 54, 1)
+ >>> print addr.registered_on
+ 2007-05-08 22:54:01
+ >>> addr.validated_on is None
+ True
+
+And of course, you can also set the validation date.
+
+ >>> addr.validated_on = datetime(2007, 5, 13, 22, 54, 1)
+ >>> print addr.registered_on
+ 2007-05-08 22:54:01
+ >>> print addr.validated_on
+ 2007-05-13 22:54:01
+
+
+The null roster
+---------------
+
+All address objects that have been created are members of the null roster.
+
+ >>> all = mgr.get_roster('')
+ >>> sorted(addr.address for addr in all.addresses)
+ ['aperson@example.com', 'bperson@example.com',
+ 'cperson@example.com', 'dperson@example.com']
+
+And conversely, all addresses should have the null roster on their list of
+rosters.
+
+ >>> for addr in all.addresses:
+ ... assert all in addr.rosters, 'Address is missing null roster'
+
+
+Clean up
+--------
+
+ >>> for roster in mgr.rosters:
+ ... mgr.delete_roster(roster)
+ >>> flush()
+ >>> sorted(roster.name for roster in mgr.rosters)
+ []
diff --git a/Mailman/docs/mlist-addresses.txt b/Mailman/docs/mlist-addresses.txt
new file mode 100644
index 000000000..257cf95c7
--- /dev/null
+++ b/Mailman/docs/mlist-addresses.txt
@@ -0,0 +1,85 @@
+Mailing list addresses
+======================
+
+Every mailing list has a number of addresses which are publicly available.
+These are defined in the IMailingListAddresses interface.
+
+ >>> from Mailman.configuration import config
+ >>> from Mailman.interfaces import IMailingListAddresses
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> IMailingListAddresses.providedBy(mlist)
+ True
+
+The posting address is where people send messages to be posted to the mailing
+list. This is exactly the same as the fully qualified list name.
+
+ >>> mlist.fqdn_listname
+ '_xtest@example.com'
+ >>> mlist.posting_address
+ '_xtest@example.com'
+
+Messages to the mailing list's 'no reply' address always get discarded without
+prejudice.
+
+ >>> mlist.noreply_address
+ 'noreply@example.com'
+
+The mailing list's owner address reaches the human moderators.
+
+ >>> mlist.owner_address
+ '_xtest-owner@example.com'
+
+The request address goes to the list's email command robot.
+
+ >>> mlist.request_address
+ '_xtest-request@example.com'
+
+The bounces address accepts and processes all potential bounces.
+
+ >>> mlist.bounces_address
+ '_xtest-bounces@example.com'
+
+The join (a.k.a. subscribe) address is where someone can email to get added to
+the mailing list. The subscribe alias is a synonym for join, but it's
+deprecated.
+
+ >>> mlist.join_address
+ '_xtest-join@example.com'
+ >>> mlist.subscribe_address
+ '_xtest-subscribe@example.com'
+
+The leave (a.k.a. unsubscribe) address is where someone can email to get added
+to the mailing list. The unsubscribe alias is a synonym for leave, but it's
+deprecated.
+
+ >>> mlist.leave_address
+ '_xtest-leave@example.com'
+ >>> mlist.unsubscribe_address
+ '_xtest-unsubscribe@example.com'
+
+
+Email confirmations
+-------------------
+
+Email confirmation messages are sent when actions such as subscriptions need
+to be confirmed. It requires that a cookie be provided, which will be
+included in the local part of the email address. The exact format of this is
+dependent on the VERP_CONFIRM_FORMAT configuration variable.
+
+ >>> mlist.confirm_address('cookie')
+ '_xtest-confirm+cookie@example.com'
+ >>> mlist.confirm_address('wookie')
+ '_xtest-confirm+wookie@example.com'
+
+ >>> old_format = config.VERP_CONFIRM_FORMAT
+ >>> config.VERP_CONFIRM_FORMAT = '$address---$cookie'
+ >>> mlist.confirm_address('cookie')
+ '_xtest-confirm---cookie@example.com'
+ >>> config.VERP_CONFIRM_FORMAT = old_format
+
+
+Clean up
+--------
+
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
diff --git a/Mailman/docs/mlist-rosters.txt b/Mailman/docs/mlist-rosters.txt
new file mode 100644
index 000000000..490a07e0c
--- /dev/null
+++ b/Mailman/docs/mlist-rosters.txt
@@ -0,0 +1,118 @@
+Mailing list rosters
+====================
+
+Mailing lists use rosters to manage and organize users for various purposes.
+In order to allow for separate storage of mailing list data and user data, the
+connection between mailing list objects and rosters is indirect. Mailing
+lists manage roster names, and these roster names are used to find the rosters
+that contain the actual users.
+
+
+Privileged rosters
+------------------
+
+Mailing lists have two types of privileged users, owners and moderators.
+Owners get to change the configuration of mailing lists and moderators get to
+approve or deny held messages and subscription requests.
+
+When a mailing list is created, it automatically contains a roster for the
+list owners and a roster for the list moderators.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> flush()
+ >>> sorted(roster.name for roster in mlist.owner_rosters)
+ ['_xtest@example.com owners']
+ >>> sorted(roster.name for roster in mlist.moderator_rosters)
+ ['_xtest@example.com moderators']
+
+These rosters are initially empty.
+
+ >>> owner_roster = list(mlist.owner_rosters)[0]
+ >>> sorted(address for address in owner_roster.addresses)
+ []
+ >>> moderator_roster = list(mlist.moderator_rosters)[0]
+ >>> sorted(address for address in moderator_roster.addresses)
+ []
+
+You can create new rosters and add them to the list of owner or moderator
+rosters.
+
+ >>> roster_1 = config.user_manager.create_roster('roster-1')
+ >>> roster_2 = config.user_manager.create_roster('roster-2')
+ >>> roster_3 = config.user_manager.create_roster('roster-3')
+ >>> flush()
+
+Make roster-1 an owner roster, roster-2 a moderator roster, and roster-3 both
+an owner and a moderator roster.
+
+ >>> mlist.add_owner_roster(roster_1)
+ >>> mlist.add_moderator_roster(roster_2)
+ >>> mlist.add_owner_roster(roster_3)
+ >>> mlist.add_moderator_roster(roster_3)
+ >>> flush()
+
+ >>> sorted(roster.name for roster in mlist.owner_rosters)
+ ['_xtest@example.com owners', 'roster-1', 'roster-3']
+ >>> sorted(roster.name for roster in mlist.moderator_rosters)
+ ['_xtest@example.com moderators', 'roster-2', 'roster-3']
+
+
+Privileged users
+----------------
+
+Rosters are the lower level way of managing owners and moderators, but usually
+you just want to know which users have owner and moderator privileges. You
+can get the list of such users by using different attributes.
+
+Because the rosters are all empty to start with, we can create a bunch of
+users that will end up being our owners and moderators.
+
+ >>> aperson = config.user_manager.create_user()
+ >>> bperson = config.user_manager.create_user()
+ >>> cperson = config.user_manager.create_user()
+
+These users need addresses, because rosters manage addresses.
+
+ >>> address_1 = roster_1.create('aperson@example.com', 'Anne Person')
+ >>> aperson.link(address_1)
+ >>> address_2 = roster_2.create('bperson@example.com', 'Ben Person')
+ >>> bperson.link(address_2)
+ >>> address_3 = roster_1.create('cperson@example.com', 'Claire Person')
+ >>> cperson.link(address_3)
+ >>> roster_3.addresses.append(address_3)
+ >>> flush()
+
+Now that everything is set up, we can iterate through the various collections
+of privileged users. Here are the owners of the list.
+
+ >>> from Mailman.interfaces import IUser
+ >>> addresses = []
+ >>> for user in mlist.owners:
+ ... assert IUser.providedBy(user), 'Non-IUser owner found'
+ ... for address in user.addresses:
+ ... addresses.append(address.address)
+ >>> sorted(addresses)
+ ['aperson@example.com', 'cperson@example.com']
+
+Here are the moderators of the list.
+
+ >>> addresses = []
+ >>> for user in mlist.moderators:
+ ... assert IUser.providedBy(user), 'Non-IUser moderator found'
+ ... for address in user.addresses:
+ ... addresses.append(address.address)
+ >>> sorted(addresses)
+ ['bperson@example.com', 'cperson@example.com']
+
+The administrators of a mailing list are the union of the owners and
+moderators.
+
+ >>> addresses = []
+ >>> for user in mlist.administrators:
+ ... assert IUser.providedBy(user), 'Non-IUser administrator found'
+ ... for address in user.addresses:
+ ... addresses.append(address.address)
+ >>> sorted(addresses)
+ ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
diff --git a/Mailman/docs/use-listmanager.txt b/Mailman/docs/use-listmanager.txt
new file mode 100644
index 000000000..9e237f02f
--- /dev/null
+++ b/Mailman/docs/use-listmanager.txt
@@ -0,0 +1,124 @@
+Using the IListManager interface
+================================
+
+The IListManager is how you create, delete, and retrieve mailing list
+objects. The Mailman system instantiates an IListManager for you based on the
+configuration variable MANAGERS_INIT_FUNCTION. The instance is accessible
+on the global config object.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> from Mailman.interfaces import IListManager
+ >>> IListManager.providedBy(config.list_manager)
+ True
+ >>> mgr = config.list_manager
+
+
+Creating a mailing list
+-----------------------
+
+Creating the list returns the newly created IMailList object.
+
+ >>> from Mailman.interfaces import IMailingList
+ >>> mlist = mgr.create('_xtest@example.com')
+ >>> flush()
+ >>> IMailingList.providedBy(mlist)
+ True
+
+This object has an identity.
+
+ >>> from Mailman.interfaces import IMailingListIdentity
+ >>> IMailingListIdentity.providedBy(mlist)
+ True
+
+All lists with identities have a short name, a host name, and a fully
+qualified listname. This latter is what uniquely distinguishes the mailing
+list to the system.
+
+ >>> mlist.list_name
+ '_xtest'
+ >>> mlist.host_name
+ 'example.com'
+ >>> mlist.fqdn_listname
+ '_xtest@example.com'
+
+If you try to create a mailing list with the same name as an existing list,
+you will get an exception.
+
+ >>> mlist_dup = mgr.create('_xtest@example.com')
+ Traceback (most recent call last):
+ ...
+ MMListAlreadyExistsError: _xtest@example.com
+
+
+Deleting a mailing list
+-----------------------
+
+Deleting an existing mailing list also deletes its rosters and roster sets.
+
+ >>> sorted(r.name for r in config.user_manager.rosters)
+ ['', '_xtest@example.com moderators', '_xtest@example.com owners']
+
+ >>> mgr.delete(mlist)
+ >>> flush()
+ >>> sorted(mgr.names)
+ []
+ >>> sorted(r.name for r in config.user_manager.rosters)
+ ['']
+
+Attempting to access attributes of the deleted mailing list raises an
+exception:
+
+ >>> mlist.fqdn_listname
+ Traceback (most recent call last):
+ ...
+ AttributeError: fqdn_listname
+
+After deleting the list, you can create it again.
+
+ >>> mlist = mgr.create('_xtest@example.com')
+ >>> flush()
+ >>> mlist.fqdn_listname
+ '_xtest@example.com'
+
+
+Retrieving a mailing list
+-------------------------
+
+When a mailing list exists, you can ask the list manager for it and you will
+always get the same object back.
+
+ >>> mlist_2 = mgr.get('_xtest@example.com')
+ >>> mlist_2 is mlist
+ True
+
+Don't try to get a list that doesn't exist yet though, or you will get an
+exception.
+
+ >>> mgr.get('_xtest_2@example.com')
+ Traceback (most recent call last):
+ ...
+ MMUnknownListError: _xtest_2@example.com
+
+
+Iterating over all mailing lists
+--------------------------------
+
+Once you've created a bunch of mailing lists, you can use the list manager to
+iterate over either the list objects, or the list names.
+
+ >>> mlist_3 = mgr.create('_xtest_3@example.com')
+ >>> mlist_4 = mgr.create('_xtest_4@example.com')
+ >>> flush()
+ >>> sorted(mgr.names)
+ ['_xtest@example.com', '_xtest_3@example.com', '_xtest_4@example.com']
+ >>> sorted(m.fqdn_listname for m in mgr.mailing_lists)
+ ['_xtest@example.com', '_xtest_3@example.com', '_xtest_4@example.com']
+
+
+Cleaning up
+-----------
+
+ >>> for mlist in mgr.mailing_lists:
+ ... mgr.delete(mlist)
+ >>> flush()
diff --git a/Mailman/docs/use-usermanager.txt b/Mailman/docs/use-usermanager.txt
new file mode 100644
index 000000000..f79bff8c6
--- /dev/null
+++ b/Mailman/docs/use-usermanager.txt
@@ -0,0 +1,100 @@
+The user manager and rosters
+============================
+
+The IUserManager is how you create, delete, and roster objects. Rosters
+manage collections of users. The Mailman system instantiates an IUserManager
+for you based on the configuration variable MANAGERS_INIT_FUNCTION. The
+instance is accessible on the global config object.
+
+ >>> from Mailman.configuration import config
+ >>> from Mailman.interfaces import IUserManager
+ >>> mgr = config.user_manager
+ >>> IUserManager.providedBy(mgr)
+ True
+
+
+The default roster
+------------------
+
+The user manager always contains at least one roster, the 'null' roster or
+'all inclusive roster'.
+
+ >>> sorted(roster.name for roster in mgr.rosters)
+ ['']
+
+
+Adding rosters
+--------------
+
+You create a roster to hold users. The only thing a roster needs is a name,
+basically just an identifying string.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.interfaces import IRoster
+ >>> roster = mgr.create_roster('roster-1')
+ >>> IRoster.providedBy(roster)
+ True
+ >>> roster.name
+ 'roster-1'
+ >>> flush()
+
+If you try to create a roster with the same name as an existing roster, you
+will get an exception.
+
+ >>> roster_dup = mgr.create_roster('roster-1')
+ Traceback (most recent call last):
+ ...
+ RosterExistsError: roster-1
+
+
+Deleting a roster
+-----------------
+
+Delete the roster, and you can then create it again.
+
+ >>> mgr.delete_roster(roster)
+ >>> flush()
+ >>> roster = mgr.create_roster('roster-1')
+ >>> flush()
+ >>> roster.name
+ 'roster-1'
+
+
+Retrieving a roster
+-------------------
+
+When a roster exists, you can ask the user manager for it and you will always
+get the same object back.
+
+ >>> roster_2 = mgr.get_roster('roster-1')
+ >>> roster_2.name
+ 'roster-1'
+ >>> roster is roster_2
+ True
+
+Trying to get a roster that does not yet exist returns None.
+
+ >>> print mgr.get_roster('no roster')
+ None
+
+
+Iterating over all the rosters
+------------------------------
+
+Once you've created a bunch of rosters, you can use the user manager to
+iterate over all the rosters.
+
+ >>> roster_2 = mgr.create_roster('roster-2')
+ >>> roster_3 = mgr.create_roster('roster-3')
+ >>> roster_4 = mgr.create_roster('roster-4')
+ >>> flush()
+ >>> sorted(roster.name for roster in mgr.rosters)
+ ['', 'roster-1', 'roster-2', 'roster-3', 'roster-4']
+
+
+Cleaning up
+-----------
+
+ >>> for roster in mgr.rosters:
+ ... mgr.delete_roster(roster)
+ >>> flush()
diff --git a/Mailman/docs/users.txt b/Mailman/docs/users.txt
new file mode 100644
index 000000000..caad6b216
--- /dev/null
+++ b/Mailman/docs/users.txt
@@ -0,0 +1,180 @@
+Users
+=====
+
+Users are entities that combine addresses, preferences, and a password
+scheme. Password schemes can be anything from a traditional
+challenge/response type password string to an OpenID url.
+
+
+Create, deleting, and managing users
+------------------------------------
+
+Users are managed by the IUserManager. Users don't have any unique
+identifying information, and no such id is needed to create them.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> mgr = config.user_manager
+ >>> user = mgr.create_user()
+
+Users have a real name, a password scheme, a default profile, and a set of
+addresses that they control. All of these data are None or empty for a newly
+created user.
+
+ >>> user.real_name is None
+ True
+ >>> user.password is None
+ True
+ >>> user.addresses
+ []
+
+You can iterate over all the users in a user manager.
+
+ >>> another_user = mgr.create_user()
+ >>> flush()
+ >>> all_users = list(mgr.users)
+ >>> len(list(all_users))
+ 2
+ >>> user is not another_user
+ True
+ >>> user in all_users
+ True
+ >>> another_user in all_users
+ True
+
+You can also delete users from the user manager.
+
+ >>> mgr.delete_user(user)
+ >>> mgr.delete_user(another_user)
+ >>> flush()
+ >>> len(list(mgr.users))
+ 0
+
+
+Simple user information
+-----------------------
+
+Users may have a real name and a password scheme.
+
+ >>> user = mgr.create_user()
+ >>> user.password = 'my password'
+ >>> user.real_name = 'Zoe Person'
+ >>> flush()
+ >>> only_person = list(mgr.users)[0]
+ >>> only_person.password
+ 'my password'
+ >>> only_person.real_name
+ 'Zoe Person'
+
+The password and real name can be changed at any time.
+
+ >>> user.real_name = 'Zoe X. Person'
+ >>> user.password = 'another password'
+ >>> only_person.real_name
+ 'Zoe X. Person'
+ >>> only_person.password
+ 'another password'
+
+
+Users and addresses
+-------------------
+
+One of the pieces of information that a user links to is a set of email
+addresses, in the form of IAddress objects. A user can control many
+addresses, but addresses may be control by only one user.
+
+Given a user and an address, you can link the two together.
+
+ >>> roster = mgr.get_roster('')
+ >>> address = roster.create('aperson@example.com', 'Anne Person')
+ >>> user.link(address)
+ >>> flush()
+ >>> sorted(address.address for address in user.addresses)
+ ['aperson@example.com']
+
+But don't try to link an address to more than one user.
+
+ >>> another_user = mgr.create_user()
+ >>> another_user.link(address)
+ Traceback (most recent call last):
+ ...
+ AddressAlreadyLinkedError: Anne Person <aperson@example.com>
+
+You can also ask whether a given user controls a given address.
+
+ >>> user.controls(address)
+ True
+ >>> not_my_address = roster.create('bperson@example.com', 'Ben Person')
+ >>> user.controls(not_my_address)
+ False
+
+Given a text email address, the user manager can find the user that controls
+that address.
+
+ >>> mgr.get_user('aperson@example.com') is user
+ True
+ >>> mgr.get_user('bperson@example.com') is None
+ True
+
+Addresses can also be unlinked from a user.
+
+ >>> user.unlink(address)
+ >>> user.controls(address)
+ False
+ >>> mgr.get_user('aperson@example.com') is None
+ True
+
+But don't try to unlink the address from a user it's not linked to.
+
+ >>> user.unlink(address)
+ Traceback (most recent call last):
+ ...
+ AddressNotLinkedError: Anne Person <aperson@example.com>
+ >>> another_user.unlink(address)
+ Traceback (most recent call last):
+ ...
+ AddressNotLinkedError: Anne Person <aperson@example.com>
+ >>> mgr.delete_user(another_user)
+
+
+Users and profiles
+------------------
+
+Users always have a default profile.
+
+ >>> from Mailman.interfaces import IProfile
+ >>> IProfile.providedBy(user.profile)
+ True
+
+A profile is a set of preferences such as whether the user wants to receive an
+acknowledgment of all of their posts to a mailing list...
+
+ >>> user.profile.acknowledge_posts
+ False
+
+...whether the user wants to hide their email addresses on web pages and in
+postings to the list...
+
+ >>> user.profile.hide_address
+ True
+
+...the language code for the user's preferred language...
+
+ >>> user.profile.preferred_language
+ 'en'
+
+...whether the user wants to receive the list's copy of a message if they are
+explicitly named in one of the recipient headers...
+
+ >>> user.profile.receive_list_copy
+ True
+
+...whether the user wants to receive a copy of their own postings...
+
+ >>> user.profile.receive_own_postings
+ True
+
+...and the preferred delivery method.
+
+ >>> print user.profile.delivery_mode
+ DeliveryMode.regular
diff --git a/Mailman/enum.py b/Mailman/enum.py
deleted file mode 100644
index 893e988ba..000000000
--- a/Mailman/enum.py
+++ /dev/null
@@ -1,132 +0,0 @@
-# Copyright (C) 2004-2007 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.
-
-"""Enumeration meta class.
-
-To define your own enumeration, do something like:
-
->>> class Colors(Enum):
-... red = 1
-... green = 2
-... blue = 3
-
-Enum subclasses cannot be instantiated, but you can convert them to integers
-and from integers. Returned enumeration attributes are singletons and can be
-compared by identity only.
-"""
-
-COMMASPACE = ', '
-
-# Based on example by Jeremy Hylton
-# Modified and extended by Barry Warsaw
-
-
-
-class EnumMetaclass(type):
- def __init__(cls, name, bases, dict):
- # cls == the class being defined
- # name == the name of the class
- # bases == the class's bases
- # dict == the class attributes
- super(EnumMetaclass, cls).__init__(name, bases, dict)
- # Store EnumValues here for easy access.
- cls._enums = {}
- # Figure out the set of enum values on the base classes, to ensure
- # that we don't get any duplicate values (which would screw up
- # conversion from integer).
- for basecls in cls.__mro__:
- if hasattr(basecls, '_enums'):
- cls._enums.update(basecls._enums)
- # For each class attribute, create an EnumValue and store that back on
- # the class instead of the int. Skip Python reserved names. Also add
- # a mapping from the integer to the instance so we can return the same
- # object on conversion.
- for attr in dict:
- if not (attr.startswith('__') and attr.endswith('__')):
- intval = dict[attr]
- enumval = EnumValue(name, intval, attr)
- if intval in cls._enums:
- raise TypeError('Multiple enum values: %s' % enumval)
- # Store as an attribute on the class, and save the attr name
- setattr(cls, attr, enumval)
- cls._enums[intval] = attr
-
- def __getattr__(cls, name):
- if name == '__members__':
- return cls._enums.values()
- raise AttributeError(name)
-
- def __repr__(cls):
- enums = ['%s: %d' % (cls._enums[k], k) for k in sorted(cls._enums)]
- return '<%s {%s}>' % (cls.__name__, COMMASPACE.join(enums))
-
- def __iter__(cls):
- for i in sorted(cls._enums):
- yield getattr(cls, cls._enums[i])
-
- def __getitem__(cls, i):
- # i can be an integer or a string
- attr = cls._enums.get(i)
- if attr is None:
- # It wasn't an integer -- try attribute name
- try:
- return getattr(cls, i)
- except (AttributeError, TypeError):
- raise ValueError(i)
- return getattr(cls, attr)
-
- # Support both MyEnum[i] and MyEnum(i)
- __call__ = __getitem__
-
-
-
-class EnumValue(object):
- """Class to represent an enumeration value.
-
- EnumValue('Color', 'red', 12) prints as 'Color.red' and can be converted
- to the integer 12.
- """
- def __init__(self, classname, value, enumname):
- self._classname = classname
- self._value = value
- self._enumname = enumname
-
- def __repr__(self):
- return 'EnumValue(%s, %s, %d)' % (
- self._classname, self._enumname, self._value)
-
- def __str__(self):
- return self._enumname
-
- def __int__(self):
- return self._value
-
- # Support only comparison by identity. Yes, really raise
- # NotImplementedError instead of returning NotImplemented.
- def __eq__(self, other):
- raise NotImplementedError
-
- __ne__ = __eq__
- __lt__ = __eq__
- __gt__ = __eq__
- __le__ = __eq__
- __ge__ = __eq__
-
-
-
-class Enum:
- __metaclass__ = EnumMetaclass
diff --git a/Mailman/ext/Makefile.in b/Mailman/ext/Makefile.in
new file mode 100644
index 000000000..be14be4f4
--- /dev/null
+++ b/Mailman/ext/Makefile.in
@@ -0,0 +1,71 @@
+# Copyright (C) 2007 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.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+DESTDIR=
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+PACKAGEDIR= $(prefix)/Mailman/ext
+SHELL= /bin/sh
+
+MODULES= *.py
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+
+# Rules
+
+all:
+
+install:
+ for f in $(MODULES); \
+ do \
+ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm *.pyc
+ -rm Makefile
diff --git a/Mailman/ext/__init__.py b/Mailman/ext/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/Mailman/ext/__init__.py
diff --git a/Mailman/initialize.py b/Mailman/initialize.py
index a47fbd045..2e7e65b70 100644
--- a/Mailman/initialize.py
+++ b/Mailman/initialize.py
@@ -25,11 +25,15 @@ by the command line arguments.
"""
import os
+import sys
import Mailman.configuration
import Mailman.database
+import Mailman.ext
import Mailman.loginit
+DOT = '.'
+
# These initialization calls are separated for the testing framework, which
@@ -47,6 +51,15 @@ def initialize_1(config, propagate_logs):
os.umask(007)
Mailman.configuration.config.load(config)
Mailman.loginit.initialize(propagate_logs)
+ # Set up site extensions directory
+ Mailman.ext.__path__.append(Mailman.configuration.config.EXT_DIR)
+ # Initialize the IListManager, IMemberManager, and IMessageManager
+ modparts = Mailman.configuration.config.MANAGERS_INIT_FUNCTION.split(DOT)
+ funcname = modparts.pop()
+ modname = DOT.join(modparts)
+ __import__(modname)
+ initfunc = getattr(sys.modules[modname], funcname)
+ initfunc()
def initialize_2():
diff --git a/Mailman/interfaces/Makefile.in b/Mailman/interfaces/Makefile.in
new file mode 100644
index 000000000..d16bf9b70
--- /dev/null
+++ b/Mailman/interfaces/Makefile.in
@@ -0,0 +1,71 @@
+# Copyright (C) 2007 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.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+DESTDIR=
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+PACKAGEDIR= $(prefix)/Mailman/interfaces
+SHELL= /bin/sh
+
+MODULES= *.py
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+
+# Rules
+
+all:
+
+install:
+ for f in $(MODULES); \
+ do \
+ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm *.pyc
+ -rm Makefile
diff --git a/Mailman/interfaces/__init__.py b/Mailman/interfaces/__init__.py
new file mode 100644
index 000000000..e04ad1e78
--- /dev/null
+++ b/Mailman/interfaces/__init__.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2007 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.
+
+import os
+import sys
+
+from zope.interface import implementedBy
+from zope.interface.interfaces import IInterface
+
+__all__ = []
+
+
+
+def _populate():
+ import Mailman.interfaces
+ iface_mod = sys.modules['Mailman.interfaces']
+ # Expose interfaces defined in sub-modules into the top-level package
+ for filename in os.listdir(os.path.dirname(iface_mod.__file__)):
+ base, ext = os.path.splitext(filename)
+ if ext <> '.py':
+ continue
+ modname = 'Mailman.interfaces.' + base
+ __import__(modname)
+ module = sys.modules[modname]
+ for name in dir(module):
+ obj = getattr(module, name)
+ if IInterface.providedBy(obj):
+ setattr(iface_mod, name, obj)
+ __all__.append(name)
+
+
+_populate()
diff --git a/Mailman/interfaces/address.py b/Mailman/interfaces/address.py
new file mode 100644
index 000000000..5f6a9193d
--- /dev/null
+++ b/Mailman/interfaces/address.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2007 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.
+
+"""Interface for email address related information."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IAddress(Interface):
+ """Email address related information."""
+
+ address = Attribute(
+ """Read-only text email address.""")
+
+ real_name = Attribute(
+ """Optional real name associated with the email address.""")
+
+ registered_on = Attribute(
+ """The date and time at which this email address was registered.
+
+ Registeration is really the date at which this address became known to
+ us. It may have been explicitly registered by a user, or it may have
+ been implicitly registered, e.g. by showing up in a non-member
+ posting.""")
+
+ validated_on = Attribute(
+ """The date and time at which this email address was validated, or
+ None if the email address has not yet been validated. The specific
+ method of validation is not defined here.""")
diff --git a/Mailman/interfaces/listmanager.py b/Mailman/interfaces/listmanager.py
new file mode 100644
index 000000000..113ca35af
--- /dev/null
+++ b/Mailman/interfaces/listmanager.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2007 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.
+
+"""Interface for list storage, deleting, and finding."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IListManager(Interface):
+ """The interface of the global list manager.
+
+ The list manager manages IMailingList objects. You can add and remove
+ IMailingList objects from the list manager, and you can retrieve them
+ from the manager via their fully qualified list name
+ (e.g. 'mylist@example.com').
+ """
+
+ def create(fqdn_listname):
+ """Create an IMailingList with the given fully qualified list name.
+
+ Raises MMListAlreadyExistsError if the named list already exists.
+ """
+
+ def get(fqdn_listname):
+ """Return the IMailingList with the given fully qualified list name.
+
+ Raises MMUnknownListError if the names list does not exist.
+ """
+
+ def delete(mlist):
+ """Remove the IMailingList from the backend storage."""
+
+ def get(fqdn_listname):
+ """Find the IMailingList with the matching fully qualified list name.
+
+ Returns the matching IMailList instance or None if there was no
+ matching mailing list. fqdn_listname
+ """
+
+ mailing_lists = Attribute(
+ """An iterator over all the IMailingList objects managed by this list
+ manager.""")
+
+ names = Attribute(
+ """An iterator over the fully qualified list names of all mailing
+ lists managed by this list manager.""")
diff --git a/Mailman/interfaces/mailinglist.py b/Mailman/interfaces/mailinglist.py
new file mode 100644
index 000000000..c162801ab
--- /dev/null
+++ b/Mailman/interfaces/mailinglist.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2007 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.
+
+"""Marker interface for a mailing list."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingList(Interface):
+ """Marker interface for a mailing list.
+
+ This is usually composed with several other interfaces.
+ """
diff --git a/Mailman/interfaces/manager.py b/Mailman/interfaces/manager.py
new file mode 100644
index 000000000..fe22ec74d
--- /dev/null
+++ b/Mailman/interfaces/manager.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2007 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.
+
+"""Generic database object manager interface."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IManaged(Interface):
+ """An object managed by an IManager."""
+
+ name = Attribute("""The name of the managed object.""")
+
+
+
+class IManager(Interface):
+ """Create and manage profiles."""
+
+ def create(name):
+ """Create and return a new IManaged object.
+
+ name is the unique name for this object. Raises
+ ExistingManagedObjectError if an IManaged object with the given name
+ already exists.
+ """
+
+ def get(name):
+ """Return the named IManaged object.
+
+ Raises NoSuchManagedObjectError if the named IManaged object does not
+ exist.
+ """
+
+ def delete(name):
+ """Delete the named IManaged object.
+
+ Raises NoSuchManagedObjectError if the named IManaged object does not
+ exist.
+ """
+
+ iterator = Attribute(
+ """Return an iterator over the all the IManaged objects.""")
diff --git a/Mailman/interfaces/mlistdigests.py b/Mailman/interfaces/mlistdigests.py
new file mode 100644
index 000000000..bd9467b14
--- /dev/null
+++ b/Mailman/interfaces/mlistdigests.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2007 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.
+
+"""Interface for digest related information."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingListDigests(Interface):
+ """Digest related information for the mailing list."""
+
+ volume_number = Attribute(
+ """A monotonically increasing integer sequentially assigned to each
+ new digest volume. The volume number may be bumped either
+ automatically (i.e. on a defined schedule) or manually. When the
+ volume number is bumped, the digest number is always reset to 1.""")
+
+ digest_number = Attribute(
+ """A sequence number for a specific digest in a given volume. When
+ the digest volume number is bumped, the digest number is reset to
+ 1.""")
+
+ def bump():
+ """Bump the digest's volume number to the next integer in the
+ sequence, and reset the digest number to 1.
+ """
+
+ message_count = Attribute(
+ """The number of messages in the digest currently being collected.""")
+
+ digest_size = Attribute(
+ """The approximate size in kilobytes of the digest currently being
+ collected.""")
+
+ messages = Attribute(
+ """An iterator over all the messages in the digest currently being
+ created. Returns individual IPostedMessage objects.
+ """)
+
+ limits = Attribute(
+ """An iterator over the IDigestLimiters associated with this digest.
+ Each limiter can make a determination of whether the digest has
+ reached the threshold for being automatically sent.""")
+
+ def send():
+ """Send this digest now."""
+
+ decorators = Attribute(
+ """An iterator over all the IDecorators associated with this digest.
+ When a digest is being sent, each decorator may modify the final
+ digest text.""")
diff --git a/Mailman/interfaces/mlistemail.py b/Mailman/interfaces/mlistemail.py
new file mode 100644
index 000000000..958ea324d
--- /dev/null
+++ b/Mailman/interfaces/mlistemail.py
@@ -0,0 +1,78 @@
+# Copyright (C) 2007 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.
+
+"""Interface for the email addresses associated with a mailing list."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingListAddresses(Interface):
+ """The email addresses associated with a mailing list.
+
+ All attributes are read-only.
+ """
+
+ posting_address = Attribute(
+ """The address to which messages are posted for copying to the full
+ list membership, where 'full' of course means those members for which
+ delivery is currently enabled.""")
+
+ noreply_address = Attribute(
+ """The address to which all messages will be immediately discarded,
+ without prejudice or record. This address is specific to the ddomain,
+ even though it's available on the IMailingListAddresses interface.
+ Generally, humans should never respond directly to this address.""")
+
+ owner_address = Attribute(
+ """The address which reaches the owners and moderators of the mailing
+ list. There is no address which reaches just the owners or just the
+ moderators of a mailing list.""")
+
+ request_address = Attribute(
+ """The address which reaches the email robot for this mailing list.
+ This robot can process various email commands such as changing
+ delivery options, getting information or help about the mailing list,
+ or processing subscrptions and unsubscriptions (although for the
+ latter two, it's better to use the join_address and leave_address.""")
+
+ bounces_address = Attribute(
+ """The address which reaches the automated bounce processor for this
+ mailing list. Generally, humans should never respond directly to this
+ address.""")
+
+ join_address = Attribute(
+ """The address to which subscription requests should be sent. See
+ subscribe_address for a backward compatible alias.""")
+
+ leave_address = Attribute(
+ """The address to which unsubscription requests should be sent. See
+ unsubscribe_address for a backward compatible alias.""")
+
+ subscribe_address = Attribute(
+ """Deprecated address to which subscription requests may be sent.
+ This address is provided for backward compatibility only. See
+ join_address for the preferred alias.""")
+
+ leave_address = Attribute(
+ """Deprecated address to which unsubscription requests may be sent.
+ This address is provided for backward compatibility only. See
+ leave_address for the preferred alias.""")
+
+ def confirm_address(cookie=''):
+ """The address used for various forms of email confirmation."""
+
diff --git a/Mailman/interfaces/mlistid.py b/Mailman/interfaces/mlistid.py
new file mode 100644
index 000000000..ecd4b39cb
--- /dev/null
+++ b/Mailman/interfaces/mlistid.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2007 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.
+
+"""Interface for a mailing list identity."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingListIdentity(Interface):
+ """The basic identifying information of a mailing list."""
+
+ list_name = Attribute(
+ """The read-only short name of the mailing list. Note that where a
+ Mailman installation supports multiple domains, this short name may
+ not be unique. Use the fqdn_listname attribute for a guaranteed
+ unique id for the mailing list. This short name is always the local
+ part of the posting email address. For example, if messages are
+ posted to mylist@example.com, then the list_name is 'mylist'.""")
+
+ host_name = Attribute(
+ """The read-only domain name 'hosting' this mailing list. This is
+ always the domain name part of the posting email address, and it may
+ bear no relationship to the web url used to access this mailing list.
+ For example, if messages are posted to mylist@example.com, then the
+ host_name is 'example.com'.""")
+
+ fqdn_listname = Attribute(
+ """The read-only fully qualified name of the mailing list. This is
+ the guaranteed unique id for the mailing list, and it is always the
+ address to which messages are posted, e.g. mylist@example.com. It is
+ always comprised of the list_name + '@' + host_name.""")
diff --git a/Mailman/interfaces/mlistrequest.py b/Mailman/interfaces/mlistrequest.py
new file mode 100644
index 000000000..b9e1d4702
--- /dev/null
+++ b/Mailman/interfaces/mlistrequest.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2007 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.
+
+"""Interface for a web request accessing a mailing list."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingListRequest(Interface):
+ """The web request accessing a mailing list."""
+
+ location = Attribute(
+ """The url location of the request, used to calculate relative urls by
+ other components.""")
diff --git a/Mailman/interfaces/mlistrosters.py b/Mailman/interfaces/mlistrosters.py
new file mode 100644
index 000000000..1b407f472
--- /dev/null
+++ b/Mailman/interfaces/mlistrosters.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2007 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.
+
+"""Interface for mailing list rosters and roster sets."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingListRosters(Interface):
+ """Mailing list rosters, roster sets, and members.
+
+ This are all the email addresses that might possibly get messages from or
+ relating to this mailing list.
+ """
+
+ owners = Attribute(
+ """The IUser owners of this mailing list.
+
+ This does not include the IUsers who are moderators but not owners of
+ the mailing list.""")
+
+ moderators = Attribute(
+ """The IUser moderators of this mailing list.
+
+ This does not include the IUsers who are owners but not moderators of
+ the mailing list.""")
+
+ administrators = Attribute(
+ """The IUser administrators of this mailing list.
+
+ This includes the IUsers who are both owners and moderators of the
+ mailing list.""")
+
+ owner_rosters = Attribute(
+ """An iterator over the IRosters containing all the owners of this
+ mailing list.""")
+
+ moderator_rosters = Attribute(
+ """An iterator over the IRosters containing all the moderators of this
+ mailing list.""")
+
+ def add_owner_roster(roster):
+ """Add an IRoster to this mailing list's set of owner rosters."""
+
+ def delete_owner_roster(roster):
+ """Remove an IRoster from this mailing list's set of owner rosters."""
+
+ def add_moderator_roster(roster):
+ """Add an IRoster to this mailing list's set of moderator rosters."""
+
+ def delete_moderator_roster(roster):
+ """Remove an IRoster from this mailing list's set of moderator
+ rosters."""
+
+ members = Attribute(
+ """An iterator over all the members of the mailing list, regardless of
+ whether they are to receive regular messages or digests, or whether
+ they have their delivery disabled or not.""")
+
+ regular_members = Attribute(
+ """An iterator over all the IMembers who are to receive regular
+ postings (i.e. non-digests) from the mailing list, regardless of
+ whether they have their delivery disabled or not.""")
+
+ digest_members = Attribute(
+ """An iterator over all the IMembers who are to receive digests of
+ postings to this mailing list, regardless of whether they have their
+ deliver disabled or not, or of the type of digest they are to
+ receive.""")
+
+ member_rosters = Attribute(
+ """An iterator over the IRosters containing all the members of this
+ mailing list.""")
+
+ def add_member_roster(roster):
+ """Add the given IRoster to the list of rosters for the members of this
+ mailing list."""
+
+ def remove_member_roster(roster):
+ """Remove the given IRoster to the list of rosters for the members of
+ this mailing list."""
diff --git a/Mailman/interfaces/mliststats.py b/Mailman/interfaces/mliststats.py
new file mode 100644
index 000000000..9ed25b1ce
--- /dev/null
+++ b/Mailman/interfaces/mliststats.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2007 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.
+
+"""Interface for various mailing list statistics."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingListStatistics(Interface):
+ """Various statistics of a mailing list."""
+
+ creation_date = Attribute(
+ """The date and time that the mailing list was created.""")
+
+ last_post_date = Attribute(
+ """The date and time a message was last posted to the mailing list.""")
+
+ post_number = Attribute(
+ """A monotonically increasing integer sequentially assigned to each
+ list posting.""")
+
+ last_digest_date = Attribute(
+ """The date and time a digest of this mailing list was last sent.""")
diff --git a/Mailman/interfaces/mlistweb.py b/Mailman/interfaces/mlistweb.py
new file mode 100644
index 000000000..16eb94281
--- /dev/null
+++ b/Mailman/interfaces/mlistweb.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2007 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.
+
+"""Interface for the web addresses associated with a mailing list."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IMailingListURLs(Interface):
+ """The web addresses associated with a mailing list."""
+
+ protocol = Attribute(
+ """The web protocol to use to contact the server providing the web
+ interface for this mailing list, e.g. 'http' or 'https'.""")
+
+ web_host = Attribute(
+ """The read-only domain name of the host to contact for interacting
+ with the web interface of the mailing list.""")
+
+ def script_url(target, context=None):
+ """Return the url to the given script target. If 'context' is not
+ given, or is None, then an absolute url is returned. If context is
+ given, it must be an IMailingListRequest object, and the returned url
+ will be relative to that object's 'location' attribute."""
diff --git a/Mailman/interfaces/permissions.py b/Mailman/interfaces/permissions.py
new file mode 100644
index 000000000..c6c5e015a
--- /dev/null
+++ b/Mailman/interfaces/permissions.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2007 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.
+
+"""Interfaces for various permissions."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IPostingPermission(Interface):
+ """Posting related permissions."""
+
+ okay_to_post = Attribute(
+ """Boolean specifying whether it is okay to post to the list.""")
diff --git a/Mailman/interfaces/profile.py b/Mailman/interfaces/profile.py
new file mode 100644
index 000000000..ed3968f9d
--- /dev/null
+++ b/Mailman/interfaces/profile.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2007 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.
+
+"""Interface for a profile, which describes delivery related information."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IProfile(Interface):
+ """Delivery related information."""
+
+ acknowledge_posts = Attribute(
+ """Boolean specifying whether to send an acknowledgment receipt for
+ every posting to the mailing list.
+ """)
+
+ hide_address = Attribute(
+ """Boolean specifying whether to hide this email address from fellow
+ list members.
+ """)
+
+ preferred_language = Attribute(
+ """Preferred language for interacting with a mailing list.""")
+
+ receive_list_copy = Attribute(
+ """Boolean specifying whether to receive a list copy if the user is
+ explicitly named in one of the recipient headers.
+ """)
+
+ receive_own_postings = Attribute(
+ """Boolean specifying whether to receive a list copy of the user's own
+ postings to the mailing list.
+ """)
+
+ delivery_mode = Attribute(
+ """The preferred delivery mode.
+
+ This is an enum constant of the type DeliveryMode.""")
diff --git a/Mailman/interfaces/roster.py b/Mailman/interfaces/roster.py
new file mode 100644
index 000000000..7ddbd5101
--- /dev/null
+++ b/Mailman/interfaces/roster.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2007 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.
+
+"""Interface for a roster of members."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IRoster(Interface):
+ """A roster is a collection of IUsers."""
+
+ name = Attribute(
+ """The name for this roster.
+
+ Rosters are considered equal if they have the same name.""")
+
+ addresses = Attribute(
+ """An iterator over all the addresses managed by this roster.""")
+
+ def create(email_address, real_name=None):
+ """Create an IAddress and return it.
+
+ email_address is textual email address to add. real_name is the
+ optional real name that gets associated with the email address.
+
+ Raises ExistingAddressError if address already exists.
+ """
diff --git a/Mailman/interfaces/rosterset.py b/Mailman/interfaces/rosterset.py
new file mode 100644
index 000000000..12f28bffa
--- /dev/null
+++ b/Mailman/interfaces/rosterset.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2007 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.
+
+"""Interface for a collection of rosters."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IRosterSet(Interface):
+ """A collection of IRosters."""
+
+ serial = Attribute(
+ """The unique integer serial number for this roster set.
+
+ This is necessary to enforce the separation between the list storage
+ and the user/roster storage. You should always reference a roster set
+ indirectly through its serial number.""")
+
+ rosters = Attribute(
+ """An iterator over all the IRosters in this collection.""")
+
+ def add(roster):
+ """Add the IRoster to this collection.
+
+ Does nothing if the roster is already a member of this collection.
+ """
+
+ def delete(roster):
+ """Delete the IRoster from this collection.
+
+ Does nothing if the roster is not a member of this collection.
+ """
diff --git a/Mailman/interfaces/user.py b/Mailman/interfaces/user.py
new file mode 100644
index 000000000..6990eee4b
--- /dev/null
+++ b/Mailman/interfaces/user.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2007 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.
+
+"""Interface describing the basics of a user."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IUser(Interface):
+ """A basic user."""
+
+ real_name = Attribute(
+ """This user's Real Name.""")
+
+ password = Attribute(
+ """This user's password information.""")
+
+ profile = Attribute(
+ """The default IProfile for this user.""")
+
+ addresses = Attribute(
+ """An iterator over all the IAddresses controlled by this user.""")
+
+ def link(address):
+ """Link this user to the given IAddress.
+
+ Raises AddressAlreadyLinkedError if this IAddress is already linked to
+ another user.
+ """
+
+ def unlink(address):
+ """Unlink this IAddress from the user.
+
+ Raises AddressNotLinkedError if this address is not linked to this
+ user, either because it's not linked to any user or it's linked to
+ some other user.
+ """
+
+ def controls(address):
+ """Determine whether this user controls the given email address.
+
+ 'address' is a text email address. This method returns true if the
+ user controls the given email address, otherwise false.
+ """
diff --git a/Mailman/interfaces/usermanager.py b/Mailman/interfaces/usermanager.py
new file mode 100644
index 000000000..302fe9b60
--- /dev/null
+++ b/Mailman/interfaces/usermanager.py
@@ -0,0 +1,82 @@
+# Copyright (C) 2007 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.
+
+"""Interface describing a user manager service."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class IUserManager(Interface):
+ """The interface of a global user manager service.
+
+ Different user managers have different concepts of what a user is, and the
+ users managed by different IUserManagers are completely independent. This
+ is how you can separate the user contexts for different domains, on a
+ multiple domain system.
+
+ There is one special roster, the null roster ('') which contains all
+ IUsers in all IRosters.
+ """
+
+ def create_roster(name):
+ """Create and return the named IRoster.
+
+ Raises RosterExistsError if the named roster already exists.
+ """
+
+ def get_roster(name):
+ """Return the named IRoster.
+
+ Raises NoSuchRosterError if the named roster doesnot yet exist.
+ """
+
+ def delete_roster(name):
+ """Delete the named IRoster.
+
+ Raises NoSuchRosterError if the named roster doesnot yet exist.
+ """
+
+ rosters = Attribute(
+ """An iterator over all IRosters managed by this user manager.""")
+
+ def create_user():
+ """Create and return an IUser."""
+
+ def delete_user(user):
+ """Delete the given IUser."""
+
+ def get_user(address):
+ """Get the user that controls the given email address, or None.
+
+ 'address' is a text email address.
+ """
+
+ users = Attribute(
+ """An iterator over all the IUsers managed by this user manager.""")
+
+ def create_rosterset():
+ """Create and return a new IRosterSet.
+
+ IRosterSets manage groups of IRosters.
+ """
+
+ def delete_rosterset(rosterset):
+ """Delete the given IRosterSet."""
+
+ def get_rosterset(serial):
+ """Return the IRosterSet that matches the serial number, or None."""
diff --git a/Mailman/loginit.py b/Mailman/loginit.py
index 678875707..c3b3aac05 100644
--- a/Mailman/loginit.py
+++ b/Mailman/loginit.py
@@ -121,7 +121,8 @@ def initialize(propagate=False):
# minimal overloading of our logger configurations.
cp = ReallySafeConfigParser()
if config.LOG_CONFIG_FILE:
- cp.read(config.LOG_CONFIG_FILE)
+ path = os.path.join(config.ETC_DIR, config.LOG_CONFIG_FILE)
+ cp.read(os.path.normpath(path))
# Create the subloggers
for logger in LOGGERS:
log = logging.getLogger('mailman.' + logger)
diff --git a/Mailman/passwords.py b/Mailman/passwords.py
index 615bbcc17..9e7c5615c 100644
--- a/Mailman/passwords.py
+++ b/Mailman/passwords.py
@@ -28,9 +28,9 @@ import hmac
from array import array
from base64 import urlsafe_b64decode as decode
from base64 import urlsafe_b64encode as encode
+from munepy import Enum
from Mailman import Errors
-from Mailman.enum import Enum
SALT_LENGTH = 20 # bytes
ITERATIONS = 2000
diff --git a/Mailman/testing/inmemory.py b/Mailman/testing/inmemory.py
index ca5313452..1489f0ffb 100644
--- a/Mailman/testing/inmemory.py
+++ b/Mailman/testing/inmemory.py
@@ -134,65 +134,6 @@ class Address(object):
-class RegularDelivery(object):
- implements(IRegularDelivery)
-
-
-class PlainTextDigestDelivery(object):
- implements(IPlainTextDigestDelivery)
-
-
-class MIMEDigestDelivery(object):
- implements(IMIMEDigestDeliver)
-
-
-
-class DeliveryEnabled(object):
- implements(IDeliveryStatus)
-
- @property
- def enabled(self):
- return True
-
-
-class DeliveryDisabled(object):
- implements(IDeliveryStatus)
-
- @property
- def enabled(self):
- return False
-
-
-class DeliveryDisabledByUser(DeliveryDisabled):
- implements(IDeliveryDisabledByUser)
-
-
-class DeliveryDisabledbyAdministrator(DeliveryDisabled):
- implements(IDeliveryDisabledByAdministrator)
-
- reason = u'Unknown'
-
-
-class DeliveryDisabledByBounces(DeliveryDisabled):
- implements(IDeliveryDisabledByBounces)
-
- bounce_info = 'XXX'
-
-
-class DeliveryTemporarilySuspended(object):
- implements(IDeliveryTemporarilySuspended)
-
- def __init__(self, start_date, end_date):
- self.start_date = start_date
- self.end_date = end_date
-
- @property
- def enabled(self):
- now = datetime.datetime.now()
- return not (self.start_date <= now < self.end_date)
-
-
-
class OkayToPost(object):
implements(IPostingPermission)
@@ -201,21 +142,6 @@ class OkayToPost(object):
-class Profile(object):
- implements(IProfile)
-
- # System defaults
- acknowledge = False
- hide = True
- language = 'en'
- list_copy = True
- own_postings = True
- delivery_mode = RegularDelivery()
- delivery_status = DeliveryEnabled()
- posting_permission = OkayToPost()
-
-
-
class Roster(object):
implements(IRoster)
@@ -247,24 +173,6 @@ class Roster(object):
-class Member(object):
- implements(IMember)
-
- def __init__(self, address, roster, profile=None):
- self._address = address
- self._roster = roster
- self.profile = profile or Profile()
-
- @property
- def address(self):
- return self._address
-
- @property
- def roster(self):
- return self._roster
-
-
-
class ListManager(object):
implements(IListManager)
diff --git a/Mailman/testing/test_address.py b/Mailman/testing/test_address.py
new file mode 100644
index 000000000..a04b3e795
--- /dev/null
+++ b/Mailman/testing/test_address.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2007 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.
+
+"""Doctest harness for testing IAddress interface."""
+
+import doctest
+import unittest
+
+options = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/addresses.txt',
+ optionflags=options))
+ return suite
diff --git a/Mailman/testing/test_enum.py b/Mailman/testing/test_enum.py
deleted file mode 100644
index a8c389bb4..000000000
--- a/Mailman/testing/test_enum.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# Copyright (C) 2007 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.
-
-"""Unit tests for Enums."""
-
-import operator
-import unittest
-
-from Mailman.enum import Enum
-
-
-
-class Colors(Enum):
- red = 1
- green = 2
- blue = 3
-
-
-class MoreColors(Colors):
- pink = 4
- cyan = 5
-
-
-class OtherColors(Enum):
- red = 1
- blue = 2
- yellow = 3
-
-
-
-class TestEnum(unittest.TestCase):
- def test_enum_basics(self):
- unless = self.failUnless
- raises = self.assertRaises
- # Cannot compare by equality
- raises(NotImplementedError, operator.eq, Colors.red, Colors.red)
- raises(NotImplementedError, operator.ne, Colors.red, Colors.red)
- raises(NotImplementedError, operator.lt, Colors.red, Colors.red)
- raises(NotImplementedError, operator.gt, Colors.red, Colors.red)
- raises(NotImplementedError, operator.le, Colors.red, Colors.red)
- raises(NotImplementedError, operator.ge, Colors.red, Colors.red)
- raises(NotImplementedError, operator.eq, Colors.red, 1)
- raises(NotImplementedError, operator.ne, Colors.red, 1)
- raises(NotImplementedError, operator.lt, Colors.red, 1)
- raises(NotImplementedError, operator.gt, Colors.red, 1)
- raises(NotImplementedError, operator.le, Colors.red, 1)
- raises(NotImplementedError, operator.ge, Colors.red, 1)
- # Comparison by identity
- unless(Colors.red is Colors.red)
- unless(Colors.red is MoreColors.red)
- unless(Colors.red is not OtherColors.red)
- unless(Colors.red is not Colors.blue)
-
- def test_enum_conversions(self):
- eq = self.assertEqual
- unless = self.failUnless
- raises = self.assertRaises
- unless(Colors.red is Colors['red'])
- unless(Colors.red is Colors[1])
- unless(Colors.red is Colors('red'))
- unless(Colors.red is Colors(1))
- unless(Colors.red is not Colors['blue'])
- unless(Colors.red is not Colors[2])
- unless(Colors.red is not Colors('blue'))
- unless(Colors.red is not Colors(2))
- unless(Colors.red is MoreColors['red'])
- unless(Colors.red is MoreColors[1])
- unless(Colors.red is MoreColors('red'))
- unless(Colors.red is MoreColors(1))
- unless(Colors.red is not OtherColors['red'])
- unless(Colors.red is not OtherColors[1])
- unless(Colors.red is not OtherColors('red'))
- unless(Colors.red is not OtherColors(1))
- raises(ValueError, Colors.__getitem__, 'magenta')
- raises(ValueError, Colors.__getitem__, 99)
- raises(ValueError, Colors.__call__, 'magenta')
- raises(ValueError, Colors.__call__, 99)
- eq(int(Colors.red), 1)
- eq(int(Colors.blue), 3)
- eq(int(MoreColors.red), 1)
- eq(int(OtherColors.blue), 2)
-
- def test_enum_duplicates(self):
- try:
- # This is bad because kyle and kenny have the same integer value.
- class Bad(Enum):
- cartman = 1
- stan = 2
- kyle = 3
- kenny = 3
- butters = 4
- except TypeError:
- got_error = True
- else:
- got_error = False
- self.failUnless(got_error)
-
- def test_enum_iteration(self):
- eq = self.assertEqual
- # Iteration sorts on the int value of the enum
- values = [str(v) for v in MoreColors]
- eq(values, ['red', 'green', 'blue', 'pink', 'cyan'])
- values = [int(v) for v in MoreColors]
- eq(values, [1, 2, 3, 4, 5])
-
-
-
-def test_suite():
- suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(TestEnum))
- return suite
diff --git a/Mailman/testing/test_mlist_addresses.py b/Mailman/testing/test_mlist_addresses.py
new file mode 100644
index 000000000..fe32c12b2
--- /dev/null
+++ b/Mailman/testing/test_mlist_addresses.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2007 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.
+
+"""Doctest harness for the IMailingListAddresses interface."""
+
+import doctest
+import unittest
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/mlist-addresses.txt',
+ optionflags=doctest.ELLIPSIS))
+ return suite
diff --git a/Mailman/testing/test_mlist_rosters.py b/Mailman/testing/test_mlist_rosters.py
new file mode 100644
index 000000000..e8713b828
--- /dev/null
+++ b/Mailman/testing/test_mlist_rosters.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2007 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.
+
+"""Doctest harness for the IMailingListRosters interface."""
+
+import doctest
+import unittest
+
+options = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/mlist-rosters.txt',
+ optionflags=options))
+ return suite
diff --git a/Mailman/testing/test_use_listmanager.py b/Mailman/testing/test_use_listmanager.py
new file mode 100644
index 000000000..f78b50a0f
--- /dev/null
+++ b/Mailman/testing/test_use_listmanager.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2007 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.
+
+"""Doctest harness for testing mailing list creation and deletion."""
+
+import doctest
+import unittest
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/use-listmanager.txt',
+ optionflags=doctest.ELLIPSIS))
+ return suite
diff --git a/Mailman/testing/test_use_usermanager.py b/Mailman/testing/test_use_usermanager.py
new file mode 100644
index 000000000..0484e3e10
--- /dev/null
+++ b/Mailman/testing/test_use_usermanager.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2007 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.
+
+"""Doctest harness for testing mailing list creation and deletion."""
+
+import doctest
+import unittest
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/use-usermanager.txt',
+ optionflags=doctest.ELLIPSIS))
+ return suite
diff --git a/Mailman/testing/test_user.py b/Mailman/testing/test_user.py
new file mode 100644
index 000000000..1c075a164
--- /dev/null
+++ b/Mailman/testing/test_user.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2007 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.
+
+"""Doctest harness for testing users."""
+
+import doctest
+import unittest
+
+options = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/users.txt',
+ optionflags=options))
+ return suite
diff --git a/Mailman/testing/testing.cfg.in b/Mailman/testing/testing.cfg.in
index 80e5e8bfc..2609ef5cf 100644
--- a/Mailman/testing/testing.cfg.in
+++ b/Mailman/testing/testing.cfg.in
@@ -4,7 +4,6 @@
# both the process running the tests and all sub-processes (e.g. qrunners)
# must share the same configuration file.
-MANAGERS_INIT_FUNCTION = 'Mailman.testing.inmemory.initialize'
SMTPPORT = 10825
MAX_RESTARTS = 1
MTA = None