diff options
| author | Aurélien Bompard | 2016-02-02 11:49:00 +0100 |
|---|---|---|
| committer | Barry Warsaw | 2016-02-29 21:52:13 -0500 |
| commit | 15238cb5683eb9a0eab9dcd251f509a693a22451 (patch) | |
| tree | b57d661d1a6f2b1ff4b8c6920d898c36aa016164 /src | |
| parent | 14dbe7fb4a6b29ce955fa1c8d4c1859c514e8e13 (diff) | |
| download | mailman-15238cb5683eb9a0eab9dcd251f509a693a22451.tar.gz mailman-15238cb5683eb9a0eab9dcd251f509a693a22451.tar.zst mailman-15238cb5683eb9a0eab9dcd251f509a693a22451.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/chains/tests/test_headers.py | 18 | ||||
| -rw-r--r-- | src/mailman/config/configure.zcml | 4 | ||||
| -rw-r--r-- | src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py | 40 | ||||
| -rw-r--r-- | src/mailman/database/tests/test_migrations.py | 6 | ||||
| -rw-r--r-- | src/mailman/interfaces/mailinglist.py | 59 | ||||
| -rw-r--r-- | src/mailman/model/mailinglist.py | 129 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_mailinglist.py | 176 | ||||
| -rw-r--r-- | src/mailman/rules/docs/header-matching.rst | 8 | ||||
| -rw-r--r-- | src/mailman/utilities/importer.py | 6 |
9 files changed, 378 insertions, 68 deletions
diff --git a/src/mailman/chains/tests/test_headers.py b/src/mailman/chains/tests/test_headers.py index ff42feb95..d42baa55e 100644 --- a/src/mailman/chains/tests/test_headers.py +++ b/src/mailman/chains/tests/test_headers.py @@ -30,7 +30,7 @@ from mailman.config import config from mailman.core.chains import process from mailman.email.message import Message from mailman.interfaces.chain import LinkAction, HoldEvent -from mailman.interfaces.mailinglist import IHeaderMatchSet +from mailman.interfaces.mailinglist import IHeaderMatchList from mailman.testing.helpers import ( LogFileMark, configuration, event_subscribers, specialized_message_from_string as mfs) @@ -146,8 +146,8 @@ class TestHeaderChain(unittest.TestCase): # Test that the header-match chain has the header checks from the # mailing-list configuration. chain = config.chains['header-match'] - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('Foo', 'a+') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('Foo', 'a+') links = [link for link in chain.get_links(self._mlist, Message(), {}) if link.rule.name != 'any'] self.assertEqual(len(links), 1) @@ -159,10 +159,10 @@ class TestHeaderChain(unittest.TestCase): # Test that the mailing-list header-match complex rules are read # properly. chain = config.chains['header-match'] - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('Foo', 'a+', 'reject') - header_matches.add('Bar', 'b+', 'discard') - header_matches.add('Baz', 'z+', 'accept') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('Foo', 'a+', 'reject') + header_matches.append('Bar', 'b+', 'discard') + header_matches.append('Baz', 'z+', 'accept') links = [link for link in chain.get_links(self._mlist, Message(), {}) if link.rule.name != 'any'] self.assertEqual(len(links), 3) @@ -192,8 +192,8 @@ MIME-Version: 1.0 A message body. """) msgdata = {} - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('Foo', 'foo', 'accept') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('Foo', 'foo', 'accept') # This event subscriber records the event that occurs when the message # is processed by the owner chain. events = [] diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml index b717e125f..535cf729f 100644 --- a/src/mailman/config/configure.zcml +++ b/src/mailman/config/configure.zcml @@ -36,8 +36,8 @@ <adapter for="mailman.interfaces.mailinglist.IMailingList" - provides="mailman.interfaces.mailinglist.IHeaderMatchSet" - factory="mailman.model.mailinglist.HeaderMatchSet" + provides="mailman.interfaces.mailinglist.IHeaderMatchList" + factory="mailman.model.mailinglist.HeaderMatchList" /> <adapter diff --git a/src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py b/src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py new file mode 100644 index 000000000..00064bc1e --- /dev/null +++ b/src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py @@ -0,0 +1,40 @@ +"""Add a numerical index to sort header matches. + +Revision ID: d4fbb4fd34ca +Revises: bfda02ab3a9b +Create Date: 2016-02-01 15:57:09.807678 + +""" + +# revision identifiers, used by Alembic. +revision = 'd4fbb4fd34ca' +down_revision = 'bfda02ab3a9b' + +import sqlalchemy as sa +from alembic import op +from mailman.database.helpers import is_sqlite + + +def upgrade(): + op.add_column( + 'headermatch', sa.Column('index', sa.Integer(), nullable=True)) + if not is_sqlite(op.get_bind()): + op.alter_column( + 'headermatch', 'mailing_list_id', + existing_type=sa.INTEGER(), nullable=False) + op.create_index( + op.f('ix_headermatch_index'), 'headermatch', ['index'], unique=False) + op.create_index( + op.f('ix_headermatch_mailing_list_id'), 'headermatch', + ['mailing_list_id'], unique=False) + + +def downgrade(): + op.drop_index( + op.f('ix_headermatch_mailing_list_id'), table_name='headermatch') + op.drop_index(op.f('ix_headermatch_index'), table_name='headermatch') + if not is_sqlite(op.get_bind()): + op.alter_column( + 'headermatch', 'mailing_list_id', + existing_type=sa.INTEGER(), nullable=True) + op.drop_column('headermatch', 'index') diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py index 89173e26b..c3558abea 100644 --- a/src/mailman/database/tests/test_migrations.py +++ b/src/mailman/database/tests/test_migrations.py @@ -85,7 +85,9 @@ class TestMigrations(unittest.TestCase): sa.sql.column('header', sa.Unicode), sa.sql.column('pattern', sa.Unicode), ) - # Downgrading. + # Bring the DB to the revision that is being tested. + alembic.command.downgrade(alembic_cfg, '42756496720') + # Test downgrading. config.db.store.execute(mlist_table.insert().values(id=1)) config.db.store.execute(header_match_table.insert().values( [{'mailing_list_id': 1, 'header': hm[0], 'pattern': hm[1]} @@ -97,7 +99,7 @@ class TestMigrations(unittest.TestCase): self.assertEqual(results[0].header_matches, test_header_matches) self.assertFalse(exists_in_db(config.db.engine, 'headermatch')) config.db.store.commit() - # Upgrading. + # Test upgrading. alembic.command.upgrade(alembic_cfg, '42756496720') results = config.db.store.execute( header_match_table.select()).fetchall() diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index 25690945e..533bf89a7 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -21,6 +21,7 @@ __all__ = [ 'IAcceptableAlias', 'IAcceptableAliasSet', 'IHeaderMatch', + 'IHeaderMatchList', 'IListArchiver', 'IListArchiverSet', 'IMailingList', @@ -873,14 +874,14 @@ class IHeaderMatch(Interface): """) -class IHeaderMatchSet(Interface): - """The set of header matching rules for a mailing list.""" +class IHeaderMatchList(Interface): + """The list of header matching rules for a mailing list.""" def clear(): - """Clear the set of header matching rules.""" + """Clear the list of header matching rules.""" - def add(header, pattern, chain=None): - """Add the given header matching rule to this mailing list's set. + def append(header, pattern, chain=None): + """Append the given rule to this mailing list's header match list. :param header: The email header to filter on. It will be converted to lower case for consistency. @@ -894,8 +895,26 @@ class IHeaderMatchSet(Interface): mailing list. """ + def insert(index, header, pattern, chain=None): + """Insert the given rule at the given index position in this mailing + list's header match list. + + :param index: The index to insert the rule at. + :type index: integer + :param header: The email header to filter on. It will be converted to + lower case for consistency. + :type header: string + :param pattern: The regular expression to use. + :type pattern: string + :param chain: The chain to jump to, or None to use the site-wide + configuration. Defaults to None. + :type chain: string or None + :raises ValueError: if the header/pattern pair already exists for this + mailing list. + """ + def remove(header, pattern): - """Remove the given header matching rule from this mailing list's set. + """Remove the given rule from this mailing list's header match list. :param header: The email header part of the rule to be removed. :type header: string @@ -903,8 +922,34 @@ class IHeaderMatchSet(Interface): :type pattern: string """ + def __getitem__(key): + """Return the header match at the given index for this mailing list. + + :param key: The index of the header match to return. + :type key: integer + :raises IndexError: if there is no header match at this index for + this mailing list. + :rtype: `IHeaderMatch`. + """ + + def __delitem__(key): + """Remove the rule at the given index from this mailing list's header + match list. + + :param key: The index of the header match to remove. + :type key: integer + :raises IndexError: if there is no header match at this index for + this mailing list. + """ + + def __len__(): + """Return the number of header matches for this mailing list. + + :rtype: integer + """ + def __iter__(): - """An iterator over all the IHeaderMatches defined in this set. + """An iterator over all the IHeaderMatches defined in this list. :return: iterator over `IHeaderMatch`. """ diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 4160f6cc7..29f4ce26d 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -37,7 +37,7 @@ from mailman.interfaces.digests import DigestFrequency from mailman.interfaces.domain import IDomainManager from mailman.interfaces.languages import ILanguageManager from mailman.interfaces.mailinglist import ( - IAcceptableAlias, IAcceptableAliasSet, IHeaderMatch, IHeaderMatchSet, + IAcceptableAlias, IAcceptableAliasSet, IHeaderMatch, IHeaderMatchList, IListArchiver, IListArchiverSet, IMailingList, Personalization, ReplyToMunging, SubscriptionPolicy) from mailman.interfaces.member import ( @@ -188,6 +188,10 @@ class MailingList(Model): topics_bodylines_limit = Column(Integer) topics_enabled = Column(Boolean) welcome_message_uri = Column(Unicode) + # ORM relationships + header_matches = relationship( + 'HeaderMatch', backref='mailing_list', cascade="all, delete-orphan", + order_by="HeaderMatch.index") def __init__(self, fqdn_listname): super().__init__() @@ -631,30 +635,62 @@ class HeaderMatch(Model): id = Column(Integer, primary_key=True) - mailing_list_id = Column(Integer, ForeignKey('mailinglist.id')) - mailing_list = relationship('MailingList', backref='header_matches') + mailing_list_id = Column( + Integer, ForeignKey('mailinglist.id'), + index=True, nullable=False) + index = Column(Integer, index=True, default=0) header = Column(Unicode) pattern = Column(Unicode) chain = Column(Unicode, nullable=True) + @dbconnection + def move_to(self, store, index): + if index == self.index: + return # Nothing to do + elif index < self.index: + # Moving up: header matches between the new position and the + # current one must be moved down the list to make room. Those after + # the current position must not be changed. + for header_match in store.query(HeaderMatch).filter( + HeaderMatch.mailing_list == self.mailing_list, + HeaderMatch.index >= index, + HeaderMatch.index < self.index): + header_match.index = header_match.index + 1 + elif index > self.index: + # Moving down: header matches between the current position and the + # new one must be moved up the list to make room. Those after + # the new position must not be changed. + for header_match in store.query(HeaderMatch).filter( + HeaderMatch.mailing_list == self.mailing_list, + HeaderMatch.index > self.index, + HeaderMatch.index <= index): + header_match.index = header_match.index - 1 + self.index = index + -@implementer(IHeaderMatchSet) -class HeaderMatchSet: - """See `IHeaderMatchSet`.""" +@implementer(IHeaderMatchList) +class HeaderMatchList: + """See `IHeaderMatchList`. + + All write operations must mark the mailing list's header_matches collection + as expired: + http://docs.sqlalchemy.org/en/latest/orm/session_state_management.html#refreshing-expiring + """ def __init__(self, mailing_list): self._mailing_list = mailing_list @dbconnection def clear(self, store): - """See `IHeaderMatchSet`.""" + """See `IHeaderMatchList`.""" store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self._mailing_list).delete() + store.expire(self._mailing_list, ['header_matches']) @dbconnection - def add(self, store, header, pattern, chain=None): + def append(self, store, header, pattern, chain=None): header = header.lower() existing = store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self._mailing_list, @@ -662,17 +698,35 @@ class HeaderMatchSet: HeaderMatch.pattern == pattern).count() if existing > 0: raise ValueError('Pattern already exists') + last_index = store.query(HeaderMatch.index).filter( + HeaderMatch.mailing_list == self._mailing_list + ).order_by(HeaderMatch.index.desc()).limit(1).scalar() + if last_index is None: + last_index = -1 header_match = HeaderMatch( mailing_list=self._mailing_list, - header=header, pattern=pattern, chain=chain) + header=header, pattern=pattern, chain=chain, + index=last_index + 1) store.add(header_match) + store.expire(self._mailing_list, ['header_matches']) + + @dbconnection + def insert(self, store, index, header, pattern, chain=None): + self.append(header, pattern, chain) + # Get the header match that was just added. + header_match = store.query(HeaderMatch).filter( + HeaderMatch.mailing_list == self._mailing_list, + HeaderMatch.header == header.lower(), + HeaderMatch.pattern == pattern, + HeaderMatch.chain == chain).one() + header_match.move_to(index) + store.expire(self._mailing_list, ['header_matches']) @dbconnection def remove(self, store, header, pattern): header = header.lower() - # Don't just filter and use delete(), or the MailingList.header_matches - # collection will not be updated: - # http://docs.sqlalchemy.org/en/rel_1_0/orm/collections.html#dynamic-relationship-loaders + # Query.delete() has many caveats, don't use it here: + # http://docs.sqlalchemy.org/en/rel_1_0/orm/query.html#sqlalchemy.orm.query.Query.delete try: existing = store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self._mailing_list, @@ -681,9 +735,56 @@ class HeaderMatchSet: except NoResultFound: raise ValueError('Pattern does not exist') else: - self._mailing_list.header_matches.remove(existing) + store.delete(existing) + self._restore_index_sequence() + store.expire(self._mailing_list, ['header_matches']) + + @dbconnection + def __getitem__(self, store, key): + if key < 0: + key = len(self) + key + try: + return store.query(HeaderMatch).filter( + HeaderMatch.mailing_list == self._mailing_list, + HeaderMatch.index == key).one() + except NoResultFound: + raise IndexError + + @dbconnection + def __delitem__(self, store, key): + try: + existing = store.query(HeaderMatch).filter( + HeaderMatch.mailing_list == self._mailing_list, + HeaderMatch.index == key).one() + except NoResultFound: + raise IndexError + else: + store.delete(existing) + self._restore_index_sequence() + store.expire(self._mailing_list, ['header_matches']) + + @dbconnection + def __len__(self, store): + return store.query(HeaderMatch).filter( + HeaderMatch.mailing_list == self._mailing_list).count() @dbconnection def __iter__(self, store): yield from store.query(HeaderMatch).filter( - HeaderMatch.mailing_list == self._mailing_list) + HeaderMatch.mailing_list == self._mailing_list + ).order_by(HeaderMatch.index) + + @dbconnection + def _restore_index_sequence(self, store): + """Restore a continuous index sequence for this mailing list's header + matches. + + The header match indexes may not be continuous after deleting an item. + It won't prevent this component from working properly, but it's cleaner + to restore a continuous sequence. + """ + for index, header_match in enumerate(store.query(HeaderMatch).filter( + HeaderMatch.mailing_list == self._mailing_list + ).order_by(HeaderMatch.index)): + header_match.index = index + store.expire(self._mailing_list, ['header_matches']) diff --git a/src/mailman/model/tests/test_mailinglist.py b/src/mailman/model/tests/test_mailinglist.py index 4779382b6..ee8a724d5 100644 --- a/src/mailman/model/tests/test_mailinglist.py +++ b/src/mailman/model/tests/test_mailinglist.py @@ -32,7 +32,7 @@ from mailman.config import config from mailman.database.transaction import transaction from mailman.interfaces.listmanager import IListManager from mailman.interfaces.mailinglist import ( - IAcceptableAliasSet, IHeaderMatchSet, IListArchiverSet) + IAcceptableAliasSet, IHeaderMatchList, IListArchiverSet) from mailman.interfaces.member import ( AlreadySubscribedError, MemberRole, MissingPreferredAddressError) from mailman.interfaces.usermanager import IUserManager @@ -200,56 +200,178 @@ class TestHeaderMatch(unittest.TestCase): self._mlist = create_list('ant@example.com') def test_lowercase_header(self): - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('Header', 'pattern') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('Header', 'pattern') self.assertEqual(len(self._mlist.header_matches), 1) self.assertEqual(self._mlist.header_matches[0].header, 'header') def test_chain_defaults_to_none(self): - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('header', 'pattern') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header', 'pattern') self.assertEqual(len(self._mlist.header_matches), 1) self.assertEqual(self._mlist.header_matches[0].chain, None) def test_duplicate(self): - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('Header', 'pattern') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('Header', 'pattern') self.assertRaises( - ValueError, header_matches.add, 'Header', 'pattern') + ValueError, header_matches.append, 'Header', 'pattern') self.assertEqual(len(self._mlist.header_matches), 1) def test_remove_non_existent(self): - header_matches = IHeaderMatchSet(self._mlist) + header_matches = IHeaderMatchList(self._mlist) self.assertRaises( ValueError, header_matches.remove, 'header', 'pattern') def test_add_remove(self): - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('header', 'pattern') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header1', 'pattern') + header_matches.append('header2', 'pattern') + self.assertEqual(len(self._mlist.header_matches), 2) + self.assertEqual(len(header_matches), 2) + header_matches.remove('header1', 'pattern') self.assertEqual(len(self._mlist.header_matches), 1) - header_matches.remove('header', 'pattern') + self.assertEqual(len(header_matches), 1) + del header_matches[0] self.assertEqual(len(self._mlist.header_matches), 0) + self.assertEqual(len(header_matches), 0) def test_iterator(self): - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('Header', 'pattern') - header_matches.add('Subject', 'patt.*') - header_matches.add('From', '.*@example.com', 'discard') - header_matches.add('From', '.*@example.org', 'accept') - matches = sorted((match.header, match.pattern, match.chain) - for match in IHeaderMatchSet(self._mlist)) + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('Header', 'pattern') + header_matches.append('Subject', 'patt.*') + header_matches.append('From', '.*@example.com', 'discard') + header_matches.append('From', '.*@example.org', 'accept') + matches = [(match.header, match.pattern, match.chain) + for match in IHeaderMatchList(self._mlist)] self.assertEqual( - matches, - [('from', '.*@example.com', 'discard'), - ('from', '.*@example.org', 'accept'), - ('header', 'pattern', None), - ('subject', 'patt.*', None), - ]) + matches, [ + ('header', 'pattern', None), + ('subject', 'patt.*', None), + ('from', '.*@example.com', 'discard'), + ('from', '.*@example.org', 'accept'), + ]) def test_clear(self): - header_matches = IHeaderMatchSet(self._mlist) - header_matches.add('Header', 'pattern') + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('Header', 'pattern') self.assertEqual(len(self._mlist.header_matches), 1) with transaction(): header_matches.clear() self.assertEqual(len(self._mlist.header_matches), 0) + + def test_get_by_index(self): + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header', 'pattern') + hm = header_matches[0] + self.assertEqual(hm.header, 'header') + self.assertEqual(hm.pattern, 'pattern') + + def test_get_by_negative_index(self): + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header', 'pattern') + hm = header_matches[-1] + self.assertEqual(hm.header, 'header') + self.assertEqual(hm.pattern, 'pattern') + + def test_get_non_existent_by_index(self): + header_matches = IHeaderMatchList(self._mlist) + with self.assertRaises(IndexError): + header_matches[0] + + def test_move_up(self): + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header-0', 'pattern') + header_matches.append('header-1', 'pattern') + header_matches.append('header-2', 'pattern') + header_matches.append('header-3', 'pattern') + self.assertEqual( + [(match.header, match.index) for match in header_matches], [ + ('header-0', 0), + ('header-1', 1), + ('header-2', 2), + ('header-3', 3), + ]) + header_match_2 = self._mlist.header_matches[2] + self.assertEqual(header_match_2.index, 2) + header_match_2.move_to(1) + self.assertEqual( + [(match.header, match.index) for match in header_matches], [ + ('header-0', 0), + ('header-2', 1), + ('header-1', 2), + ('header-3', 3), + ]) + + def test_move_down(self): + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header-0', 'pattern') + header_matches.append('header-1', 'pattern') + header_matches.append('header-2', 'pattern') + header_matches.append('header-3', 'pattern') + self.assertEqual( + [(match.header, match.index) for match in header_matches], [ + ('header-0', 0), + ('header-1', 1), + ('header-2', 2), + ('header-3', 3), + ]) + header_match_1 = self._mlist.header_matches[1] + self.assertEqual(header_match_1.index, 1) + header_match_1.move_to(2) + self.assertEqual( + [(match.header, match.index) for match in header_matches], [ + ('header-0', 0), + ('header-2', 1), + ('header-1', 2), + ('header-3', 3), + ]) + + def test_move_identical(self): + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header-0', 'pattern') + header_matches.append('header-1', 'pattern') + header_matches.append('header-2', 'pattern') + self.assertEqual( + [(match.header, match.index) for match in header_matches], + [('header-0', 0), ('header-1', 1), ('header-2', 2)]) + header_match_1 = self._mlist.header_matches[1] + self.assertEqual(header_match_1.index, 1) + header_match_1.move_to(1) + self.assertEqual( + [(match.header, match.index) for match in header_matches], + [('header-0', 0), ('header-1', 1), ('header-2', 2)]) + + def test_insert(self): + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header-0', 'pattern') + header_matches.append('header-1', 'pattern') + self.assertEqual( + [(match.header, match.index) for match in header_matches], + [('header-0', 0), ('header-1', 1)]) + header_matches.insert(1, 'header-2', 'pattern') + self.assertEqual( + [(match.header, match.index) for match in header_matches], + [('header-0', 0), ('header-2', 1), ('header-1', 2)]) + + def test_rebuild_sequence_after_remove(self): + header_matches = IHeaderMatchList(self._mlist) + header_matches.append('header-0', 'pattern') + header_matches.append('header-1', 'pattern') + header_matches.append('header-2', 'pattern') + self.assertEqual( + [(match.header, match.index) for match in header_matches], + [('header-0', 0), ('header-1', 1), ('header-2', 2)]) + del header_matches[0] + self.assertEqual( + [(match.header, match.index) for match in header_matches], + [('header-1', 0), ('header-2', 1)]) + header_matches.remove('header-1', 'pattern') + self.assertEqual( + [(match.header, match.index) for match in header_matches], + [('header-2', 0)]) + + def test_remove_non_existent_by_index(self): + header_matches = IHeaderMatchList(self._mlist) + with self.assertRaises(IndexError): + del header_matches[0] diff --git a/src/mailman/rules/docs/header-matching.rst b/src/mailman/rules/docs/header-matching.rst index 6618d9cc9..4e6c0853d 100644 --- a/src/mailman/rules/docs/header-matching.rst +++ b/src/mailman/rules/docs/header-matching.rst @@ -131,9 +131,9 @@ action. The list administrator wants to match not on four stars, but on three plus signs, but only for the current mailing list. - >>> from mailman.interfaces.mailinglist import IHeaderMatchSet - >>> header_matches = IHeaderMatchSet(mlist) - >>> header_matches.add('x-spam-score', '[+]{3,}') + >>> from mailman.interfaces.mailinglist import IHeaderMatchList + >>> header_matches = IHeaderMatchList(mlist) + >>> header_matches.append('x-spam-score', '[+]{3,}') A message with a spam score of two pluses does not match. @@ -178,7 +178,7 @@ Now, the list administrator wants to match on three plus signs, but wants those emails to be discarded instead of held. >>> header_matches.remove('x-spam-score', '[+]{3,}') - >>> header_matches.add('x-spam-score', '[+]{3,}', 'discard') + >>> header_matches.append('x-spam-score', '[+]{3,}', 'discard') A message with a spam score of three pluses will still match, and the message will be discarded. diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py index 59f4255b2..52de967cf 100644 --- a/src/mailman/utilities/importer.py +++ b/src/mailman/utilities/importer.py @@ -41,7 +41,7 @@ from mailman.interfaces.bans import IBanManager from mailman.interfaces.bounce import UnrecognizedBounceDisposition from mailman.interfaces.digests import DigestFrequency from mailman.interfaces.languages import ILanguageManager -from mailman.interfaces.mailinglist import IAcceptableAliasSet, IHeaderMatchSet +from mailman.interfaces.mailinglist import IAcceptableAliasSet, IHeaderMatchList from mailman.interfaces.mailinglist import Personalization, ReplyToMunging from mailman.interfaces.mailinglist import SubscriptionPolicy from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole @@ -333,7 +333,7 @@ def import_config_pck(mlist, config_dict): # expression. Make that explicit for MM3. alias_set.add('^' + address) # Handle header_filter_rules conversion to header_matches. - header_match_set = IHeaderMatchSet(mlist) + header_matches = IHeaderMatchList(mlist) header_filter_rules = config_dict.get('header_filter_rules', []) for line_patterns, action, _unused in header_filter_rules: try: @@ -374,7 +374,7 @@ def import_config_pck(mlist, config_dict): 'invalid regular expression: %r', line_pattern) continue try: - header_match_set.add(header, pattern, chain) + header_matches.append(header, pattern, chain) except ValueError: log.warning('Skipping duplicate header_filter rule: %r', line_pattern) |
