summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAurélien Bompard2016-02-02 11:49:00 +0100
committerBarry Warsaw2016-02-29 21:52:13 -0500
commit15238cb5683eb9a0eab9dcd251f509a693a22451 (patch)
treeb57d661d1a6f2b1ff4b8c6920d898c36aa016164 /src
parent14dbe7fb4a6b29ce955fa1c8d4c1859c514e8e13 (diff)
downloadmailman-15238cb5683eb9a0eab9dcd251f509a693a22451.tar.gz
mailman-15238cb5683eb9a0eab9dcd251f509a693a22451.tar.zst
mailman-15238cb5683eb9a0eab9dcd251f509a693a22451.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/chains/tests/test_headers.py18
-rw-r--r--src/mailman/config/configure.zcml4
-rw-r--r--src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py40
-rw-r--r--src/mailman/database/tests/test_migrations.py6
-rw-r--r--src/mailman/interfaces/mailinglist.py59
-rw-r--r--src/mailman/model/mailinglist.py129
-rw-r--r--src/mailman/model/tests/test_mailinglist.py176
-rw-r--r--src/mailman/rules/docs/header-matching.rst8
-rw-r--r--src/mailman/utilities/importer.py6
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)