diff options
Diffstat (limited to 'Mailman')
27 files changed, 868 insertions, 633 deletions
diff --git a/Mailman/MailList.py b/Mailman/MailList.py index 0e57aa0a8..9dff467ce 100644 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -185,14 +185,6 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin, - # IMailingListIdentity - - @property - def fqdn_listname(self): - return Utils.fqdn_listname(self._data.list_name, self._data.host_name) - - - # IMailingListAddresses @property diff --git a/Mailman/constants.py b/Mailman/constants.py index 852704364..7b3876a2a 100644 --- a/Mailman/constants.py +++ b/Mailman/constants.py @@ -18,6 +18,9 @@ """Various constants and enumerations.""" from munepy import Enum +from zope.interface import implements + +from Mailman.interfaces import IPreferences @@ -42,3 +45,22 @@ class DeliveryStatus(Enum): by_bounces = 3 # Delivery was disabled by an administrator or moderator by_moderator = 4 + + + +class MemberRole(Enum): + member = 1 + owner = 2 + moderator = 3 + + + +class SystemDefaultPreferences(object): + implements(IPreferences) + + acknowledge_posts = False + hide_address = True + preferred_language = 'en' + receive_list_copy = True + receive_own_postings = True + delivery_mode = DeliveryMode.regular diff --git a/Mailman/database/listmanager.py b/Mailman/database/listmanager.py index de8abbd58..83c913ffb 100644 --- a/Mailman/database/listmanager.py +++ b/Mailman/database/listmanager.py @@ -54,7 +54,6 @@ class ListManager(object): 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 diff --git a/Mailman/database/model/__init__.py b/Mailman/database/model/__init__.py index 11ca11f89..612510632 100644 --- a/Mailman/database/model/__init__.py +++ b/Mailman/database/model/__init__.py @@ -19,9 +19,7 @@ __all__ = [ 'Address', 'Language', 'MailingList', - 'Profile', - 'Roster', - 'RosterSet', + 'Preferences', 'User', 'Version', ] @@ -44,9 +42,8 @@ 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.member import Member +from Mailman.database.model.profile import Preferences from Mailman.database.model.user import User from Mailman.database.model.version import Version diff --git a/Mailman/database/model/address.py b/Mailman/database/model/address.py index 53d5016e5..450c40235 100644 --- a/Mailman/database/model/address.py +++ b/Mailman/database/model/address.py @@ -21,11 +21,12 @@ from zope.interface import implements from Mailman.interfaces import IAddress - -ROSTER_KIND = 'Mailman.database.model.roster.Roster' -USER_KIND = 'Mailman.database.model.user.User' +MEMBER_KIND = 'Mailman.database.model.member.Member' +PREFERENCE_KIND = 'Mailman.database.model.profile.Preferences' +USER_KIND = 'Mailman.database.model.user.User' + class Address(Entity): implements(IAddress) @@ -35,8 +36,10 @@ class Address(Entity): 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) + belongs_to('user', of_kind=USER_KIND) + belongs_to('preferences', of_kind=PREFERENCE_KIND) + # Options + using_options(shortnames=True) def __str__(self): return formataddr((self.real_name, self.address)) @@ -44,3 +47,11 @@ class Address(Entity): def __repr__(self): return '<Address: %s [%s]>' % ( str(self), ('verified' if self.verified else 'not verified')) + + def subscribe(self, mlist, role): + from Mailman.database.model import Member + # This member has no preferences by default. + member = Member(role=role, + mailing_list=mlist.fqdn_listname, + address=self) + return member diff --git a/Mailman/database/model/language.py b/Mailman/database/model/language.py index 3597a128d..e065d5bad 100644 --- a/Mailman/database/model/language.py +++ b/Mailman/database/model/language.py @@ -20,3 +20,5 @@ from elixir import * class Language(Entity): has_field('code', Unicode) + # Options + using_options(shortnames=True) diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py index 28e2c11dc..1e7823911 100644 --- a/Mailman/database/model/mailinglist.py +++ b/Mailman/database/model/mailinglist.py @@ -22,6 +22,8 @@ from Mailman.Utils import fqdn_listname, split_listname from Mailman.configuration import config from Mailman.interfaces import * +PREFERENCE_KIND = 'Mailman.database.model.profile.Preferences' + class MailingList(Entity): @@ -50,16 +52,6 @@ class MailingList(Entity): 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. @@ -163,113 +155,30 @@ class MailingList(Entity): 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 + belongs_to('preferences', of_kind=PREFERENCE_KIND) ## has_and_belongs_to_many( ## 'available_languages', ## of_kind='Mailman.database.model.languages.Language') + # Options + using_options(shortnames=True) 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 + # Create several rosters for filtering out or querying the membership + # table. + from Mailman.database.model import roster + self.owners = roster.OwnerRoster(self) + self.moderators = roster.ModeratorRoster(self) + self.administrators = roster.AdministratorRoster(self) + self.members = roster.MemberRoster(self) + self.regular_members = roster.RegularMemberRoster(self) + self.digest_members = roster.DigestMemberRoster(self) @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 + def fqdn_listname(self): + """See IMailingListIdentity.""" + return fqdn_listname(self.list_name, self.host_name) diff --git a/Mailman/database/model/member.py b/Mailman/database/model/member.py new file mode 100644 index 000000000..bcf859cf4 --- /dev/null +++ b/Mailman/database/model/member.py @@ -0,0 +1,62 @@ +# 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 * +from zope.interface import implements + +from Mailman.Utils import split_listname +from Mailman.constants import SystemDefaultPreferences +from Mailman.database.types import EnumType +from Mailman.interfaces import IMember, IPreferences + + +ADDRESS_KIND = 'Mailman.database.model.address.Address' +PREFERENCE_KIND = 'Mailman.database.model.profile.Preferences' + + + +class Member(Entity): + implements(IMember) + + has_field('role', EnumType) + has_field('mailing_list', Unicode) + # Relationships + belongs_to('address', of_kind=ADDRESS_KIND) + belongs_to('_preferences', of_kind=PREFERENCE_KIND) + # Options + using_options(shortnames=True) + + def __repr__(self): + return '<Member: %s on %s as %s>' % ( + self.address, self.mailing_list, self.role) + + @property + def preferences(self): + from Mailman.database.model import MailingList + if self._preferences: + return self._preferences + if self.address.preferences: + return self.address.preferences + # It's possible this address isn't linked to a user. + if self.address.user and self.address.user.preferences: + return self.address.user.preferences + list_name, host_name = split_listname(self.mailing_list) + mlist = MailingList.get_by(list_name=list_name, + host_name=host_name) + if mlist.preferences: + return mlist.preferences + return SystemDefaultPreferences diff --git a/Mailman/database/model/profile.py b/Mailman/database/model/profile.py index 49e108728..935ae08fb 100644 --- a/Mailman/database/model/profile.py +++ b/Mailman/database/model/profile.py @@ -19,13 +19,18 @@ from elixir import * from email.utils import formataddr from zope.interface import implements -from Mailman.constants import DeliveryMode +from Mailman.constants import SystemDefaultPreferences as Prefs from Mailman.database.types import EnumType -from Mailman.interfaces import IProfile +from Mailman.interfaces import IPreferences +ADDRESS_KIND = 'Mailman.database.model.address.Address' +MEMBER_KIND = 'Mailman.database.model.member.Member' +USER_KIND = 'Mailman.database.model.user.User' -class Profile(Entity): - implements(IProfile) + + +class Preferences(Entity): + implements(IPreferences) has_field('acknowledge_posts', Boolean) has_field('hide_address', Boolean) @@ -33,14 +38,14 @@ class Profile(Entity): 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') + # Options + using_options(shortnames=True) 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 + super(Preferences, self).__init__() + self.acknowledge_posts = Prefs.acknowledge_posts + self.hide_address = Prefs.hide_address + self.preferred_language = Prefs.preferred_language + self.receive_list_copy = Prefs.receive_list_copy + self.receive_own_postings = Prefs.receive_own_postings + self.delivery_mode = Prefs.delivery_mode diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py index bf8447433..03aa9efc3 100644 --- a/Mailman/database/model/roster.py +++ b/Mailman/database/model/roster.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-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 @@ -15,37 +15,164 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -from elixir import * +"""An implementation of an IRoster. + +These are hard-coded rosters which know how to filter a set of members to find +the ones that fit a particular role. These are used as the member, owner, +moderator, and administrator roster filters. +""" + from zope.interface import implements -from Mailman.Errors import ExistingAddressError +from Mailman.constants import DeliveryMode, MemberRole +from Mailman.database.model import Member from Mailman.interfaces import IRoster -ADDRESS_KIND = 'Mailman.database.model.address.Address' -ROSTERSET_KIND = 'Mailman.database.model.rosterset.RosterSet' + +class AbstractRoster(object): + """An abstract IRoster class. + This class takes the simple approach of implemented the 'users' and + 'addresses' properties in terms of the 'members' property. This may not + be the most efficient way, but it works. -class Roster(Entity): + This requires that subclasses implement the 'members' property. + """ 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 __init__(self, mlist): + self._mlist = mlist + + @property + def members(self): + raise NotImplementedError + + @property + def users(self): + # Members are linked to addresses, which in turn are linked to users. + # So while the 'members' attribute does most of the work, we have to + # keep a set of unique users. It's possible for the same user to be + # subscribed to a mailing list multiple times with different + # addresses. + users = set(member.address.user for member in self.members) + for user in users: + yield user + + @property + def addresses(self): + # Every Member is linked to exactly one address so the 'members' + # attribute does most of the work. + for member in self.members: + yield member.address + + + +class MemberRoster(AbstractRoster): + """Return all the members of a list.""" + + name = 'member' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. XXX we have to use a private + # data attribute of MailList for now. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.member): + yield member + + + +class OwnerRoster(AbstractRoster): + """Return all the owners of a list.""" + + name = 'owner' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. XXX we have to use a private + # data attribute of MailList for now. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.owner): + yield member + + + +class ModeratorRoster(AbstractRoster): + """Return all the owners of a list.""" + + name = 'moderator' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. XXX we have to use a private + # data attribute of MailList for now. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.moderator): + yield member + + + +class AdministratorRoster(AbstractRoster): + """Return all the administrators of a list.""" + + name = 'administrator' + + @property + def members(self): + # Administrators are defined as the union of the owners and the + # moderators. Until I figure out a more efficient way of doing this, + # this will have to do. + owners = Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.owner) + moderators = Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.moderator) + members = set(owners) + members.update(set(moderators)) + for member in members: + yield member + + + +class RegularMemberRoster(AbstractRoster): + """Return all the regular delivery members of a list.""" + + name = 'regular_members' + + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. Then return only those members + # that have a regular delivery mode. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.member): + if member.preferences.delivery_mode == DeliveryMode.regular: + yield member + + + +_digest_modes = ( + DeliveryMode.mime_digests, + DeliveryMode.plaintext_digests, + DeliveryMode.summary_digests, + ) + + + +class DigestMemberRoster(AbstractRoster): + """Return all the regular delivery members of a list.""" + + name = 'regular_members' - 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 + @property + def members(self): + # Query for all the Members which have a role of MemberRole.member and + # are subscribed to this mailing list. Then return only those members + # that have one of the digest delivery modes. + for member in Member.select_by(mailing_list=self._mlist.fqdn_listname, + role=MemberRole.member): + if member.preferences.delivery_mode in _digest_modes: + yield member diff --git a/Mailman/database/model/user.py b/Mailman/database/model/user.py index be634b9df..45fcbfdd3 100644 --- a/Mailman/database/model/user.py +++ b/Mailman/database/model/user.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-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 @@ -23,15 +23,24 @@ from Mailman import Errors from Mailman.database.model import Address from Mailman.interfaces import IUser +ADDRESS_KIND = 'Mailman.database.model.address.Address' +PREFERENCE_KIND = 'Mailman.database.model.profile.Preferences' + + 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') + has_many('addresses', of_kind=ADDRESS_KIND) + belongs_to('preferences', of_kind=PREFERENCE_KIND) + # Options + using_options(shortnames=True) + + def __repr__(self): + return '<User "%s" at %#x>' % (self.real_name, id(self)) def link(self, address): if address.user is not None: @@ -46,5 +55,5 @@ class User(Entity): self.addresses.remove(address) def controls(self, address): - found = Address.get_by(address=address.address) + found = Address.get_by(address=address) return bool(found and found.user is self) diff --git a/Mailman/database/model/version.py b/Mailman/database/model/version.py index e22e8ae11..7b12778ce 100644 --- a/Mailman/database/model/version.py +++ b/Mailman/database/model/version.py @@ -19,7 +19,7 @@ from elixir import * class Version(Entity): - with_fields( - component = Field(String), - version = Field(Integer), - ) + has_field('component', Unicode) + has_field('version', Integer) + # Options + using_options(shortnames=True) diff --git a/Mailman/database/usermanager.py b/Mailman/database/usermanager.py index 97a740803..2f12188aa 100644 --- a/Mailman/database/usermanager.py +++ b/Mailman/database/usermanager.py @@ -35,47 +35,11 @@ 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): + def create_user(self, address=None, real_name=None): user = User() - # Users always have a profile - user.profile = Profile() - user.profile.user = user + user.real_name = (real_name if real_name is not None else '') + if address: + user.link(Address(address=address, real_name=user.real_name)) return user def delete_user(self, user): @@ -89,3 +53,27 @@ class UserManager(object): def get_user(self, address): found = Address.get_by(address=address) return found and found.user + + def create_address(self, address, real_name=None): + found = Address.get_by(address=address) + if found: + raise Errors.ExistingAddressError(address) + if real_name is None: + real_name = '' + address = Address(address=address, real_name=real_name) + return address + + def delete_address(self, address): + # If there's a user controlling this address, it has to first be + # unlinked before the address can be deleted. + if address.user: + address.user.unlink(address) + address.delete() + + def get_address(self, address): + return Address.get_by(address=address) + + @property + def addresses(self): + for address in Address.select(): + yield address diff --git a/Mailman/docs/addresses.txt b/Mailman/docs/addresses.txt index a8cf9f655..c6a2a9873 100644 --- a/Mailman/docs/addresses.txt +++ b/Mailman/docs/addresses.txt @@ -1,144 +1,194 @@ -Email addresses and rosters -=========================== +Email addresses +=============== -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. +Addresses represent a text email address, along with some meta data about +those addresses, such as their registration date, and whether and when they've +been validated. Addresses may be linked to the users that Mailman knows +about. Addresses are subscribed to mailing lists though members. >>> 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. +Addresses are created directly through the user manager, which starts out with +no addresses. - >>> addr_1 = roster_1.create('aperson@example.com') + >>> sorted(address.address for address in mgr.addresses) + [] + +Creating an unlinked email address is straightforward. + + >>> address_1 = mgr.create_address('aperson@example.com') >>> flush() - >>> addr_1.address - 'aperson@example.com' - >>> addr_1.real_name is None - True + >>> sorted(address.address for address in mgr.addresses) + ['aperson@example.com'] -You can also create an email address object with a real name. +However, such addresses have no real name. - >>> addr_2 = roster_1.create('bperson@example.com', 'Barney Person') - >>> addr_2.address - 'bperson@example.com' - >>> addr_2.real_name - 'Barney Person' + >>> address_1.real_name + '' -You can also iterate through all the addresses on a roster. +You can also create an email address object with a real name. - >>> sorted(addr.address for addr in roster_1.addresses) + >>> address_2 = mgr.create_address('bperson@example.com', 'Ben Person') + >>> flush() + >>> sorted(address.address for address in mgr.addresses) ['aperson@example.com', 'bperson@example.com'] + >>> sorted(address.real_name for address in mgr.addresses) + ['', 'Ben Person'] -You can create another roster and add a bunch of existing addresses to the -second roster. +You can assign real names to existing addresses. - >>> roster_2 = mgr.create_roster('roster-2') + >>> address_1.real_name = 'Anne Person' >>> 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) + >>> sorted(address.real_name for address in mgr.addresses) + ['Anne Person', 'Ben Person'] + +These addresses are not linked to users, and can be seen by searching the user +manager for an associated user. + + >>> print mgr.get_user('aperson@example.com') + None + >>> print mgr.get_user('bperson@example.com') + None + +You can create email addresses that are linked to users by using a different +interface. + + >>> user_1 = mgr.create_user('cperson@example.com', 'Claire Person') + >>> flush() + >>> sorted(address.address for address in mgr.addresses) ['aperson@example.com', 'bperson@example.com', 'cperson@example.com'] + >>> sorted(address.real_name for address in mgr.addresses) + ['Anne Person', 'Ben Person', 'Claire Person'] -The first roster hasn't been affected. +And now you can find the associated user. - >>> sorted(addr.address for addr in roster_1.addresses) - ['aperson@example.com', 'bperson@example.com'] + >>> print mgr.get_user('aperson@example.com') + None + >>> print mgr.get_user('bperson@example.com') + None + >>> mgr.get_user('cperson@example.com') + <User "Claire Person" at ...> -Removing addresses +Deleting addresses ------------------ -You can remove an address from a roster just by deleting it. +You can remove an unlinked address from the usre manager. - >>> 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'] + >>> mgr.delete_address(address_1) + >>> flush() + >>> sorted(address.address for address in mgr.addresses) + ['bperson@example.com', 'cperson@example.com'] + >>> sorted(address.real_name for address in mgr.addresses) + ['Ben Person', 'Claire Person'] -Again, this doesn't affect the other rosters. +Deleting a linked address does not delete the user, but it does unlink the +address from the user. - >>> sorted(addr.address for addr in roster_2.addresses) - ['aperson@example.com', 'bperson@example.com', 'cperson@example.com'] + >>> sorted(address.address for address in user_1.addresses) + ['cperson@example.com'] + >>> user_1.controls('cperson@example.com') + True + >>> address_3 = list(user_1.addresses)[0] + >>> mgr.delete_address(address_3) + >>> flush() + >>> sorted(address.address for address in user_1.addresses) + [] + >>> user_1.controls('cperson@example.com') + False + >>> sorted(address.address for address in mgr.addresses) + ['bperson@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. +the address was validated on. Neither date is set by default. - >>> addr = roster_1.create('dperson@example.com', 'David Person') - >>> addr.registered_on is None - True - >>> addr.validated_on is None - True + >>> address_4 = mgr.create_address('dperson@example.com', 'Dan Person') + >>> flush() + >>> print address_4.registered_on + None + >>> print address_4.validated_on + None 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 + >>> address_4.registered_on = datetime(2007, 5, 8, 22, 54, 1) + >>> flush() + >>> print address_4.registered_on 2007-05-08 22:54:01 - >>> addr.validated_on is None - True + >>> print address_4.validated_on + None And of course, you can also set the validation date. - >>> addr.validated_on = datetime(2007, 5, 13, 22, 54, 1) - >>> print addr.registered_on + >>> address_4.validated_on = datetime(2007, 5, 13, 22, 54, 1) + >>> flush() + >>> print address_4.registered_on 2007-05-08 22:54:01 - >>> print addr.validated_on + >>> print address_4.validated_on 2007-05-13 22:54:01 -The null roster ---------------- +Subscriptions +------------- -All address objects that have been created are members of the null roster. +Addresses get subscribed to mailing lists, not users. When the address is +subscribed, a role is specified. - >>> all = mgr.get_roster('') - >>> sorted(addr.address for addr in all.addresses) - ['aperson@example.com', 'bperson@example.com', - 'cperson@example.com', 'dperson@example.com'] + >>> address_5 = mgr.create_address('eperson@example.com', 'Elly Person') + >>> mlist = config.list_manager.create('_xtext@example.com') + >>> from Mailman.constants import MemberRole + >>> address_5.subscribe(mlist, MemberRole.owner) + <Member: Elly Person <eperson@example.com> on + _xtext@example.com as MemberRole.owner> + >>> address_5.subscribe(mlist, MemberRole.member) + <Member: Elly Person <eperson@example.com> on + _xtext@example.com as MemberRole.member> + >>> flush() -And conversely, all addresses should have the null roster on their list of -rosters. +Now that Elly is both an owner and a member of the mailing list. - >>> for addr in all.addresses: - ... assert all in addr.rosters, 'Address is missing null roster' + >>> sorted(mlist.owners.members) + [<Member: Elly Person <eperson@example.com> on + _xtext@example.com as MemberRole.owner>] + >>> sorted(mlist.moderators.members) + [] + >>> sorted(mlist.administrators.members) + [<Member: Elly Person <eperson@example.com> on + _xtext@example.com as MemberRole.owner>] + >>> sorted(mlist.members.members) + [<Member: Elly Person <eperson@example.com> on + _xtext@example.com as MemberRole.member>] + >>> sorted(mlist.regular_members.members) + [<Member: Elly Person <eperson@example.com> on + _xtext@example.com as MemberRole.member>] + >>> sorted(mlist.digest_members.members) + [] Clean up -------- - >>> for roster in mgr.rosters: - ... mgr.delete_roster(roster) + >>> for mlist in config.list_manager.mailing_lists: + ... config.list_manager.delete(mlist) + >>> for user in mgr.users: + ... mgr.delete_user(user) + >>> for address in mgr.addresses: + ... mgr.delete_address(address) >>> flush() - >>> sorted(roster.name for roster in mgr.rosters) + >>> sorted(config.list_manager.names) + [] + >>> sorted(mgr.users) + [] + >>> sorted(mgr.addresses) [] diff --git a/Mailman/docs/listmanager.txt b/Mailman/docs/listmanager.txt index 9e237f02f..03943a237 100644 --- a/Mailman/docs/listmanager.txt +++ b/Mailman/docs/listmanager.txt @@ -54,17 +54,12 @@ you will get an exception. 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'] +Use the list manager to delete a mailing list. >>> 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: diff --git a/Mailman/docs/membership.txt b/Mailman/docs/membership.txt new file mode 100644 index 000000000..36b1508a8 --- /dev/null +++ b/Mailman/docs/membership.txt @@ -0,0 +1,183 @@ +List memberships +================ + +Users represent people in Mailman. Users control email addresses, and rosters +are collectons of members. A member gives an email address a role, such as +'member', 'administrator', or 'moderator'. Roster sets are collections of +rosters and a mailing list has a single roster set that contains all its +members, regardless of that member's role. + +Mailing lists and roster sets have an indirect relationship, through the +roster set's name. Roster also have names, but are related to roster sets +by a more direct containment relationship. This is because it is possible to +store mailing list data in a different database than user data. + +When we create a mailing list, it starts out with no members... + + >>> from Mailman.configuration import config + >>> from Mailman.database import flush + >>> mlist = config.list_manager.create('_xtest@example.com') + >>> flush() + >>> mlist + <mailing list "_xtest@example.com" (unlocked) at ...> + >>> sorted(member.address.address for member in mlist.members.members) + [] + >>> sorted(user.real_name for user in mlist.members.users) + [] + >>> sorted(address.address for member in mlist.members.addresses) + [] + +...no owners... + + >>> sorted(member.address.address for member in mlist.owners.members) + [] + >>> sorted(user.real_name for user in mlist.owners.users) + [] + >>> sorted(address.address for member in mlist.owners.addresses) + [] + +...no moderators... + + >>> sorted(member.address.address for member in mlist.moderators.members) + [] + >>> sorted(user.real_name for user in mlist.moderators.users) + [] + >>> sorted(address.address for member in mlist.moderators.addresses) + [] + +...and no administrators. + + >>> sorted(member.address.address + ... for member in mlist.administrators.members) + [] + >>> sorted(user.real_name for user in mlist.administrators.users) + [] + >>> sorted(address.address for member in mlist.administrators.addresses) + [] + + + +Administrators +-------------- + +A mailing list's administrators are defined as union of the list's owners and +the list's moderators. We can add new owners or moderators to this list by +assigning roles to users. First we have to create the user, because there are +no users in the user database yet. + + >>> user_1 = config.user_manager.create_user( + ... 'aperson@example.com', 'Anne Person') + >>> flush() + >>> user_1.real_name + 'Anne Person' + >>> sorted(address.address for address in user_1.addresses) + ['aperson@example.com'] + +We can add Anne as an owner of the mailing list, by creating a member role for +her. + + >>> from Mailman.constants import MemberRole + >>> address_1 = list(user_1.addresses)[0] + >>> address_1.address + 'aperson@example.com' + >>> address_1.subscribe(mlist, MemberRole.owner) + <Member: Anne Person <aperson@example.com> on + _xtest@example.com as MemberRole.owner> + >>> flush() + >>> sorted(member.address.address for member in mlist.owners.members) + ['aperson@example.com'] + >>> sorted(user.real_name for user in mlist.owners.users) + ['Anne Person'] + >>> sorted(address.address for address in mlist.owners.addresses) + ['aperson@example.com'] + +Adding Anne as a list owner also makes her an administrator, but does not make +her a moderator. Nor does it make her a member of the list. + + >>> sorted(user.real_name for user in mlist.administrators.users) + ['Anne Person'] + >>> sorted(user.real_name for user in mlist.moderators.users) + [] + >>> sorted(user.real_name for user in mlist.members.users) + [] + +We can add Ben as a moderator of the list, by creating a different member role +for him. + + >>> user_2 = config.user_manager.create_user( + ... 'bperson@example.com', 'Ben Person') + >>> flush() + >>> user_2.real_name + 'Ben Person' + >>> address_2 = list(user_2.addresses)[0] + >>> address_2.address + 'bperson@example.com' + >>> address_2.subscribe(mlist, MemberRole.moderator) + <Member: Ben Person <bperson@example.com> + on _xtest@example.com as MemberRole.moderator> + >>> flush() + >>> sorted(member.address.address for member in mlist.moderators.members) + ['bperson@example.com'] + >>> sorted(user.real_name for user in mlist.moderators.users) + ['Ben Person'] + >>> sorted(address.address for address in mlist.moderators.addresses) + ['bperson@example.com'] + +Now, both Anne and Ben are list administrators. + + >>> sorted(member.address.address + ... for member in mlist.administrators.members) + ['aperson@example.com', 'bperson@example.com'] + >>> sorted(user.real_name for user in mlist.administrators.users) + ['Anne Person', 'Ben Person'] + >>> sorted(address.address for address in mlist.administrators.addresses) + ['aperson@example.com', 'bperson@example.com'] + + +Members +------- + +Similarly, list members are born of users being given the proper role. It's +more interesting here because these roles should have a preference which can +be used to decide whether the member is to get regular delivery or digest +delivery. Without a preference, Mailman will fall back first to the address's +preference, then the user's preference, then the list's preference. Start +without any member preference to see the system defaults. + + >>> user_3 = config.user_manager.create_user( + ... 'cperson@example.com', 'Claire Person') + >>> flush() + >>> user_3.real_name + 'Claire Person' + >>> address_3 = list(user_3.addresses)[0] + >>> address_3.address + 'cperson@example.com' + >>> address_3.subscribe(mlist, MemberRole.member) + <Member: Claire Person <cperson@example.com> + on _xtest@example.com as MemberRole.member> + >>> flush() + +Claire will be a regular delivery member but not a digest member. + + >>> sorted(address.address for address in mlist.members.addresses) + ['cperson@example.com'] + >>> sorted(address.address for address in mlist.regular_members.addresses) + ['cperson@example.com'] + >>> sorted(address.address for address in mlist.digest_members.addresses) + [] + +It's easy to make the list administrators members of the mailing list too. + + >>> for address in mlist.administrators.addresses: + ... address.subscribe(mlist, MemberRole.member) + <Member: Ben Person <bperson@example.com> on + _xtest@example.com as MemberRole.member> + <Member: Anne Person <aperson@example.com> on + _xtest@example.com as MemberRole.member> + >>> flush() + >>> sorted(address.address for address in mlist.members.addresses) + ['aperson@example.com', 'bperson@example.com', 'cperson@example.com'] + >>> sorted(address.address for address in mlist.regular_members.addresses) + ['aperson@example.com', 'bperson@example.com', 'cperson@example.com'] + >>> sorted(address.address for address in mlist.digest_members.addresses) + [] diff --git a/Mailman/docs/mlist-rosters.txt b/Mailman/docs/mlist-rosters.txt deleted file mode 100644 index 096ceb7d2..000000000 --- a/Mailman/docs/mlist-rosters.txt +++ /dev/null @@ -1,127 +0,0 @@ -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'] - - -Clean up --------- - - >>> config.list_manager.delete(mlist) - >>> flush() - >>> [name for name in config.list_manager.names] - [] diff --git a/Mailman/docs/usermanager.txt b/Mailman/docs/usermanager.txt index f79bff8c6..389c68956 100644 --- a/Mailman/docs/usermanager.txt +++ b/Mailman/docs/usermanager.txt @@ -1,100 +1,141 @@ -The user manager and rosters -============================ +The user manager +================ -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. +The IUserManager is how you create, delete, and manage 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.database import flush >>> from Mailman.interfaces import IUserManager >>> mgr = config.user_manager >>> IUserManager.providedBy(mgr) True -The default roster ------------------- +Creating users +-------------- -The user manager always contains at least one roster, the 'null' roster or -'all inclusive roster'. +There are several ways you can create a user object. The simplest is to +create a 'blank' user by not providing an address or real name at creation +time. This user will have an empty string as their real name, but will not +have a password or preferences. - >>> sorted(roster.name for roster in mgr.rosters) - [''] + >>> from Mailman.interfaces import IUser + >>> user = mgr.create_user() + >>> flush() + >>> IUser.providedBy(user) + True + >>> sorted(address.address for address in user.addresses) + [] + >>> user.real_name + '' + >>> print user.preferences + None + >>> print user.password + None +A user can be assigned a real name. -Adding rosters --------------- + >>> user.real_name = 'Anne Person' + >>> flush() + >>> sorted(user.real_name for user in mgr.users) + ['Anne Person'] -You create a roster to hold users. The only thing a roster needs is a name, -basically just an identifying string. +A user can be assigned a password. - >>> from Mailman.database import flush - >>> from Mailman.interfaces import IRoster - >>> roster = mgr.create_roster('roster-1') - >>> IRoster.providedBy(roster) - True - >>> roster.name - 'roster-1' + >>> user.password = 'secret' >>> flush() + >>> sorted(user.password for user in mgr.users) + ['secret'] -If you try to create a roster with the same name as an existing roster, you -will get an exception. +You can also create a user with an address to start out with. - >>> roster_dup = mgr.create_roster('roster-1') - Traceback (most recent call last): - ... - RosterExistsError: roster-1 + >>> user_2 = mgr.create_user('bperson@example.com') + >>> flush() + >>> IUser.providedBy(user_2) + True + >>> sorted(address.address for address in user_2.addresses) + ['bperson@example.com'] + >>> sorted(user.real_name for user in mgr.users) + ['', 'Anne Person'] +As above, you can assign a real name to such users. -Deleting a roster ------------------ + >>> user_2.real_name = 'Ben Person' + >>> flush() + >>> sorted(user.real_name for user in mgr.users) + ['Anne Person', 'Ben Person'] -Delete the roster, and you can then create it again. +You can also create a user with just a real name. - >>> mgr.delete_roster(roster) + >>> user_3 = mgr.create_user(real_name='Claire Person') >>> flush() - >>> roster = mgr.create_roster('roster-1') + >>> IUser.providedBy(user_3) + True + >>> sorted(address.address for address in user.addresses) + [] + >>> sorted(user.real_name for user in mgr.users) + ['Anne Person', 'Ben Person', 'Claire Person'] + +Finally, you can create a user with both an address and a real name. + + >>> user_4 = mgr.create_user('dperson@example.com', 'Dan Person') >>> flush() - >>> roster.name - 'roster-1' + >>> IUser.providedBy(user_3) + True + >>> sorted(address.address for address in user_4.addresses) + ['dperson@example.com'] + >>> sorted(address.real_name for address in user_4.addresses) + ['Dan Person'] + >>> sorted(user.real_name for user in mgr.users) + ['Anne Person', 'Ben Person', 'Claire Person', 'Dan Person'] -Retrieving a roster -------------------- +Deleting users +-------------- -When a roster exists, you can ask the user manager for it and you will always -get the same object back. +You delete users by going through the user manager. The deleted user is no +longer available through the user manager iterator. - >>> roster_2 = mgr.get_roster('roster-1') - >>> roster_2.name - 'roster-1' - >>> roster is roster_2 - True + >>> mgr.delete_user(user) + >>> flush() + >>> sorted(user.real_name for user in mgr.users) + ['Ben Person', 'Claire Person', 'Dan Person'] -Trying to get a roster that does not yet exist returns None. - >>> print mgr.get_roster('no roster') - None +Finding users +------------- +You can ask the user manager to find the IUser that controls a particular +email address. You'll get back the original user object if it's found. Note +that the .get_user() method takes a string email address, not an IAddress +object. -Iterating over all the rosters ------------------------------- + >>> address = list(user_4.addresses)[0] + >>> found_user = mgr.get_user(address.address) + >>> found_user + <User "Dan Person" at ...> + >>> found_user is user_4 + True -Once you've created a bunch of rosters, you can use the user manager to -iterate over all the rosters. +If the address is not in the user database or does not have a user associated +with it, you will get None back. - >>> roster_2 = mgr.create_roster('roster-2') - >>> roster_3 = mgr.create_roster('roster-3') - >>> roster_4 = mgr.create_roster('roster-4') + >>> print mgr.get_user('zperson@example.com') + None + >>> user_4.unlink(address) >>> flush() - >>> sorted(roster.name for roster in mgr.rosters) - ['', 'roster-1', 'roster-2', 'roster-3', 'roster-4'] + >>> print mgr.get_user(address.address) + None -Cleaning up ------------ +Clean up +-------- - >>> for roster in mgr.rosters: - ... mgr.delete_roster(roster) + >>> for user in mgr.users: + ... mgr.delete_user(user) >>> flush() + >>> list(mgr.users) + [] diff --git a/Mailman/docs/users.txt b/Mailman/docs/users.txt index caad6b216..a65527eff 100644 --- a/Mailman/docs/users.txt +++ b/Mailman/docs/users.txt @@ -1,83 +1,44 @@ 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. +Users are entities that represent people. A user has a real name and a +password. Optionally a user may have some preferences and a set of addresses +they control. - -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. +See usermanager.txt for examples of how to create, delete, and find users. >>> 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 - [] +User data +--------- -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. +Users may have a real name and a password. >>> 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' + >>> sorted(user.real_name for user in mgr.users) + ['Zoe Person'] + >>> sorted(user.password for user in mgr.users) + ['my password'] 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' + >>> flush() + >>> sorted(user.real_name for user in mgr.users) + ['Zoe X. Person'] + >>> sorted(user.password for user in mgr.users) + ['another password'] -Users and addresses -------------------- +Users 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 diff --git a/Mailman/interfaces/address.py b/Mailman/interfaces/address.py index 5f6a9193d..1363d56ab 100644 --- a/Mailman/interfaces/address.py +++ b/Mailman/interfaces/address.py @@ -42,3 +42,9 @@ class IAddress(Interface): """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.""") + + def subscribe(mlist, role): + """Subscribe the address to the given mailing list with the given role. + + role is a Mailman.constants.MemberRole enum. + """ diff --git a/Mailman/testing/test_mlist_rosters.py b/Mailman/interfaces/member.py index e8713b828..5d5e3f717 100644 --- a/Mailman/testing/test_mlist_rosters.py +++ b/Mailman/interfaces/member.py @@ -15,16 +15,34 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -"""Doctest harness for the IMailingListRosters interface.""" -import doctest -import unittest +"""Interface describing the basics of a member.""" -options = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE +from zope.interface import Interface, Attribute -def test_suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocFileSuite('../docs/mlist-rosters.txt', - optionflags=options)) - return suite + +class IMember(Interface): + """A member of a mailing list.""" + + mailing_list = Attribute( + """The mailing list subscribed to.""") + + address = Attribute( + """The email address that's subscribed to the list.""") + + preferences = Attribute( + """The set of preferences for this subscription. + + This will return an IPreferences object using the following lookup + rules: + + 1. member + 2. address + 3. user + 4. mailing list + 5. system default + """) + + role = Attribute( + """The role of this membership.""") diff --git a/Mailman/interfaces/mlistrosters.py b/Mailman/interfaces/mlistrosters.py index 1b407f472..9cd20e3ef 100644 --- a/Mailman/interfaces/mlistrosters.py +++ b/Mailman/interfaces/mlistrosters.py @@ -46,27 +46,6 @@ class IMailingListRosters(Interface): 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 @@ -82,15 +61,3 @@ 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.""") - - 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/profile.py b/Mailman/interfaces/profile.py index ed3968f9d..a0b5131cb 100644 --- a/Mailman/interfaces/profile.py +++ b/Mailman/interfaces/profile.py @@ -15,13 +15,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. -"""Interface for a profile, which describes delivery related information.""" +"""Interface for preferences.""" from zope.interface import Interface, Attribute -class IProfile(Interface): +class IPreferences(Interface): """Delivery related information.""" acknowledge_posts = Attribute( diff --git a/Mailman/interfaces/roster.py b/Mailman/interfaces/roster.py index 7ddbd5101..281e02a05 100644 --- a/Mailman/interfaces/roster.py +++ b/Mailman/interfaces/roster.py @@ -22,21 +22,25 @@ from zope.interface import Interface, Attribute class IRoster(Interface): - """A roster is a collection of IUsers.""" + """A roster is a collection of IMembers.""" 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.""") + members = Attribute( + """An iterator over all the IMembers managed by this roster.""") + + users = Attribute( + """An iterator over all the IUsers reachable by this roster. - def create(email_address, real_name=None): - """Create an IAddress and return it. + This returns all the users for all the members managed by this roster. + """) - email_address is textual email address to add. real_name is the - optional real name that gets associated with the email address. + addresses = Attribute( + """An iterator over all the IAddresses reachable by this roster. - Raises ExistingAddressError if address already exists. - """ + This returns all the addresses for all the users for all the members + managed by this roster. + """) diff --git a/Mailman/interfaces/user.py b/Mailman/interfaces/user.py index 6990eee4b..9e89c2416 100644 --- a/Mailman/interfaces/user.py +++ b/Mailman/interfaces/user.py @@ -30,12 +30,21 @@ class IUser(Interface): password = Attribute( """This user's password information.""") - profile = Attribute( - """The default IProfile for this user.""") + preferences = Attribute( + """The default preferences for this user.""") addresses = Attribute( """An iterator over all the IAddresses controlled by this user.""") + def register(address): + """Register the given email address and link it to this user. + + In this case, 'address' is a text email address, not an IAddress + object. Raises AddressAlreadyLinkedError if this IAddress is already + linked to another user. If the corresponding IAddress already exists + but is not linked, then it is simply linked to the user. + """ + def link(address): """Link this user to the given IAddress. diff --git a/Mailman/interfaces/usermanager.py b/Mailman/interfaces/usermanager.py index 302fe9b60..f201b3591 100644 --- a/Mailman/interfaces/usermanager.py +++ b/Mailman/interfaces/usermanager.py @@ -33,30 +33,19 @@ class IUserManager(Interface): IUsers in all IRosters. """ - def create_roster(name): - """Create and return the named IRoster. + def create_user(address=None, real_name=None): + """Create and return an IUser. - 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. + When address is given, an IAddress is also created and linked to the + new IUser object. If the address already exists, an + ExistingAddressError is raised. If the address exists but is already + linked to another user, an AddressAlreadyLinkedError is raised. - Raises NoSuchRosterError if the named roster doesnot yet exist. + When real_name is given, the IUser's real_name is set to this string. + If an IAddress is also created and linked, its real_name is set to the + same string. """ - 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.""" @@ -69,14 +58,27 @@ class IUserManager(Interface): users = Attribute( """An iterator over all the IUsers managed by this user manager.""") - def create_rosterset(): - """Create and return a new IRosterSet. + def create_address(address, real_name=None): + """Create and return an unlinked IAddress object. + + address is the text email address. If real_name is not given, it + defaults to the empty string. If the IAddress already exists an + ExistingAddressError is raised. + """ + + def delete_address(address): + """Delete the given IAddress object. - IRosterSets manage groups of IRosters. + If this IAddress linked to a user, it is first unlinked before it is + deleted. """ - def delete_rosterset(rosterset): - """Delete the given IRosterSet.""" + def get_address(address): + """Find and return an IAddress. + + 'address' is a text email address. None is returned if there is no + registered IAddress for the given text address. + """ - def get_rosterset(serial): - """Return the IRosterSet that matches the serial number, or None.""" + addresses = Attribute( + """An iterator over all the IAddresses managed by this manager.""") diff --git a/Mailman/testing/test_membership.py b/Mailman/testing/test_membership.py index 8e8285034..8a25287ac 100644 --- a/Mailman/testing/test_membership.py +++ b/Mailman/testing/test_membership.py @@ -19,6 +19,7 @@ import os import time +import doctest import unittest from Mailman import MailList @@ -30,6 +31,8 @@ from Mailman.UserDesc import UserDesc from Mailman.configuration import config from Mailman.testing.base import TestBase +OPTIONFLAGS = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE + def password(cleartext): @@ -387,6 +390,6 @@ class TestMembers(TestBase): def test_suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TestNoMembers)) - suite.addTest(unittest.makeSuite(TestMembers)) + suite.addTest(doctest.DocFileSuite('../docs/membership.txt', + optionflags=OPTIONFLAGS)) return suite |
