From d43508dc2bb456070192239d02fb8079b433781c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toke=20H=C3=B8iland-J=C3=B8rgensen?= Date: Thu, 24 Aug 2023 22:03:01 +0200 Subject: [PATCH 1/2] HeaderMatchingFilter: Gracefully handle missing headers Notmuch can raise a NullPointerException when trying to read a non-existent header from a message. Gracefully handle this in the HeaderMatchingFilter by just aborting processing when encountering such an error. --- afew/filters/HeaderMatchingFilter.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/afew/filters/HeaderMatchingFilter.py b/afew/filters/HeaderMatchingFilter.py index 8ec2c09..4cbcf86 100644 --- a/afew/filters/HeaderMatchingFilter.py +++ b/afew/filters/HeaderMatchingFilter.py @@ -6,6 +6,8 @@ from afew.filters.BaseFilter import Filter +from notmuch.errors import NullPointerError + import re @@ -22,10 +24,13 @@ def __init__(self, database, **kwargs): def handle_message(self, message): if self.header is not None and self.pattern is not None: if not self._tag_blacklist.intersection(message.get_tags()): - value = message.get_header(self.header) - match = self.pattern.search(value) - if match: - tagdict = {k: v.lower() for k, v in match.groupdict().items()} - sub = (lambda tag: tag.format(**tagdict)) - self.remove_tags(message, *map(sub, self._tags_to_remove)) - self.add_tags(message, *map(sub, self._tags_to_add)) + try: + value = message.get_header(self.header) + match = self.pattern.search(value) + if match: + tagdict = {k: v.lower() for k, v in match.groupdict().items()} + sub = (lambda tag: tag.format(**tagdict)) + self.remove_tags(message, *map(sub, self._tags_to_remove)) + self.add_tags(message, *map(sub, self._tags_to_add)) + except NullPointerError: + pass From 38797f63e5f23900a22a21d12d9efc076530f2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toke=20H=C3=B8iland-J=C3=B8rgensen?= Date: Wed, 27 Sep 2023 14:57:06 +0200 Subject: [PATCH 2/2] afew/tests: Add a test for HeaderMatchingFilter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Toke Høiland-Jørgensen --- afew/tests/test_headermatchingfilter.py | 84 +++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 afew/tests/test_headermatchingfilter.py diff --git a/afew/tests/test_headermatchingfilter.py b/afew/tests/test_headermatchingfilter.py new file mode 100644 index 0000000..8b09a67 --- /dev/null +++ b/afew/tests/test_headermatchingfilter.py @@ -0,0 +1,84 @@ +"""Test suite for DKIMValidityFilter. +""" +import unittest +from email.utils import make_msgid +from unittest import mock + +from afew.Database import Database +from afew.filters.HeaderMatchingFilter import HeaderMatchingFilter + +from notmuch.errors import NullPointerError + + +class _AddTags: # pylint: disable=too-few-public-methods + """Mock for `add_tags` method of base filter. We need to easily collect + tags added by filter for test assertion. + """ + def __init__(self, tags): + self._tags = tags + + def __call__(self, message, *tags): + self._tags.update(tags) + + +def _make_header_matching_filter(): + """Make `HeaderMatchingFilter` with mocked `HeaderMatchingFilter.add_tags` + method, so in tests we can easily check what tags were added by filter + without fiddling with db. + """ + tags = set() + add_tags = _AddTags(tags) + header_filter = HeaderMatchingFilter(Database(), header="X-test", pattern="") + header_filter.add_tags = add_tags + return header_filter, tags + + +def _make_message(should_fail): + """Make mock email Message. + + Mocked methods: + + - `get_header()` returns non-empty string. When testing with mocked + function for verifying DKIM signature, DKIM signature doesn't matter as + long as it's non-empty string. + + - `get_filenames()` returns list of non-empty string. When testing with + mocked file open, it must just be non-empty string. + + - `get_message_id()` returns some generated message ID. + """ + message = mock.Mock() + if should_fail: + message.get_header.side_effect = NullPointerError + else: + message.get_header.return_value = 'header' + message.get_filenames.return_value = ['a'] + message.get_tags.return_value = ['a'] + message.get_message_id.return_value = make_msgid() + return message + + +class TestHeaderMatchingFilter(unittest.TestCase): + """Test suite for `HeaderMatchingFilter`. + """ + @mock.patch('afew.filters.HeaderMatchingFilter.open', + mock.mock_open(read_data=b'')) + def test_header_exists(self): + """Test message with header that exists. + """ + header_filter, tags = _make_header_matching_filter() + message = _make_message(False) + header_filter.handle_message(message) + + self.assertSetEqual(tags, set()) + + @mock.patch('afew.filters.HeaderMatchingFilter.open', + mock.mock_open(read_data=b'')) + def test_header_doesnt_exist(self): + """Test message with header that exists. + """ + header_filter, tags = _make_header_matching_filter() + message = _make_message(True) + header_filter.handle_message(message) + + self.assertSetEqual(tags, set())