summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2007-08-05 22:49:04 -0500
committerBarry Warsaw2007-08-05 22:49:04 -0500
commit89bdaec5c735ffb2b27cc29620cb01b451b72550 (patch)
tree61c6fa40eb1d3e267475430005b50ecf44b46512
parente0abca9fbdde530f7517396677c87f19c86bc0c6 (diff)
downloadmailman-89bdaec5c735ffb2b27cc29620cb01b451b72550.tar.gz
mailman-89bdaec5c735ffb2b27cc29620cb01b451b72550.tar.zst
mailman-89bdaec5c735ffb2b27cc29620cb01b451b72550.zip
-rw-r--r--Mailman/Errors.py29
-rw-r--r--[-rwxr-xr-x]Mailman/app/__init__.py0
-rw-r--r--Mailman/app/lifecycle.py (renamed from Mailman/app/create.py)56
-rw-r--r--Mailman/bin/newlist.py2
-rw-r--r--Mailman/bin/rmlist.py59
-rw-r--r--Mailman/database/model/address.py12
-rw-r--r--Mailman/database/model/mailinglist.py1
-rw-r--r--Mailman/database/model/roster.py12
-rw-r--r--Mailman/docs/lifecycle.txt (renamed from Mailman/docs/create.txt)38
-rw-r--r--Mailman/docs/membership.txt29
-rw-r--r--Mailman/interfaces/address.py8
-rw-r--r--Mailman/interfaces/mlistrosters.py5
12 files changed, 177 insertions, 74 deletions
diff --git a/Mailman/Errors.py b/Mailman/Errors.py
index f3f7671e8..99065ddbe 100644
--- a/Mailman/Errors.py
+++ b/Mailman/Errors.py
@@ -162,11 +162,30 @@ class RejectMessage(HandlerError):
-# Additional exceptions
-class HostileSubscriptionError(MailmanError):
- """A cross-subscription attempt was made."""
- # This exception gets raised when an invitee attempts to use the
- # invitation to cross-subscribe to some other mailing list.
+# Subscription exceptions
+class SubscriptionError(MailmanError):
+ """Subscription errors base class."""
+
+
+class HostileSubscriptionError(SubscriptionError):
+ """A cross-subscription attempt was made.
+
+ This exception gets raised when an invitee attempts to use the
+ invitation to cross-subscribe to some other mailing list.
+ """
+
+
+class AlreadySubscribedError(SubscriptionError):
+ """The member is already subscribed to the mailing list with this role."""
+
+ def __init__(self, fqdn_listname, address, role):
+ self._fqdn_listname = fqdn_listname
+ self._address = address
+ self._role = role
+
+ def __str__(self):
+ return '%s is already a %s of mailing list %s' % (
+ self._address, self._role, self._fqdn_listname)
diff --git a/Mailman/app/__init__.py b/Mailman/app/__init__.py
index e69de29bb..e69de29bb 100755..100644
--- a/Mailman/app/__init__.py
+++ b/Mailman/app/__init__.py
diff --git a/Mailman/app/create.py b/Mailman/app/lifecycle.py
index d2f85d90d..1c40feaeb 100644
--- a/Mailman/app/create.py
+++ b/Mailman/app/lifecycle.py
@@ -17,6 +17,10 @@
"""Application level list creation."""
+import os
+import shutil
+import logging
+
from Mailman import Errors
from Mailman import Utils
from Mailman.Utils import ValidateEmail
@@ -25,6 +29,14 @@ from Mailman.app.styles import style_manager
from Mailman.configuration import config
from Mailman.constants import MemberRole
+__all__ = [
+ 'create_list',
+ 'remove_list',
+ ]
+
+
+log = logging.getLogger('mailman.error')
+
def create_list(fqdn_listname, owners=None):
@@ -58,3 +70,47 @@ def create_list(fqdn_listname, owners=None):
addr = list(user.addresses)[0]
addr.subscribe(mlist, MemberRole.owner)
return mlist
+
+
+
+def remove_list(fqdn_listname, mailing_list=None, archives=True):
+ """Remove the list and all associated artifacts and subscriptions."""
+ removeables = []
+ # mailing_list will be None when only residual archives are being removed.
+ if mailing_list:
+ # Remove all subscriptions, regardless of role.
+ for member in mailing_list.subscribers.members:
+ member.unsubscribe()
+ # Delete the mailing list from the database.
+ config.db.list_manager.delete(mailing_list)
+ # Do the MTA-specific list deletion tasks
+ if config.MTA:
+ modname = 'Mailman.MTA.' + config.MTA
+ __import__(modname)
+ sys.modules[modname].remove(mailing_list)
+ # Remove the list directory.
+ removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname))
+ # Remove any stale locks associated with the list.
+ for filename in os.listdir(config.LOCK_DIR):
+ fn_listname = filename.split('.')[0]
+ if fn_listname == fqdn_listname:
+ removeables.append(os.path.join(config.LOCK_DIR, filename))
+ if archives:
+ private_dir = config.PRIVATE_ARCHIVE_FILE_DIR
+ public_dir = config.PUBLIC_ARCHIVE_FILE_DIR
+ removeables.extend([
+ os.path.join(private_dir, fqdn_listname),
+ os.path.join(private_dir, fqdn_listname + '.mbox'),
+ os.path.join(public_dir, fqdn_listname),
+ os.path.join(public_dir, fqdn_listname + '.mbox'),
+ ])
+ # Now that we know what files and directories to delete, delete them.
+ for target in removeables:
+ if os.path.islink(target):
+ os.unlink(target)
+ elif os.path.isdir(target):
+ shutil.rmtree(target)
+ elif os.path.isfile(target):
+ os.unlink(target)
+ else:
+ log.error('Could not delete list artifact: $target')
diff --git a/Mailman/bin/newlist.py b/Mailman/bin/newlist.py
index 4396e6556..6847cc16f 100644
--- a/Mailman/bin/newlist.py
+++ b/Mailman/bin/newlist.py
@@ -27,7 +27,7 @@ from Mailman import Message
from Mailman import Utils
from Mailman import Version
from Mailman import i18n
-from Mailman.app.create import create_list
+from Mailman.app.lifecycle import create_list
from Mailman.configuration import config
from Mailman.initialize import initialize
diff --git a/Mailman/bin/rmlist.py b/Mailman/bin/rmlist.py
index c51ab7fdf..e0be8f6f5 100644
--- a/Mailman/bin/rmlist.py
+++ b/Mailman/bin/rmlist.py
@@ -24,66 +24,13 @@ from Mailman import Errors
from Mailman import Utils
from Mailman import Version
from Mailman.MailList import MailList
+from Mailman.app.lifecycle import remove_list
from Mailman.configuration import config
from Mailman.i18n import _
from Mailman.initialize import initialize
-__i18n_templates__ = True
-
-
-
-def remove_it(listname, filename, msg, quiet=False):
- if os.path.islink(filename):
- if not quiet:
- print _('Removing $msg')
- os.unlink(filename)
- elif os.path.isdir(filename):
- if not quiet:
- print _('Removing $msg')
- shutil.rmtree(filename)
- elif os.path.isfile(filename):
- os.unlink(filename)
- else:
- if not quiet:
- print _('$listname $msg not found as $filename')
-
-
-def delete_list(listname, mlist=None, archives=True, quiet=False):
- removeables = []
- if mlist:
- # Remove the list from the database
- config.db.list_manager.delete(mlist)
- # Do the MTA-specific list deletion tasks
- if config.MTA:
- modname = 'Mailman.MTA.' + config.MTA
- __import__(modname)
- sys.modules[modname].remove(mlist)
- # Remove the list directory
- removeables.append((os.path.join('lists', listname), _('list info')))
-
- # Remove any stale locks associated with the list
- for filename in os.listdir(config.LOCK_DIR):
- fn_listname = filename.split('.')[0]
- if fn_listname == listname:
- removeables.append((os.path.join(config.LOCK_DIR, filename),
- _('stale lock file')))
-
- if archives:
- removeables.extend([
- (os.path.join('archives', 'private', listname),
- _('private archives')),
- (os.path.join('archives', 'private', listname + '.mbox'),
- _('private archives')),
- (os.path.join('archives', 'public', listname),
- _('public archives')),
- (os.path.join('archives', 'public', listname + '.mbox'),
- _('public archives')),
- ])
-
- for dirtmpl, msg in removeables:
- path = os.path.join(config.VAR_DIR, dirtmpl)
- remove_it(listname, path, msg, quiet)
+__i18n_templates__ = True
@@ -130,7 +77,7 @@ No such list: ${fqdn_listname}. Removing its residual archives.""")
if not opts.archives:
print _('Not removing archives. Reinvoke with -a to remove them.')
- delete_list(fqdn_listname, mlist, opts.archives)
+ remove_list(fqdn_listname, mlist, opts.archives)
config.db.flush()
diff --git a/Mailman/database/model/address.py b/Mailman/database/model/address.py
index 9c36d2472..391004413 100644
--- a/Mailman/database/model/address.py
+++ b/Mailman/database/model/address.py
@@ -19,8 +19,10 @@ from elixir import *
from email.utils import formataddr
from zope.interface import implements
+from Mailman import Errors
from Mailman.interfaces import IAddress
+
MEMBER_KIND = 'Mailman.database.model.member.Member'
PREFERENCE_KIND = 'Mailman.database.model.preferences.Preferences'
USER_KIND = 'Mailman.database.model.user.User'
@@ -62,12 +64,18 @@ class Address(Entity):
return '<Address: %s [%s] key: %s at %#x>' % (
address_str, verified, self.address, id(self))
- def subscribe(self, mlist, role):
+ def subscribe(self, mailing_list, role):
from Mailman.database.model import Member
from Mailman.database.model import Preferences
# This member has no preferences by default.
+ member = Member.get_by(role=role,
+ mailing_list=mailing_list.fqdn_listname,
+ address=self)
+ if member:
+ raise Errors.AlreadySubscribedError(
+ mailing_list.fqdn_listname, self.address, role)
member = Member(role=role,
- mailing_list=mlist.fqdn_listname,
+ mailing_list=mailing_list.fqdn_listname,
address=self)
member.preferences = Preferences()
return member
diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py
index 11deb28c6..0cb968574 100644
--- a/Mailman/database/model/mailinglist.py
+++ b/Mailman/database/model/mailinglist.py
@@ -185,6 +185,7 @@ class MailingList(Entity):
self.members = roster.MemberRoster(self)
self.regular_members = roster.RegularMemberRoster(self)
self.digest_members = roster.DigestMemberRoster(self)
+ self.subscribers = roster.Subscribers(self)
@property
def fqdn_listname(self):
diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py
index 0730d2b4b..8440e0ffc 100644
--- a/Mailman/database/model/roster.py
+++ b/Mailman/database/model/roster.py
@@ -184,3 +184,15 @@ class DigestMemberRoster(AbstractRoster):
role=MemberRole.member):
if member.delivery_mode in _digest_modes:
yield member
+
+
+
+class Subscribers(AbstractRoster):
+ """Return all subscribed members regardless of their role."""
+
+ name = 'subscribers'
+
+ @property
+ def members(self):
+ for member in Member.select_by(mailing_list=self._mlist.fqdn_listname):
+ yield member
diff --git a/Mailman/docs/create.txt b/Mailman/docs/lifecycle.txt
index 9154a5c63..4a6354381 100644
--- a/Mailman/docs/create.txt
+++ b/Mailman/docs/lifecycle.txt
@@ -1,12 +1,12 @@
-Application level list creation
--------------------------------
+Application level list lifecycle
+--------------------------------
-The low-level way to create a new mailing list is to use the IListManager
-interface. This interface simply adds the appropriate database entries to
-record the list's creation.
+The low-level way to create and delete a mailing list is to use the
+IListManager interface. This interface simply adds or removes the appropriate
+database entries to record the list's creation.
-There is a higher level interface for creating mailing lists which performs a
-few additional tasks such as:
+There is a higher level interface for creating and deleting mailing lists
+which performs additional tasks such as:
* validating the list's posting address (which also serves as the list's
fully qualified name);
@@ -16,7 +16,7 @@ few additional tasks such as:
* notifying watchers of list creation;
* creating ancillary artifacts (such as the list's on-disk directory)
- >>> from Mailman.app.create import create_list
+ >>> from Mailman.app.lifecycle import create_list
Posting address validation
@@ -121,3 +121,25 @@ the system, they won't be created again.
>>> flush()
>>> sorted(user.real_name for user in mlist_3.owners.users)
['Anne Person', 'Bart Person', 'Caty Person', 'Dirk Person']
+
+
+Removing a list
+---------------
+
+Removing a mailing list deletes the list, all its subscribers, and any related
+artifacts.
+
+ >>> from Mailman import Utils
+ >>> from Mailman.app.lifecycle import remove_list
+ >>> remove_list(mlist_2.fqdn_listname, mlist_2, True)
+ >>> flush()
+ >>> Utils.list_exists('test_2@example.com')
+ False
+
+We should now be able to completely recreate the mailing list.
+
+ >>> mlist_2a = create_list('test_2@example.com', owners)
+ >>> flush()
+ >>> sorted(addr.address for addr in mlist_2a.owners.addresses)
+ ['aperson@example.com', 'bperson@example.com',
+ 'cperson@example.com', 'dperson@example.com']
diff --git a/Mailman/docs/membership.txt b/Mailman/docs/membership.txt
index ee322780c..515ac7623 100644
--- a/Mailman/docs/membership.txt
+++ b/Mailman/docs/membership.txt
@@ -209,3 +209,32 @@ is returned.
None
>>> print mlist.members.get_member('zperson@example.com')
None
+
+
+All subscribers
+---------------
+
+There is also a roster containing all the subscribers of a mailing list,
+regardless of their role.
+
+ >>> def sortkey(member):
+ ... return (member.address.address, int(member.role))
+ >>> [(member.address.address, str(member.role))
+ ... for member in sorted(mlist.subscribers.members, key=sortkey)]
+ [('aperson@example.com', 'MemberRole.member'),
+ ('aperson@example.com', 'MemberRole.owner'),
+ ('bperson@example.com', 'MemberRole.member'),
+ ('bperson@example.com', 'MemberRole.moderator'),
+ ('cperson@example.com', 'MemberRole.member')]
+
+
+Double subscriptions
+--------------------
+
+It is an error to subscribe someone to a list with the same role twice.
+
+ >>> address_1.subscribe(mlist, MemberRole.owner)
+ Traceback (most recent call last):
+ ...
+ AlreadySubscribedError: aperson@example.com is already a MemberRole.owner
+ of mailing list _xtest@example.com
diff --git a/Mailman/interfaces/address.py b/Mailman/interfaces/address.py
index 6b00d7915..1e654a2fc 100644
--- a/Mailman/interfaces/address.py
+++ b/Mailman/interfaces/address.py
@@ -56,10 +56,14 @@ class IAddress(Interface):
None if the email address has not yet been validated. The specific
method of validation is not defined here.""")
- def subscribe(mlist, role):
+ def subscribe(mailing_list, role):
"""Subscribe the address to the given mailing list with the given role.
- role is a Mailman.constants.MemberRole enum.
+ :param mailing_list: The IMailingList being subscribed to.
+ :param role: A MemberRole enum value.
+ :return: The IMember representing this subscription.
+ :raises AlreadySubscribedError: If the address is already subscribed
+ to the mailing list with the given role.
"""
preferences = Attribute(
diff --git a/Mailman/interfaces/mlistrosters.py b/Mailman/interfaces/mlistrosters.py
index 9cd20e3ef..86cd4ec91 100644
--- a/Mailman/interfaces/mlistrosters.py
+++ b/Mailman/interfaces/mlistrosters.py
@@ -61,3 +61,8 @@ class IMailingListRosters(Interface):
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.""")
+
+ subscribers = Attribute(
+ """An iterator over all IMembers subscribed to this list, with any
+ role.
+ """)