diff options
35 files changed, 366 insertions, 244 deletions
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py index 0a291d671..41a8f3560 100644 --- a/src/mailman/app/bounces.py +++ b/src/mailman/app/bounces.py @@ -192,7 +192,8 @@ def send_probe(member, msg): :return: The token representing this probe in the pendings database. :rtype: string """ - mlist = getUtility(IListManager).get(member.mailing_list) + mlist = getUtility(IListManager).get_by_list_id( + member.mailing_list.list_id) text = make('probe.txt', mlist, member.preferred_language.code, listname=mlist.fqdn_listname, address= member.address.email, diff --git a/src/mailman/app/docs/lifecycle.rst b/src/mailman/app/docs/lifecycle.rst index 9a3337123..f6bb7ddae 100644 --- a/src/mailman/app/docs/lifecycle.rst +++ b/src/mailman/app/docs/lifecycle.rst @@ -140,7 +140,7 @@ artifacts. :: >>> from mailman.app.lifecycle import remove_list - >>> remove_list(mlist_2.fqdn_listname, mlist_2) + >>> remove_list(mlist_2) >>> from mailman.interfaces.listmanager import IListManager >>> from zope.component import getUtility diff --git a/src/mailman/app/docs/subscriptions.rst b/src/mailman/app/docs/subscriptions.rst index f897d219e..dd8298cb3 100644 --- a/src/mailman/app/docs/subscriptions.rst +++ b/src/mailman/app/docs/subscriptions.rst @@ -30,13 +30,13 @@ role. At a minimum, a mailing list and an address for the new user is required. >>> mlist = create_list('test@example.com') - >>> anne = service.join('test@example.com', 'anne@example.com') + >>> anne = service.join('test.example.com', 'anne@example.com') >>> anne <Member: anne <anne@example.com> on test@example.com as MemberRole.member> The real name of the new member can be given. - >>> bart = service.join('test@example.com', 'bart@example.com', + >>> bart = service.join('test.example.com', 'bart@example.com', ... 'Bart Person') >>> bart <Member: Bart Person <bart@example.com> @@ -45,7 +45,7 @@ The real name of the new member can be given. Other roles can also be subscribed. >>> from mailman.interfaces.member import MemberRole - >>> anne_owner = service.join('test@example.com', 'anne@example.com', + >>> anne_owner = service.join('test.example.com', 'anne@example.com', ... role=MemberRole.owner) >>> anne_owner <Member: anne <anne@example.com> on test@example.com as MemberRole.owner> @@ -67,7 +67,7 @@ New members can also be added by providing an existing user id instead of an email address. However, the user must have a preferred email address. :: - >>> service.join('test@example.com', bart.user.user_id, + >>> service.join('test.example.com', bart.user.user_id, ... role=MemberRole.owner) Traceback (most recent call last): ... @@ -78,7 +78,7 @@ email address. However, the user must have a preferred email address. >>> address = list(bart.user.addresses)[0] >>> address.verified_on = now() >>> bart.user.preferred_address = address - >>> service.join('test@example.com', bart.user.user_id, + >>> service.join('test.example.com', bart.user.user_id, ... role=MemberRole.owner) <Member: Bart Person <bart@example.com> on test@example.com as MemberRole.owner> @@ -89,7 +89,7 @@ Removing members Regular members can also be removed. - >>> cris = service.join('test@example.com', 'cris@example.com') + >>> cris = service.join('test.example.com', 'cris@example.com') >>> service.get_members() [<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>, @@ -103,7 +103,7 @@ Regular members can also be removed. as MemberRole.member>] >>> sum(1 for member in service) 5 - >>> service.leave('test@example.com', 'cris@example.com') + >>> service.leave('test.example.com', 'cris@example.com') >>> service.get_members() [<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>, @@ -173,7 +173,7 @@ Memberships can also be searched for by user id. You can find all the memberships for a specific mailing list. - >>> service.find_members(fqdn_listname='test@example.com') + >>> service.find_members(list_id='test.example.com') [<Member: anne <anne@example.com> on test@example.com as MemberRole.member>, <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>, @@ -184,9 +184,11 @@ You can find all the memberships for a specific mailing list. <Member: Bart Person <bart@example.com> on test@example.com as MemberRole.owner>] -You can find all the memberships for an address on a specific mailing list. +You can find all the memberships for an address on a specific mailing list, +but you have to give it the list id, not the fqdn listname since the former is +stable but the latter could change if the list is moved. - >>> service.find_members('anne@example.com', 'test@example.com') + >>> service.find_members('anne@example.com', 'test.example.com') [<Member: anne <anne@example.com> on test@example.com as MemberRole.member>, <Member: anne <anne@example.com> on test@example.com @@ -203,7 +205,7 @@ You can find all the memberships for an address with a specific role. You can also find a specific membership by all three criteria. - >>> service.find_members('anne@example.com', 'test@example.com', + >>> service.find_members('anne@example.com', 'test.example.com', ... MemberRole.owner) [<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>] diff --git a/src/mailman/app/events.py b/src/mailman/app/events.py index a4f385239..28d78e001 100644 --- a/src/mailman/app/events.py +++ b/src/mailman/app/events.py @@ -41,7 +41,7 @@ def initialize(): domain.handle_DomainDeletingEvent, moderator.handle_ListDeletingEvent, passwords.handle_ConfigurationUpdatedEvent, - subscriptions.handle_ListDeletedEvent, + subscriptions.handle_ListDeletingEvent, switchboard.handle_ConfigurationUpdatedEvent, i18n.handle_ConfigurationUpdatedEvent, style_manager.handle_ConfigurationUpdatedEvent, diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py index 5082034bc..326498478 100644 --- a/src/mailman/app/lifecycle.py +++ b/src/mailman/app/lifecycle.py @@ -89,23 +89,19 @@ def create_list(fqdn_listname, owners=None): -def remove_list(fqdn_listname, mailing_list=None): +def remove_list(mlist): """Remove the list and all associated artifacts and subscriptions.""" + fqdn_listname = mlist.fqdn_listname removeables = [] - # mailing_list will be None when only residual archives are being removed. - if mailing_list is not None: - # Remove all subscriptions, regardless of role. - for member in mailing_list.subscribers.members: - member.unsubscribe() - # Delete the mailing list from the database. - getUtility(IListManager).delete(mailing_list) - # Do the MTA-specific list deletion tasks - call_name(config.mta.incoming).create(mailing_list) - # Remove the list directory. - removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname)) + # Delete the mailing list from the database. + getUtility(IListManager).delete(mlist) + # Do the MTA-specific list deletion tasks + call_name(config.mta.incoming).delete(mlist) + # 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] + fn_listname, dot, rest = filename.partition('.') if fn_listname == fqdn_listname: removeables.append(os.path.join(config.LOCK_DIR, filename)) # Now that we know what files and directories to delete, delete them. diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py index 7949f83ee..3937d5b5c 100644 --- a/src/mailman/app/subscriptions.py +++ b/src/mailman/app/subscriptions.py @@ -22,7 +22,7 @@ from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ 'SubscriptionService', - 'handle_ListDeletedEvent', + 'handle_ListDeletingEvent', ] @@ -39,7 +39,7 @@ from mailman.core.constants import system_preferences from mailman.database.transaction import dbconnection from mailman.interfaces.address import IEmailValidator from mailman.interfaces.listmanager import ( - IListManager, ListDeletedEvent, NoSuchListError) + IListManager, ListDeletingEvent, NoSuchListError) from mailman.interfaces.member import DeliveryMode, MemberRole from mailman.interfaces.subscriptions import ( ISubscriptionService, MissingUserError) @@ -49,16 +49,12 @@ from mailman.model.member import Member def _membership_sort_key(member): - """Sort function for get_members(). + """Sort function for find_members(). - The members are sorted first by fully-qualified mailing list name, - then by subscribed email address, then by role. + The members are sorted first by unique list id, then by subscribed email + address, then by role. """ - # member.mailing_list is already the fqdn_listname, not the IMailingList - # object. - return (member.mailing_list, - member.address.email, - int(member.role)) + return (member.list_id, member.address.email, int(member.role)) @@ -70,18 +66,18 @@ class SubscriptionService: def get_members(self): """See `ISubscriptionService`.""" - # {fqdn_listname -> {role -> [members]}} + # {list_id -> {role -> [members]}} by_list = {} user_manager = getUtility(IUserManager) for member in user_manager.members: - by_role = by_list.setdefault(member.mailing_list, {}) + by_role = by_list.setdefault(member.list_id, {}) members = by_role.setdefault(member.role.name, []) members.append(member) # Flatten into single list sorted as per the interface. all_members = [] address_of_member = attrgetter('address.email') - for fqdn_listname in sorted(by_list): - by_role = by_list[fqdn_listname] + for list_id in sorted(by_list): + by_role = by_list[list_id] all_members.extend( sorted(by_role.get('owner', []), key=address_of_member)) all_members.extend( @@ -103,14 +99,13 @@ class SubscriptionService: return members[0] @dbconnection - def find_members(self, store, - subscriber=None, fqdn_listname=None, role=None): + def find_members(self, store, subscriber=None, list_id=None, role=None): """See `ISubscriptionService`.""" # If `subscriber` is a user id, then we'll search for all addresses # which are controlled by the user, otherwise we'll just search for # the given address. user_manager = getUtility(IUserManager) - if subscriber is None and fqdn_listname is None and role is None: + if subscriber is None and list_id is None and role is None: return [] # Querying for the subscriber is the most complicated part, because # the parameter can either be an email address or a user id. @@ -136,8 +131,8 @@ class SubscriptionService: Member.address_id.is_in(address_ids))) # Calculate the rest of the query expression, which will get And'd # with the Or clause above (if there is one). - if fqdn_listname is not None: - query.append(Member.mailing_list == fqdn_listname) + if list_id is not None: + query.append(Member.list_id == list_id) if role is not None: query.append(Member.role == role) results = store.find(Member, And(*query)) @@ -147,14 +142,14 @@ class SubscriptionService: for member in self.get_members(): yield member - def join(self, fqdn_listname, subscriber, + def join(self, list_id, subscriber, display_name=None, delivery_mode=DeliveryMode.regular, role=MemberRole.member): """See `ISubscriptionService`.""" - mlist = getUtility(IListManager).get(fqdn_listname) + mlist = getUtility(IListManager).get_by_list_id(list_id) if mlist is None: - raise NoSuchListError(fqdn_listname) + raise NoSuchListError(list_id) # Is the subscriber an email address or user id? if isinstance(subscriber, basestring): # It's an email address, so we'll want a real name. Make sure @@ -181,23 +176,23 @@ class SubscriptionService: raise MissingUserError(subscriber) return mlist.subscribe(user, role) - def leave(self, fqdn_listname, email): + def leave(self, list_id, email): """See `ISubscriptionService`.""" - mlist = getUtility(IListManager).get(fqdn_listname) + mlist = getUtility(IListManager).get_by_list_id(list_id) if mlist is None: - raise NoSuchListError(fqdn_listname) + raise NoSuchListError(list_id) # XXX for now, no notification or user acknowledgment. delete_member(mlist, email, False, False) -def handle_ListDeletedEvent(event): - """Delete a mailing list's members when the list is deleted.""" +def handle_ListDeletingEvent(event): + """Delete a mailing list's members when the list is being deleted.""" - if not isinstance(event, ListDeletedEvent): + if not isinstance(event, ListDeletingEvent): return # Find all the members still associated with the mailing list. members = getUtility(ISubscriptionService).find_members( - fqdn_listname=event.fqdn_listname) + list_id=event.mailing_list.list_id) for member in members: member.unsubscribe() diff --git a/src/mailman/app/tests/test_membership.py b/src/mailman/app/tests/test_membership.py index 626be8b08..00c279910 100644 --- a/src/mailman/app/tests/test_membership.py +++ b/src/mailman/app/tests/test_membership.py @@ -52,7 +52,7 @@ class AddMemberTest(unittest.TestCase): 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language) self.assertEqual(member.address.email, 'aperson@example.com') - self.assertEqual(member.mailing_list, 'test@example.com') + self.assertEqual(member.list_id, 'test.example.com') self.assertEqual(member.role, MemberRole.member) def test_add_member_existing_user(self): @@ -64,7 +64,7 @@ class AddMemberTest(unittest.TestCase): 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language) self.assertEqual(member.address.email, 'aperson@example.com') - self.assertEqual(member.mailing_list, 'test@example.com') + self.assertEqual(member.list_id, 'test.example.com') def test_add_member_banned(self): # Test that members who are banned by specific address cannot @@ -127,7 +127,7 @@ class AddMemberTest(unittest.TestCase): system_preferences.preferred_language, MemberRole.moderator) self.assertEqual(member.address.email, 'aperson@example.com') - self.assertEqual(member.mailing_list, 'test@example.com') + self.assertEqual(member.list_id, 'test.example.com') self.assertEqual(member.role, MemberRole.moderator) def test_add_member_twice(self): @@ -159,7 +159,7 @@ class AddMemberTest(unittest.TestCase): 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language, MemberRole.owner) - self.assertEqual(member_1.mailing_list, member_2.mailing_list) + self.assertEqual(member_1.list_id, member_2.list_id) self.assertEqual(member_1.address, member_2.address) self.assertEqual(member_1.user, member_2.user) self.assertNotEqual(member_1.member_id, member_2.member_id) diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py index a63c9ac04..1c37d4cb9 100644 --- a/src/mailman/app/tests/test_subscriptions.py +++ b/src/mailman/app/tests/test_subscriptions.py @@ -52,7 +52,7 @@ class TestJoin(unittest.TestCase): def test_join_user_with_bogus_id(self): # When `subscriber` is a missing user id, an exception is raised. try: - self._service.join('test@example.com', uuid.UUID(int=99)) + self._service.join('test.example.com', uuid.UUID(int=99)) except MissingUserError as exc: self.assertEqual(exc.user_id, uuid.UUID(int=99)) else: @@ -62,7 +62,7 @@ class TestJoin(unittest.TestCase): # When `subscriber` is a string that is not an email address, an # exception is raised. try: - self._service.join('test@example.com', 'bogus') + self._service.join('test.example.com', 'bogus') except InvalidEmailAddressError as exc: self.assertEqual(exc.email, 'bogus') else: diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py index cf72c51a8..b91f708de 100644 --- a/src/mailman/commands/cli_lists.py +++ b/src/mailman/commands/cli_lists.py @@ -274,4 +274,4 @@ class Remove: return else: log(_('Removed list: $fqdn_listname')) - remove_list(fqdn_listname, mlist) + remove_list(mlist) diff --git a/src/mailman/commands/eml_membership.py b/src/mailman/commands/eml_membership.py index 860e42f47..63efbafca 100644 --- a/src/mailman/commands/eml_membership.py +++ b/src/mailman/commands/eml_membership.py @@ -84,7 +84,7 @@ used. # Is this person already a member of the list? Search for all # matching memberships. members = getUtility(ISubscriptionService).find_members( - address, mlist.fqdn_listname, MemberRole.member) + address, mlist.list_id, MemberRole.member) if len(members) > 0: print(_('$person is already a member'), file=results) else: diff --git a/src/mailman/database/schema/mm_20120407000000.py b/src/mailman/database/schema/mm_20120407000000.py index 068a05834..f6dd60be5 100644 --- a/src/mailman/database/schema/mm_20120407000000.py +++ b/src/mailman/database/schema/mm_20120407000000.py @@ -31,6 +31,12 @@ All column changes are in the `mailinglist` table. - generic_nonmember_action - nntp_host +* Added: + - list_id + +* Changes: + member.mailing_list holds the list_id not the fqdn_listname + See https://bugs.launchpad.net/mailman/+bug/971013 for details. """ @@ -77,29 +83,51 @@ def upgrade_sqlite(database, store, version, module_path): # rename the temporary table to its place. database.load_schema( store, version, 'sqlite_{0}_01.sql'.format(version), module_path) - results = store.execute( - 'SELECT id, include_list_post_header, ' - 'news_prefix_subject_too, news_moderation, ' - 'archive, archive_private FROM mailinglist;') + results = store.execute(""" + SELECT id, include_list_post_header, + news_prefix_subject_too, news_moderation, + archive, archive_private, list_name, mail_host + FROM mailinglist; + """) for value in results: (id, list_post, news_prefix, news_moderation, - archive, archive_private) = value + archive, archive_private, + list_name, mail_host) = value # Figure out what the new archive_policy column value should be. - store.execute( - 'UPDATE ml_backup SET ' - ' allow_list_posts = {0}, ' - ' newsgroup_moderation = {1}, ' - ' nntp_prefix_subject_too = {2}, ' - ' archive_policy = {3} ' - 'WHERE id = {4};'.format( + list_id = '{0}.{1}'.format(list_name, mail_host) + fqdn_listname = '{0}@{1}'.format(list_name, mail_host) + store.execute(""" + UPDATE ml_backup SET + allow_list_posts = {0}, + newsgroup_moderation = {1}, + nntp_prefix_subject_too = {2}, + archive_policy = {3}, + list_id = '{4}' + WHERE id = {5}; + """.format( list_post, news_moderation, news_prefix, archive_policy(archive, archive_private), + list_id, id)) + # Also update the member.mailing_list column to hold the list_id + # instead of the fqdn_listname. + store.execute(""" + UPDATE member SET + mailing_list = '{0}' + WHERE mailing_list = '{1}'; + """.format(list_id, fqdn_listname)) + # Pivot the backup table to the real thing. store.execute('DROP TABLE mailinglist;') store.execute('ALTER TABLE ml_backup RENAME TO mailinglist;') + # Now add some indexes that were previously missing. + store.execute( + 'CREATE INDEX ix_mailinglist_list_id ON mailinglist (list_id);') + store.execute( + 'CREATE INDEX ix_mailinglist_fqdn_listname ' + 'ON mailinglist (list_name, mail_host);') @@ -108,17 +136,20 @@ def upgrade_postgres(database, store, version, module_path): results = store.execute( 'SELECT id, archive, archive_private FROM mailinglist;') # Do the simple renames first. - store.execute( - 'ALTER TABLE mailinglist ' - ' RENAME COLUMN news_prefix_subject_too TO nntp_prefix_subject_too;') - store.execute( - 'ALTER TABLE mailinglist ' - ' RENAME COLUMN news_moderation TO newsgroup_moderation;') - store.execute( - 'ALTER TABLE mailinglist ' - ' RENAME COLUMN include_list_post_header TO allow_list_posts;') + store.execute(""" + ALTER TABLE mailinglist + RENAME COLUMN news_prefix_subject_too TO nntp_prefix_subject_too; + """) + store.execute(""" + ALTER TABLE mailinglist + RENAME COLUMN news_moderation TO newsgroup_moderation; + """) + store.execute(""" + ALTER TABLE mailinglist + RENAME COLUMN include_list_post_header TO allow_list_posts; + """) # Do the easy column drops next. - for column in ('archive_volume_frequency', + for column in ('archive_volume_frequency', 'generic_nonmember_action', 'nntp_host'): store.execute( @@ -130,11 +161,11 @@ def upgrade_postgres(database, store, version, module_path): # archive_policy from the old values. for value in results: id, archive, archive_private = value - store.execute('UPDATE mailinglist SET ' - ' archive_policy = {0} ' - 'WHERE id = {1};'.format( - archive_policy(archive, archive_private), - id)) + store.execute(""" + UPDATE mailinglist SET + archive_policy = {0} + WHERE id = {1}; + """.format(archive_policy(archive, archive_private), id)) # Now drop the old columns. for column in ('archive', 'archive_private'): store.execute( diff --git a/src/mailman/database/schema/sqlite_20120407000000_01.sql b/src/mailman/database/schema/sqlite_20120407000000_01.sql index a4eb0adce..c0e752d68 100644 --- a/src/mailman/database/schema/sqlite_20120407000000_01.sql +++ b/src/mailman/database/schema/sqlite_20120407000000_01.sql @@ -18,6 +18,7 @@ -- THESE COLUMNS ARE ADDED BY THE PYTHON MIGRATION LAYER: -- ADD allow_list_posts -- ADD archive_policy +-- ADD list_id -- ADD newsgroup_moderation -- ADD nntp_prefix_subject_too @@ -242,5 +243,6 @@ INSERT INTO ml_backup SELECT -- Add the new columns. They'll get inserted at the Python layer. ALTER TABLE ml_backup ADD COLUMN archive_policy INTEGER; +ALTER TABLE ml_backup ADD COLUMN list_id TEXT; ALTER TABLE ml_backup ADD COLUMN nntp_prefix_subject_too INTEGER; ALTER TABLE ml_backup ADD COLUMN newsgroup_moderation INTEGER; diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py index c69a8c545..4ad709c16 100644 --- a/src/mailman/database/tests/test_migrations.py +++ b/src/mailman/database/tests/test_migrations.py @@ -54,6 +54,7 @@ class MigrationTestBase(unittest.TestCase): * news_prefix_subject_too -> nntp_prefix_subject_too * include_list_post_header -> allow_list_posts * ADD archive_policy + * ADD list_id * REMOVE archive * REMOVE archive_private * REMOVE archive_volume_frequency @@ -83,6 +84,7 @@ class TestMigration20120407Schema(MigrationTestBase): # Verify that the database has not yet been migrated. for missing in ('allow_list_posts', 'archive_policy', + 'list_id', 'nntp_prefix_subject_too'): self.assertRaises(DatabaseError, self._database.store.execute, @@ -111,6 +113,7 @@ class TestMigration20120407Schema(MigrationTestBase): # Verify that the database has been migrated. for present in ('allow_list_posts', 'archive_policy', + 'list_id', 'nntp_prefix_subject_too'): # This should not produce an exception. Is there some better test # that we can perform? @@ -263,6 +266,13 @@ class TestMigration20120407MigratedData(MigrationTestBase): mlist = getUtility(IListManager).get('test@example.com') self.assertEqual(mlist.archive_policy, ArchivePolicy.public) + def test_list_id(self): + # Test that the mailinglist table gets a list_id column. + self._upgrade() + with temporary_db(self._database): + mlist = getUtility(IListManager).get('test@example.com') + self.assertEqual(mlist.list_id, 'test.example.com') + def test_news_moderation_none(self): # Test that news_moderation becomes newsgroup_moderation. self._database.store.execute( diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index f273b099f..94105c65c 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -14,6 +14,18 @@ Here is a history of user visible changes to Mailman. Architecture ------------ + * The link between members and the mailing lists they are subscribed to, is + now via the RFC 2369 `list_id` instead of the fqdn listname (i.e. posting + address). This is because while the posting address can change if the + mailing list is moved to a new server, the list id is fixed. + (LP: #1024509) + + IListManager.get_by_list_id() added. + + IListManager.list_ids added. + + IMailingList.list_id added. + + Several internal APIs that accepted fqdn list names now require list ids, + e.g. ISubscriptionService.join() and .find_members(). + + IMember.list_id attribute added; .mailing_list is now an alias that + retrieves and returns the IMailingList. * `passlib`_ is now used for all password hashing instead of flufl.password. The default hash is `sha512_crypt`. (LP: #1015758) * Internally, all datetimes are kept in the UTC timezone, however because of @@ -66,6 +78,7 @@ Database - archive and archive_private have been collapsed into archive_policy. - nntp_host has been removed. - generic_nonmember_action has been removed (LP: #975696) + - list_id added (LP: #1024509) * The PostgreSQL port of the schema accidentally added a moderation_callback column to the mailinglist table. Since this is unused in Mailman, it was simply commented out of the base schema for PostgreSQL. diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py index 6f43edf3f..573ba11df 100644 --- a/src/mailman/interfaces/listmanager.py +++ b/src/mailman/interfaces/listmanager.py @@ -113,6 +113,15 @@ class IListManager(Interface): not exist. """ + def get_by_list_id(list_id): + """Return the mailing list with the given list id, if it exists. + + :type fqdn_listname: Unicode. + :param fqdn_listname: The fully qualified name of the mailing list. + :return: the matching `IMailingList` or None if the named list does + not exist. + """ + def delete(mlist): """Remove the mailing list from the database. @@ -134,6 +143,10 @@ class IListManager(Interface): """An iterator over the fully qualified list names of all mailing lists managed by this list manager.""") + list_ids = Attribute( + """An iterator over the list ids of all mailing lists managed by this + list manager.""") + name_components = Attribute( """An iterator over the 2-tuple of (list_name, mail_host) for all mailing lists managed by this list manager.""") diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index c5079bad0..8a4436a21 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -81,6 +81,11 @@ class IMailingList(Interface): mail_host is 'example.com'. """) + list_id = Attribute("""\ + The identity of the mailing list. This value will never change. It + is defined in RFC 2369. + """) + 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 diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py index 52bacc72d..997338835 100644 --- a/src/mailman/interfaces/member.py +++ b/src/mailman/interfaces/member.py @@ -136,8 +136,11 @@ class IMember(Interface): member_id = Attribute( """The member's unique, random identifier as a UUID.""") + list_id = Attribute( + """The list id of the mailing list the member is subscribed to.""") + mailing_list = Attribute( - """The mailing list subscribed to.""") + """The `IMailingList` that the member is subscribed to.""") address = Attribute( """The email address that's subscribed to the list.""") diff --git a/src/mailman/interfaces/subscriptions.py b/src/mailman/interfaces/subscriptions.py index 85f333cf8..cb4900053 100644 --- a/src/mailman/interfaces/subscriptions.py +++ b/src/mailman/interfaces/subscriptions.py @@ -69,7 +69,7 @@ class ISubscriptionService(Interface): :rtype: `IMember` """ - def find_members(subscriber=None, fqdn_listname=None, role=None): + def find_members(subscriber=None, list_id=None, role=None): """Search for and return a specific member. The members are sorted first by fully-qualified mailing list name, @@ -80,9 +80,9 @@ class ISubscriptionService(Interface): :param subscriber: The email address or user id of the user getting subscribed. :type subscriber: string or int - :param fqdn_listname: The posting address of the mailing list to - search for the subscriber's memberships on. - :type fqdn_listname: string + :param list_id: The list id of the mailing list to search for the + subscriber's memberships on. + :type list_id: string :param role: The member role. :type role: `MemberRole` :return: The list of all memberships, which may be empty. @@ -92,8 +92,8 @@ class ISubscriptionService(Interface): def __iter__(): """See `get_members()`.""" - def join(fqdn_listname, subscriber, display_name=None, - delivery_mode=DeliveryMode.regular, + def join(list_id, subscriber, display_name=None, + delivery_mode=DeliveryMode.regular, role=MemberRole.member): """Subscribe to a mailing list. @@ -103,9 +103,9 @@ class ISubscriptionService(Interface): the subscription request is still dependent on the policy of the mailing list. - :param fqdn_listname: The posting address of the mailing list to - subscribe the user to. - :type fqdn_listname: string + :param list_id: The list id of the mailing list the user is + subscribing to. + :type list_id: string :param subscriber: The email address or user id of the user getting subscribed. :type subscriber: string or int @@ -130,12 +130,12 @@ class ISubscriptionService(Interface): :raises ValueError: when `delivery_mode` is invalid. """ - def leave(fqdn_listname, email): + def leave(list_id, email): """Unsubscribe from a mailing list. - :param fqdn_listname: The posting address of the mailing list to - unsubscribe the user from. - :type fqdn_listname: string + :param list_id: The list id of the mailing list the user is + unsubscribing from. + :type list_id: string :param email: The email address of the user getting unsubscribed. :type email: string :raises InvalidEmailAddressError: if the email address is not valid. diff --git a/src/mailman/model/docs/listmanager.rst b/src/mailman/model/docs/listmanager.rst index 9c72b18e7..380fe7704 100644 --- a/src/mailman/model/docs/listmanager.rst +++ b/src/mailman/model/docs/listmanager.rst @@ -16,28 +16,31 @@ Creating a mailing list Creating the list returns the newly created IMailList object. >>> from mailman.interfaces.mailinglist import IMailingList - >>> mlist = list_manager.create('_xtest@example.com') + >>> mlist = list_manager.create('test@example.com') >>> IMailingList.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. +All lists with identities have a short name, a host name, a fully qualified +listname, and an `RFC 2369`_ list id. This latter will not change even if the +mailing list moves to a different host, so it is what uniquely distinguishes +the mailing list to the system. >>> print mlist.list_name - _xtest + test >>> print mlist.mail_host example.com >>> print mlist.fqdn_listname - _xtest@example.com + test@example.com + >>> print mlist.list_id + test.example.com If you try to create a mailing list with the same name as an existing list, you will get an exception. - >>> list_manager.create('_xtest@example.com') + >>> list_manager.create('test@example.com') Traceback (most recent call last): ... - ListAlreadyExistsError: _xtest@example.com + ListAlreadyExistsError: test@example.com It is an error to create a mailing list that isn't a fully qualified list name (i.e. posting address). @@ -59,9 +62,9 @@ Use the list manager to delete a mailing list. After deleting the list, you can create it again. - >>> mlist = list_manager.create('_xtest@example.com') + >>> mlist = list_manager.create('test@example.com') >>> print mlist.fqdn_listname - _xtest@example.com + test@example.com Retrieving a mailing list @@ -70,13 +73,21 @@ 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 = list_manager.get('_xtest@example.com') + >>> mlist_2 = list_manager.get('test@example.com') + >>> mlist_2 is mlist + True + +You can also get a mailing list by it's list id. + + >>> mlist_2 = list_manager.get_by_list_id('test.example.com') >>> mlist_2 is mlist True If you try to get a list that doesn't existing yet, you get ``None``. - >>> print list_manager.get('_xtest_2@example.com') + >>> print list_manager.get('test_2@example.com') + None + >>> print list_manager.get_by_list_id('test_2.example.com') None You also get ``None`` if the list name is invalid. @@ -93,25 +104,34 @@ iterate over the mailing list objects, the list posting addresses, or the list address components. :: - >>> mlist_3 = list_manager.create('_xtest_3@example.com') - >>> mlist_4 = list_manager.create('_xtest_4@example.com') + >>> mlist_3 = list_manager.create('test_3@example.com') + >>> mlist_4 = list_manager.create('test_4@example.com') >>> for name in sorted(list_manager.names): ... print name - _xtest@example.com - _xtest_3@example.com - _xtest_4@example.com + test@example.com + test_3@example.com + test_4@example.com + + >>> for list_id in sorted(list_manager.list_ids): + ... print list_id + test.example.com + test_3.example.com + test_4.example.com >>> for fqdn_listname in sorted(m.fqdn_listname ... for m in list_manager.mailing_lists): ... print fqdn_listname - _xtest@example.com - _xtest_3@example.com - _xtest_4@example.com + test@example.com + test_3@example.com + test_4@example.com >>> for list_name, mail_host in sorted(list_manager.name_components, ... key=lambda (name, host): name): ... print list_name, '@', mail_host - _xtest @ example.com - _xtest_3 @ example.com - _xtest_4 @ example.com + test @ example.com + test_3 @ example.com + test_4 @ example.com + + +.. _`RFC 2369`: http://www.faqs.org/rfcs/rfc2369.html diff --git a/src/mailman/model/docs/mailinglist.rst b/src/mailman/model/docs/mailinglist.rst index 895068e52..21c2f0fd8 100644 --- a/src/mailman/model/docs/mailinglist.rst +++ b/src/mailman/model/docs/mailinglist.rst @@ -5,11 +5,13 @@ Mailing lists .. XXX 2010-06-18 BAW: This documentation needs a lot more detail. The mailing list is a core object in Mailman. It is uniquely identified in -the system by its posting address, i.e. the email address you would send a -message to in order to post a message to the mailing list. This must be fully -qualified. +the system by its *list-id* which is derived from its posting address, +i.e. the email address you would send a message to in order to post a message +to the mailing list. The list id is defined in `RFC 2369`_. >>> mlist = create_list('aardvark@example.com') + >>> print mlist.list_id + aardvark.example.com >>> print mlist.fqdn_listname aardvark@example.com @@ -163,3 +165,6 @@ A user cannot subscribe to a mailing list without a preferred address. ... MissingPreferredAddressError: User must have a preferred address: <User "Elly Person" (2) at ...> + + +.. _`RFC 2369`: http://www.faqs.org/rfcs/rfc2369.html diff --git a/src/mailman/model/docs/membership.rst b/src/mailman/model/docs/membership.rst index f070b4d40..3286bfe6e 100644 --- a/src/mailman/model/docs/membership.rst +++ b/src/mailman/model/docs/membership.rst @@ -283,8 +283,8 @@ though that the address their changing to must be verified. >>> gwen_address = list(gwen.addresses)[0] >>> gwen_member = bee.subscribe(gwen_address) >>> for m in bee.members.members: - ... print m.member_id.int, m.mailing_list, m.address.email - 7 bee@example.com gwen@example.com + ... print m.member_id.int, m.mailing_list.list_id, m.address.email + 7 bee.example.com gwen@example.com Gwen gets a email address. @@ -301,8 +301,8 @@ address, but the address is not yet verified. Her membership has not changed. >>> for m in bee.members.members: - ... print m.member_id.int, m.mailing_list, m.address.email - 7 bee@example.com gwen@example.com + ... print m.member_id.int, m.mailing_list.list_id, m.address.email + 7 bee.example.com gwen@example.com Gwen verifies her email address, and updates her membership. @@ -313,5 +313,5 @@ Gwen verifies her email address, and updates her membership. Now her membership reflects the new address. >>> for m in bee.members.members: - ... print m.member_id.int, m.mailing_list, m.address.email - 7 bee@example.com gperson@example.com + ... print m.member_id.int, m.mailing_list.list_id, m.address.email + 7 bee.example.com gperson@example.com diff --git a/src/mailman/model/docs/usermanager.rst b/src/mailman/model/docs/usermanager.rst index 727f82835..cf7672b27 100644 --- a/src/mailman/model/docs/usermanager.rst +++ b/src/mailman/model/docs/usermanager.rst @@ -173,8 +173,9 @@ There are now four members in the system. Sort them by address then role. ... return (member.address.email, member.role.name) >>> members = sorted(user_manager.members, key=sort_key) >>> for member in members: - ... print member.mailing_list, member.address.email, member.role - test@example.com bperson@example.com MemberRole.member - test@example.com bperson@example.com MemberRole.owner - test@example.com eperson@example.com MemberRole.member - test@example.com fperson@example.com MemberRole.member + ... print member.mailing_list.list_id, member.address.email, \ + ... member.role + test.example.com bperson@example.com MemberRole.member + test.example.com bperson@example.com MemberRole.owner + test.example.com eperson@example.com MemberRole.member + test.example.com fperson@example.com MemberRole.member diff --git a/src/mailman/model/docs/users.rst b/src/mailman/model/docs/users.rst index 95e08a8d7..997f983b2 100644 --- a/src/mailman/model/docs/users.rst +++ b/src/mailman/model/docs/users.rst @@ -335,11 +335,12 @@ membership role. ... return (member.address.email, member.mailing_list, ... int(member.role)) >>> for member in sorted(members, key=sortkey): - ... print member.address.email, member.mailing_list, member.role - zperson@example.com xtest_1@example.com MemberRole.member - zperson@example.net xtest_3@example.com MemberRole.moderator - zperson@example.org xtest_2@example.com MemberRole.member - zperson@example.org xtest_2@example.com MemberRole.owner + ... print member.address.email, member.mailing_list.list_id, \ + ... member.role + zperson@example.com xtest_1.example.com MemberRole.member + zperson@example.net xtest_3.example.com MemberRole.moderator + zperson@example.org xtest_2.example.com MemberRole.member + zperson@example.org xtest_2.example.com MemberRole.owner .. _`usermanager.txt`: usermanager.html diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py index b4bc4b323..ce94047dd 100644 --- a/src/mailman/model/listmanager.py +++ b/src/mailman/model/listmanager.py @@ -48,11 +48,11 @@ class ListManager: listname, at, hostname = fqdn_listname.partition('@') if len(hostname) == 0: raise InvalidEmailAddressError(fqdn_listname) + list_id = '{0}.{1}'.format(listname, hostname) notify(ListCreatingEvent(fqdn_listname)) mlist = store.find( MailingList, - MailingList.list_name == listname, - MailingList.mail_host == hostname).one() + MailingList._list_id == list_id).one() if mlist: raise ListAlreadyExistsError(fqdn_listname) mlist = MailingList(fqdn_listname) @@ -65,9 +65,13 @@ class ListManager: def get(self, store, fqdn_listname): """See `IListManager`.""" listname, at, hostname = fqdn_listname.partition('@') - return store.find(MailingList, - list_name=listname, - mail_host=hostname).one() + list_id = '{0}.{1}'.format(listname, hostname) + return store.find(MailingList, MailingList._list_id == list_id).one() + + @dbconnection + def get_by_list_id(self, store, list_id): + """See `IListManager`.""" + return store.find(MailingList, MailingList._list_id == list_id).one() @dbconnection def delete(self, store, mlist): @@ -101,6 +105,14 @@ class ListManager: @property @dbconnection + def list_ids(self, store): + """See `IListManager`.""" + result_set = store.find(MailingList) + for list_id in result_set.values(MailingList._list_id): + yield list_id + + @property + @dbconnection def name_components(self, store): """See `IListManager`.""" result_set = store.find(MailingList) diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 68d086ec5..2c55540be 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -80,6 +80,7 @@ class MailingList(Model): # List identity list_name = Unicode() mail_host = Unicode() + _list_id = Unicode(name='list_id') allow_list_posts = Bool() include_rfc2369_headers = Bool() advertised = Bool() @@ -199,6 +200,7 @@ class MailingList(Model): assert hostname, 'Bad list name: {0}'.format(fqdn_listname) self.list_name = listname self.mail_host = hostname + self._list_id = '{0}.{1}'.format(listname, hostname) # For the pending database self.next_request_id = 1 # We need to set up the rosters. Normally, this method will get @@ -231,6 +233,11 @@ class MailingList(Model): return '{0}@{1}'.format(self.list_name, self.mail_host) @property + def list_id(self): + """See `IMailingList`.""" + return self._list_id + + @property def domain(self): """See `IMailingList`.""" return getUtility(IDomainManager)[self.mail_host] @@ -463,7 +470,7 @@ class MailingList(Model): member = store.find( Member, Member.role == role, - Member.mailing_list == self.fqdn_listname, + Member.list_id == self._list_id, Member._address == subscriber).one() if member: raise AlreadySubscribedError( @@ -474,7 +481,7 @@ class MailingList(Model): member = store.find( Member, Member.role == role, - Member.mailing_list == self.fqdn_listname, + Member.list_id == self._list_id, Member._user == subscriber).one() if member: raise AlreadySubscribedError( @@ -482,7 +489,7 @@ class MailingList(Model): else: raise ValueError('subscriber must be an address or user') member = Member(role=role, - mailing_list=self.fqdn_listname, + list_id=self._list_id, subscriber=subscriber) member.preferences = Preferences() store.add(member) diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py index b791ea0f2..343c69e66 100644 --- a/src/mailman/model/member.py +++ b/src/mailman/model/member.py @@ -53,7 +53,7 @@ class Member(Model): id = Int(primary=True) _member_id = UUID() role = Enum(MemberRole) - mailing_list = Unicode() + list_id = Unicode(name='mailing_list') moderation_action = Enum(Action) address_id = Int() @@ -63,10 +63,10 @@ class Member(Model): user_id = Int() _user = Reference(user_id, 'User.id') - def __init__(self, role, mailing_list, subscriber): + def __init__(self, role, list_id, subscriber): self._member_id = uid_factory.new_uid() self.role = role - self.mailing_list = mailing_list + self.list_id = list_id if IAddress.providedBy(subscriber): self._address = subscriber # Look this up dynamically. @@ -80,17 +80,23 @@ class Member(Model): if role in (MemberRole.owner, MemberRole.moderator): self.moderation_action = Action.accept elif role is MemberRole.member: - self.moderation_action = getUtility(IListManager).get( - mailing_list).default_member_action + self.moderation_action = getUtility(IListManager).get_by_list_id( + list_id).default_member_action else: assert role is MemberRole.nonmember, ( 'Invalid MemberRole: {0}'.format(role)) - self.moderation_action = getUtility(IListManager).get( - mailing_list).default_nonmember_action + self.moderation_action = getUtility(IListManager).get_by_list_id( + list_id).default_nonmember_action def __repr__(self): return '<Member: {0} on {1} as {2}>'.format( - self.address, self.mailing_list, self.role) + self.address, self.mailing_list.fqdn_listname, self.role) + + @property + def mailing_list(self): + """See `IMember`.""" + list_manager = getUtility(IListManager) + return list_manager.get_by_list_id(self.list_id) @property def member_id(self): diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py index 56dad4bc8..f6f86fbeb 100644 --- a/src/mailman/model/roster.py +++ b/src/mailman/model/roster.py @@ -67,8 +67,8 @@ class AbstractRoster: def _query(self, store): return store.find( Member, - mailing_list=self._mlist.fqdn_listname, - role=self.role) + Member.list_id == self._mlist.list_id, + Member.role == self.role) @property def members(self): @@ -106,7 +106,7 @@ class AbstractRoster: """See `IRoster`.""" results = store.find( Member, - Member.mailing_list == self._mlist.fqdn_listname, + Member.list_id == self._mlist.list_id, Member.role == self.role, Address.email == address, Member.address_id == Address.id) @@ -162,7 +162,7 @@ class AdministratorRoster(AbstractRoster): def _query(self, store): return store.find( Member, - Member.mailing_list == self._mlist.fqdn_listname, + Member.list_id == self._mlist.list_id, Or(Member.role == MemberRole.owner, Member.role == MemberRole.moderator)) @@ -171,7 +171,7 @@ class AdministratorRoster(AbstractRoster): """See `IRoster`.""" results = store.find( Member, - Member.mailing_list == self._mlist.fqdn_listname, + Member.list_id == self._mlist.list_id, Or(Member.role == MemberRole.moderator, Member.role == MemberRole.owner), Address.email == address, @@ -208,7 +208,7 @@ class DeliveryMemberRoster(AbstractRoster): """ results = store.find( Member, - And(Member.mailing_list == self._mlist.fqdn_listname, + And(Member.list_id == self._mlist.list_id, Member.role == MemberRole.member)) for member in results: if member.delivery_mode in delivery_modes: @@ -250,7 +250,7 @@ class Subscribers(AbstractRoster): @dbconnection def _query(self, store): - return store.find(Member, mailing_list=self._mlist.fqdn_listname) + return store.find(Member, Member.list_id == self._mlist.list_id) diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py index e791dcfbd..2e81cb030 100644 --- a/src/mailman/rest/addresses.py +++ b/src/mailman/rest/addresses.py @@ -139,7 +139,7 @@ class UserAddresses(_AddressBase): def membership_key(member): # Sort first by mailing list, then by address, then by role. - return member.mailing_list, member.address.email, int(member.role) + return member.list_id, member.address.email, int(member.role) class AddressMemberships(MemberCollection): diff --git a/src/mailman/rest/docs/addresses.rst b/src/mailman/rest/docs/addresses.rst index a8f875d12..cb9242d2b 100644 --- a/src/mailman/rest/docs/addresses.rst +++ b/src/mailman/rest/docs/addresses.rst @@ -170,16 +170,16 @@ Elle can get her memberships for each of her email addresses. entry 0: address: elle@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/2 entry 1: address: elle@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: "..." + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -207,16 +207,16 @@ does not show up in the list of memberships for his other address. entry 0: address: elle@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/2 entry 1: address: elle@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: "..." + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -229,8 +229,8 @@ does not show up in the list of memberships for his other address. entry 0: address: eperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: "..." + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/3 user: http://localhost:9001/3.0/users/2 diff --git a/src/mailman/rest/docs/membership.rst b/src/mailman/rest/docs/membership.rst index 860d33c21..6189990cb 100644 --- a/src/mailman/rest/docs/membership.rst +++ b/src/mailman/rest/docs/membership.rst @@ -43,8 +43,8 @@ the REST interface. entry 0: address: bperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/1 @@ -57,8 +57,8 @@ Bart's specific membership can be accessed directly: >>> dump_json('http://localhost:9001/3.0/members/1') address: bperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/1 @@ -71,16 +71,16 @@ the REST interface. entry 0: address: bperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/1 entry 1: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -98,24 +98,24 @@ subscribes, she is returned first. entry 0: address: aperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/3 user: http://localhost:9001/3.0/users/3 entry 1: address: bperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/1 entry 2: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -137,40 +137,40 @@ User ids are different than member ids. entry 0: address: aperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/4 user: http://localhost:9001/3.0/users/3 entry 1: address: cperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/5 user: http://localhost:9001/3.0/users/2 entry 2: address: aperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/3 user: http://localhost:9001/3.0/users/3 entry 3: address: bperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/1 entry 4: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -185,16 +185,16 @@ We can also get just the members of a single mailing list. entry 0: address: aperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/4 user: http://localhost:9001/3.0/users/3 entry 1: address: cperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/5 user: http://localhost:9001/3.0/users/2 @@ -212,7 +212,7 @@ mailing list. :: >>> dump_json('http://localhost:9001/3.0/members', { - ... 'fqdn_listname': 'ant@example.com', + ... 'list_id': 'ant.example.com', ... 'subscriber': 'dperson@example.com', ... 'role': 'moderator', ... }) @@ -223,7 +223,7 @@ mailing list. status: 201 >>> dump_json('http://localhost:9001/3.0/members', { - ... 'fqdn_listname': 'bee@example.com', + ... 'list_id': 'bee.example.com', ... 'subscriber': 'cperson@example.com', ... 'role': 'owner', ... }) @@ -237,56 +237,56 @@ mailing list. entry 0: address: dperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: moderator self_link: http://localhost:9001/3.0/members/6 user: http://localhost:9001/3.0/users/4 entry 1: address: aperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/4 user: http://localhost:9001/3.0/users/3 entry 2: address: cperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/5 user: http://localhost:9001/3.0/users/2 entry 3: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: owner self_link: http://localhost:9001/3.0/members/7 user: http://localhost:9001/3.0/users/2 entry 4: address: aperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/3 user: http://localhost:9001/3.0/users/3 entry 5: address: bperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/1 entry 6: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -301,8 +301,8 @@ We can access all the owners of a list. entry 0: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: owner self_link: http://localhost:9001/3.0/members/7 user: http://localhost:9001/3.0/users/2 @@ -320,8 +320,8 @@ A specific member can always be referenced by their role and address. ... 'bee@example.com/owner/cperson@example.com') address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: owner self_link: http://localhost:9001/3.0/members/7 user: http://localhost:9001/3.0/users/2 @@ -335,16 +335,16 @@ example, we can search for all the memberships of a particular address. entry 0: address: aperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/4 user: http://localhost:9001/3.0/users/3 entry 1: address: aperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/3 user: http://localhost:9001/3.0/users/3 @@ -355,37 +355,37 @@ example, we can search for all the memberships of a particular address. Or, we can find all the memberships for a particular mailing list. >>> dump_json('http://localhost:9001/3.0/members/find', { - ... 'fqdn_listname': 'bee@example.com', + ... 'list_id': 'bee.example.com', ... }) entry 0: address: aperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/3 user: http://localhost:9001/3.0/users/3 entry 1: address: bperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/1 user: http://localhost:9001/3.0/users/1 entry 2: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 entry 3: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: owner self_link: http://localhost:9001/3.0/members/7 user: http://localhost:9001/3.0/users/2 @@ -398,21 +398,21 @@ list. >>> dump_json('http://localhost:9001/3.0/members/find', { ... 'subscriber': 'cperson@example.com', - ... 'fqdn_listname': 'bee@example.com', + ... 'list_id': 'bee.example.com', ... }) entry 0: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 entry 1: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: owner self_link: http://localhost:9001/3.0/members/7 user: http://localhost:9001/3.0/users/2 @@ -429,16 +429,16 @@ Or, we can find all the memberships for an address with a specific role. entry 0: address: cperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/5 user: http://localhost:9001/3.0/users/2 entry 1: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -450,14 +450,14 @@ Finally, we can search for a specific member given all three criteria. >>> dump_json('http://localhost:9001/3.0/members/find', { ... 'subscriber': 'cperson@example.com', - ... 'fqdn_listname': 'bee@example.com', + ... 'list_id': 'bee.example.com', ... 'role': 'member', ... }) entry 0: address: cperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: ... + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/2 user: http://localhost:9001/3.0/users/2 @@ -478,7 +478,7 @@ address is not yet known to Mailman, a user is created for her. By default, get gets a regular delivery. >>> dump_json('http://localhost:9001/3.0/members', { - ... 'fqdn_listname': 'ant@example.com', + ... 'list_id': 'ant.example.com', ... 'subscriber': 'eperson@example.com', ... 'display_name': 'Elly Person', ... }) @@ -495,8 +495,8 @@ Elly is now a known user, and a member of the mailing list. >>> elly <User "Elly Person" (...) at ...> - >>> set(member.mailing_list for member in elly.memberships.members) - set([u'ant@example.com']) + >>> set(member.list_id for member in elly.memberships.members) + set([u'ant.example.com']) >>> dump_json('http://localhost:9001/3.0/members') entry 0: @@ -504,8 +504,8 @@ Elly is now a known user, and a member of the mailing list. entry 3: address: eperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: ... + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/8 user: http://localhost:9001/3.0/users/5 @@ -530,7 +530,7 @@ list with her preferred address. >>> transaction.commit() >>> dump_json('http://localhost:9001/3.0/members', { - ... 'fqdn_listname': 'ant@example.com', + ... 'list_id': 'ant.example.com', ... 'subscriber': user_id, ... }) content-length: 0 @@ -545,8 +545,8 @@ list with her preferred address. entry 4: address: gwen@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/9 user: http://localhost:9001/3.0/users/6 @@ -568,8 +568,8 @@ the new address. entry 4: address: gwen.person@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/9 user: http://localhost:9001/3.0/users/6 @@ -606,7 +606,7 @@ Fred joins the `ant` mailing list but wants MIME digest delivery. >>> transaction.abort() >>> dump_json('http://localhost:9001/3.0/members', { - ... 'fqdn_listname': 'ant@example.com', + ... 'list_id': 'ant.example.com', ... 'subscriber': 'fperson@example.com', ... 'display_name': 'Fred Person', ... 'delivery_mode': 'mime_digests', @@ -633,8 +633,8 @@ Fred is getting MIME deliveries. >>> dump_json('http://localhost:9001/3.0/members/10') address: fperson@example.com delivery_mode: mime_digests - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/10 user: http://localhost:9001/3.0/users/7 @@ -655,8 +655,8 @@ This can be done by PATCH'ing his member with the `delivery_mode` parameter. >>> dump_json('http://localhost:9001/3.0/members/10') address: fperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/10 user: http://localhost:9001/3.0/users/7 @@ -673,8 +673,8 @@ If a PATCH request changes no attributes, nothing happens. >>> dump_json('http://localhost:9001/3.0/members/10') address: fperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/10 user: http://localhost:9001/3.0/users/7 @@ -715,8 +715,8 @@ addresses. entry 5: address: herb@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/11 user: http://localhost:9001/3.0/users/8 @@ -724,8 +724,8 @@ addresses. entry 10: address: herb@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: "..." + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/12 user: http://localhost:9001/3.0/users/8 @@ -780,16 +780,16 @@ his membership ids have not changed. entry 0: address: hperson@example.com delivery_mode: regular - fqdn_listname: ant@example.com http_etag: "..." + list_id: ant.example.com role: member self_link: http://localhost:9001/3.0/members/11 user: http://localhost:9001/3.0/users/8 entry 1: address: hperson@example.com delivery_mode: regular - fqdn_listname: bee@example.com http_etag: "..." + list_id: bee.example.com role: member self_link: http://localhost:9001/3.0/members/12 user: http://localhost:9001/3.0/users/8 diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 3374e8f73..f25133211 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -137,7 +137,7 @@ class AList(_ListBase): """Delete the named mailing list.""" if self._mlist is None: return http.not_found() - remove_list(self._mlist.fqdn_listname, self._mlist) + remove_list(self._mlist) return no_content() @resource.child(member_matcher) @@ -146,7 +146,7 @@ class AList(_ListBase): if self._mlist is None: return http.not_found() members = getUtility(ISubscriptionService).find_members( - email, self._mlist.fqdn_listname, role) + email, self._mlist.list_id, role) if len(members) == 0: return http.not_found() assert len(members) == 1, 'Too many matches' diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py index 761e3147c..c6aaa7e39 100644 --- a/src/mailman/rest/members.py +++ b/src/mailman/rest/members.py @@ -61,7 +61,7 @@ class _MemberBase(resource.Resource, CollectionMixin): user_id = member.user.user_id.int member_id = member.member_id.int return dict( - fqdn_listname=member.mailing_list, + list_id=member.list_id, address=member.address.email, role=role, user=path_to('users/{0}'.format(user_id)), @@ -148,7 +148,7 @@ class AMember(_MemberBase): # an admin or user notification. if self._member is None: return http.not_found() - mlist = getUtility(IListManager).get(self._member.mailing_list) + mlist = getUtility(IListManager).get_by_list_id(self._member.list_id) if self._member.role is MemberRole.member: try: delete_member(mlist, self._member.address.email, False, False) @@ -197,7 +197,7 @@ class AllMembers(_MemberBase): service = getUtility(ISubscriptionService) try: validator = Validator( - fqdn_listname=unicode, + list_id=unicode, subscriber=subscriber_validator, display_name=unicode, delivery_mode=enum_validator(DeliveryMode), @@ -248,10 +248,10 @@ class FindMembers(_MemberBase): """Find a member""" service = getUtility(ISubscriptionService) validator = Validator( - fqdn_listname=unicode, + list_id=unicode, subscriber=unicode, role=enum_validator(MemberRole), - _optional=('fqdn_listname', 'subscriber', 'role')) + _optional=('list_id', 'subscriber', 'role')) members = service.find_members(**validator(request)) # We can't just return the _FoundMembers instance, because # CollectionMixins have only a GET method, which is incompatible with diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py index 875f5e254..18469e537 100644 --- a/src/mailman/rest/tests/test_membership.py +++ b/src/mailman/rest/tests/test_membership.py @@ -53,7 +53,7 @@ class TestMembership(unittest.TestCase): try: # For Python 2.6. call_api('http://localhost:9001/3.0/members', { - 'fqdn_listname': 'missing@example.com', + 'list_id': 'missing.example.com', 'subscriber': 'nobody@example.com', }) except HTTPError as exc: @@ -112,7 +112,7 @@ class TestMembership(unittest.TestCase): try: # For Python 2.6. call_api('http://localhost:9001/3.0/members', { - 'fqdn_listname': 'test@example.com', + 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', }) except HTTPError as exc: @@ -124,7 +124,7 @@ class TestMembership(unittest.TestCase): def test_join_with_invalid_delivery_mode(self): try: call_api('http://localhost:9001/3.0/members', { - 'fqdn_listname': 'test@example.com', + 'list_id': 'test.example.com', 'subscriber': 'anne@example.com', 'display_name': 'Anne Person', 'delivery_mode': 'invalid-mode', @@ -138,7 +138,7 @@ class TestMembership(unittest.TestCase): def test_join_email_contains_slash(self): content, response = call_api('http://localhost:9001/3.0/members', { - 'fqdn_listname': 'test@example.com', + 'list_id': 'test.example.com', 'subscriber': 'hugh/person@example.com', 'display_name': 'Hugh Person', }) @@ -168,7 +168,7 @@ class TestMembership(unittest.TestCase): self.assertEqual(entry_0['role'], 'member') self.assertEqual(entry_0['user'], 'http://localhost:9001/3.0/users/1') self.assertEqual(entry_0['address'], 'anne@example.com') - self.assertEqual(entry_0['fqdn_listname'], 'test@example.com') + self.assertEqual(entry_0['list_id'], 'test.example.com') def test_member_changes_preferred_address(self): with transaction(): diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py index 86863726c..cb4da396d 100644 --- a/src/mailman/styles/default.py +++ b/src/mailman/styles/default.py @@ -55,7 +55,6 @@ class DefaultStyle: mlist = mailing_list # List identity. mlist.display_name = mlist.list_name.capitalize() - mlist.list_id = '{0.list_name}.{0.mail_host}'.format(mlist) mlist.include_rfc2369_headers = True mlist.allow_list_posts = True # Most of these were ripped from the old MailList.InitVars() method. diff --git a/src/mailman/utilities/tests/test_import.py b/src/mailman/utilities/tests/test_import.py index 2cc0dafe5..396da4aa8 100644 --- a/src/mailman/utilities/tests/test_import.py +++ b/src/mailman/utilities/tests/test_import.py @@ -45,7 +45,7 @@ class TestBasicImport(unittest.TestCase): self._pckdict = cPickle.load(fp) def tearDown(self): - remove_list(self._mlist.fqdn_listname, self._mlist) + remove_list(self._mlist) def _import(self): import_config_pck(self._mlist, self._pckdict) |
