Control: tags 1098664 + patch

Dear maintainer,

I've prepared an NMU for afew (versioned as 3.0.1.post63-0.1) and
uploaded it to DELAYED/5. Please feel free to tell me if I
should delay it longer.

Regards.

David
diff -Nru afew-3.0.1/afew/configparser.py afew-3.0.1.post63/afew/configparser.py
--- afew-3.0.1/afew/configparser.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/configparser.py	2025-03-15 10:46:03.000000000 -0300
@@ -17,7 +17,9 @@
             return filter(None, result)
 
 
-class ConfigParser(configparser.ConfigParser, GetListMixIn): pass
+class ConfigParser(configparser.ConfigParser, GetListMixIn):
+    pass
 
 
-class RawConfigParser(configparser.RawConfigParser, GetListMixIn): pass
+class RawConfigParser(configparser.RawConfigParser, GetListMixIn):
+    pass
diff -Nru afew-3.0.1/afew/Database.py afew-3.0.1.post63/afew/Database.py
--- afew-3.0.1/afew/Database.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/Database.py	2025-03-15 10:46:03.000000000 -0300
@@ -97,7 +97,7 @@
 
     def get_messages(self, query, full_thread=False):
         """
-        Get all messages mathing the given query.
+        Get all messages matching the given query.
 
         :param query: the query to execute using :func:`Database.do_query`
         :type  query: str
@@ -126,7 +126,7 @@
         # TODO: bindings are *very* unpythonic here... iterator *or* None
         #       is a nono
         replies = message.get_replies()
-        if replies != None:
+        if replies is not None:
             for message in replies:
                 # TODO: yield from
                 for message in self.walk_replies(message):
diff -Nru afew-3.0.1/afew/files.py afew-3.0.1.post63/afew/files.py
--- afew-3.0.1/afew/files.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/files.py	2025-03-15 10:46:03.000000000 -0300
@@ -8,13 +8,14 @@
 import platform
 import queue
 import threading
+import notmuch
+import pyinotify
+import ctypes
+import contextlib
 
 if platform.system() != 'Linux':
     raise ImportError('Unsupported platform: {!r}'.format(platform.system()))
 
-import notmuch
-import pyinotify
-
 
 class EventHandler(pyinotify.ProcessEvent):
     def __init__(self, options, database):
@@ -22,7 +23,7 @@
         self.database = database
         super().__init__()
 
-    ignore_re = re.compile('(/xapian/.*(base.|tmp)$)|(\.lock$)|(/dovecot)')
+    ignore_re = re.compile(r'(/xapian/.*(base.|tmp)$)|(\.lock$)|(/dovecot)')
 
     def process_IN_DELETE(self, event):
         if self.ignore_re.search(event.pathname):
@@ -45,17 +46,17 @@
                     filter_.run('id:"{}"'.format(message.get_message_id()))
                     filter_.commit(self.options.dry_run)
                 except Exception as e:
-                    logging.warn('Error processing mail with filter {!r}: {}'.format(filter_.message, e))
+                    logging.warning('Error processing mail with filter {!r}: {}'.format(filter_.message, e))
 
         try:
             self.database.add_message(event.pathname,
                                       sync_maildir_flags=True,
                                       new_mail_handler=new_mail)
         except notmuch.FileError as e:
-            logging.warn('Error opening mail file: {}'.format(e))
+            logging.warning('Error opening mail file: {}'.format(e))
             return
         except notmuch.FileNotEmailError as e:
-            logging.warn('File does not look like an email: {}'.format(e))
+            logging.warning('File does not look like an email: {}'.format(e))
             return
         else:
             if src_pathname:
@@ -67,9 +68,9 @@
 def watch_for_new_files(options, database, paths, daemonize=False):
     wm = pyinotify.WatchManager()
     mask = (
-            pyinotify.IN_DELETE |
-            pyinotify.IN_MOVED_FROM |
-            pyinotify.IN_MOVED_TO)
+        pyinotify.IN_DELETE |
+        pyinotify.IN_MOVED_FROM |
+        pyinotify.IN_MOVED_TO)
     handler = EventHandler(options, database)
     notifier = pyinotify.Notifier(wm, handler)
 
@@ -83,9 +84,6 @@
     notifier.loop()
 
 
-import ctypes
-import contextlib
-
 try:
     libc = ctypes.CDLL(ctypes.util.find_library("c"))
 except ImportError as e:
@@ -171,7 +169,7 @@
                        if child not in blacklist):
         try:
             stat_result = os.stat(child_path)
-        except:
+        except Exception:
             continue
 
         if stat_result.st_mode & stat.S_IFDIR:
@@ -193,7 +191,7 @@
     while True:
         result = results.get()
 
-        if result != None:
+        if result is not None:
             yield result
         else:
             break
diff -Nru afew-3.0.1/afew/filters/DMARCReportInspectionFilter.py afew-3.0.1.post63/afew/filters/DMARCReportInspectionFilter.py
--- afew-3.0.1/afew/filters/DMARCReportInspectionFilter.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/filters/DMARCReportInspectionFilter.py	2025-03-15 10:46:03.000000000 -0300
@@ -81,6 +81,8 @@
     :param node: XML node holding status as text.
     :returns: Whether the status is reported as "failed".
     """
+    if not node or not node.text:
+        return True
     return (node.text.strip() not in ['pass', 'none'])
 
 
@@ -118,17 +120,28 @@
 class DMARCReportInspectionFilter(Filter):
     """
     Inspect DMARC reports for DKIM and SPF status.
+
+    Config:
+
+    [DMARCReportInspectionFilter]
+    dkim_ok_tag = "dmarc/dkim-ok"
+    dkim_fail_tag = "dmarc/dkim-fail"
+    spf_ok_tag = "dmarc/spf-ok"
+    spf_fail_tag = "dmarc/spf-fail"
+    subject_regexp = "^report domain:"
+
     """
     def __init__(self,                     # pylint: disable=too-many-arguments
                  database,
                  dkim_ok_tag='dmarc/dkim-ok',
                  dkim_fail_tag='dmarc/dkim-fail',
                  spf_ok_tag='dmarc/spf-ok',
-                 spf_fail_tag='dmarc/spf-fail'):
+                 spf_fail_tag='dmarc/spf-fail',
+                 subject_regexp=r'^report domain:'):
         super().__init__(database)
         self.dkim_tag = {True: dkim_ok_tag, False: dkim_fail_tag}
         self.spf_tag = {True: spf_ok_tag, False: spf_fail_tag}
-        self.dmarc_subject = re.compile(r'^report domain:',
+        self.dmarc_subject = re.compile(subject_regexp,
                                         flags=re.IGNORECASE)
         self.log = logging.getLogger('{}.{}'.format(
             self.__module__, self.__class__.__name__))
diff -Nru afew-3.0.1/afew/filters/FolderNameFilter.py afew-3.0.1.post63/afew/filters/FolderNameFilter.py
--- afew-3.0.1/afew/filters/FolderNameFilter.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/filters/FolderNameFilter.py	2025-03-15 10:46:03.000000000 -0300
@@ -2,7 +2,6 @@
 # Copyright (c) dtk <d...@gmx.de>
 
 from afew.filters.BaseFilter import Filter
-from afew.NotmuchSettings import notmuch_settings
 import re
 import shlex
 
@@ -15,7 +14,7 @@
         super().__init__(database)
 
         self.__filename_pattern = '{mail_root}/(?P<maildirs>.*)/(cur|new)/[^/]+'.format(
-            mail_root=notmuch_settings.get('database', 'path').rstrip('/'))
+            mail_root=database.db_path.rstrip('/'))
         self.__folder_explicit_list = set(shlex.split(folder_explicit_list))
         self.__folder_blacklist = set(shlex.split(folder_blacklist))
         self.__folder_transforms = self.__parse_transforms(folder_transforms)
diff -Nru afew-3.0.1/afew/filters/HeaderMatchingFilter.py afew-3.0.1.post63/afew/filters/HeaderMatchingFilter.py
--- afew-3.0.1/afew/filters/HeaderMatchingFilter.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/filters/HeaderMatchingFilter.py	2025-03-15 10:46:03.000000000 -0300
@@ -6,6 +6,8 @@
 
 from afew.filters.BaseFilter import Filter
 
+from notmuch.errors import NullPointerError
+
 import re
 
 
@@ -22,10 +24,13 @@
     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:
-                    sub = (lambda tag:
-                           tag.format(**match.groupdict()).lower())
-                    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
diff -Nru afew-3.0.1/afew/filters/SentMailsFilter.py afew-3.0.1.post63/afew/filters/SentMailsFilter.py
--- afew-3.0.1/afew/filters/SentMailsFilter.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/filters/SentMailsFilter.py	2025-03-15 10:46:03.000000000 -0300
@@ -21,11 +21,11 @@
                 my_addresses.add(other_email)
 
         self.query = (
-                '(' +
-                ' OR '.join('from:"%s"' % address for address in my_addresses)
-                + ') AND NOT (' +
-                ' OR '.join('to:"%s"' % address for address in my_addresses)
-                + ')'
+            '(' +
+            ' OR '.join('from:"%s"' % address for address in my_addresses) +
+            ') AND NOT (' +
+            ' OR '.join('to:"%s"' % address for address in my_addresses) +
+            ')'
         )
 
         self.sent_tag = sent_tag
@@ -58,7 +58,7 @@
         return email_to_tags
 
     def __get_bare_email(self, email):
-        if not '<' in email:
+        if '<' not in email:
             return email
         else:
             match = self._bare_email_re.search(email)
diff -Nru afew-3.0.1/afew/MailMover.py afew-3.0.1.post63/afew/MailMover.py
--- afew-3.0.1/afew/MailMover.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/MailMover.py	2025-03-15 10:46:03.000000000 -0300
@@ -80,7 +80,7 @@
                         shutil.copy2(fname, self.get_new_name(fname, destination))
                         to_delete_fnames.append(fname)
                     except shutil.SameFileError:
-                        logging.warn("trying to move '{}' onto itself".format(fname))
+                        logging.warning("trying to move '{}' onto itself".format(fname))
                         continue
                     except shutil.Error as e:
                         # this is ugly, but shutil does not provide more
diff -Nru afew-3.0.1/afew/NotmuchSettings.py afew-3.0.1.post63/afew/NotmuchSettings.py
--- afew-3.0.1/afew/NotmuchSettings.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/NotmuchSettings.py	2025-03-15 10:46:03.000000000 -0300
@@ -9,14 +9,15 @@
 
 
 def read_notmuch_settings(path=None):
-    if path == None:
+    if path is None:
         path = os.environ.get('NOTMUCH_CONFIG', os.path.expanduser('~/.notmuch-config'))
 
     with open(path) as fp:
         notmuch_settings.read_file(fp)
 
-def write_notmuch_settings(path = None):
-    if path == None:
+
+def write_notmuch_settings(path=None):
+    if path is None:
         path = os.environ.get('NOTMUCH_CONFIG', os.path.expanduser('~/.notmuch-config'))
 
     with open(path, 'w+') as fp:
diff -Nru afew-3.0.1/afew/tests/test_headermatchingfilter.py afew-3.0.1.post63/afew/tests/test_headermatchingfilter.py
--- afew-3.0.1/afew/tests/test_headermatchingfilter.py	1969-12-31 20:00:00.000000000 -0400
+++ afew-3.0.1.post63/afew/tests/test_headermatchingfilter.py	2025-03-15 10:46:03.000000000 -0300
@@ -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())
diff -Nru afew-3.0.1/afew/tests/test_mailmover.py afew-3.0.1.post63/afew/tests/test_mailmover.py
--- afew-3.0.1/afew/tests/test_mailmover.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/tests/test_mailmover.py	2025-03-15 10:46:03.000000000 -0300
@@ -4,7 +4,6 @@
 from email.utils import make_msgid
 from freezegun import freeze_time
 import mailbox
-import notmuch
 import os
 import shutil
 import tempfile
@@ -13,6 +12,7 @@
 from afew.Database import Database
 from afew.NotmuchSettings import notmuch_settings, write_notmuch_settings
 
+
 def create_mail(msg, maildir, notmuch_db, tags, old=False):
     email_message = email.message.EmailMessage()
     # freezegun doesn't handle time zones properly when generating UNIX
@@ -83,11 +83,9 @@
             },
         }
 
-
     def tearDown(self):
         shutil.rmtree(self.test_dir)
 
-
     @staticmethod
     def get_folder_content(db, folder):
         return {
@@ -95,7 +93,6 @@
             for msg in db.do_query('folder:{}'.format(folder)).search_messages()
         }
 
-
     def test_all_rule_cases(self):
         from afew import MailMover
 
@@ -132,7 +129,6 @@
             self.assertEqual(expect_archive, self.get_folder_content(db, '.archive'))
             self.assertEqual(expect_spam, self.get_folder_content(db, '.spam'))
 
-
     def test_max_age(self):
         from afew import MailMover
 
diff -Nru afew-3.0.1/afew/tests/test_utils.py afew-3.0.1.post63/afew/tests/test_utils.py
--- afew-3.0.1/afew/tests/test_utils.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/tests/test_utils.py	1969-12-31 20:00:00.000000000 -0400
@@ -1,12 +0,0 @@
-#
-import unittest
-
-from afew import utils
-
-
-class TestUtils(unittest.TestCase):
-    pass
-
-
-if __name__ == '__main__':
-    unittest.main()
diff -Nru afew-3.0.1/afew/version.py afew-3.0.1.post63/afew/version.py
--- afew-3.0.1/afew/version.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew/version.py	1969-12-31 20:00:00.000000000 -0400
@@ -1,4 +0,0 @@
-# coding: utf-8
-# file generated by setuptools_scm
-# don't change, don't track in version control
-version = '3.0.1'
diff -Nru afew-3.0.1/afew.egg-info/dependency_links.txt afew-3.0.1.post63/afew.egg-info/dependency_links.txt
--- afew-3.0.1/afew.egg-info/dependency_links.txt	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew.egg-info/dependency_links.txt	1969-12-31 20:00:00.000000000 -0400
@@ -1 +0,0 @@
-
diff -Nru afew-3.0.1/afew.egg-info/entry_points.txt afew-3.0.1.post63/afew.egg-info/entry_points.txt
--- afew-3.0.1/afew.egg-info/entry_points.txt	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew.egg-info/entry_points.txt	1969-12-31 20:00:00.000000000 -0400
@@ -1,19 +0,0 @@
-[afew.filter]
-ArchiveSentMailsFilter = afew.filters.ArchiveSentMailsFilter:ArchiveSentMailsFilter
-DKIMValidityFilter = afew.filters.DKIMValidityFilter:DKIMValidityFilter
-DMARCReportInspectionFilter = afew.filters.DMARCReportInspectionFilter:DMARCReportInspectionFilter
-Filter = afew.filters.BaseFilter:Filter
-FolderNameFilter = afew.filters.FolderNameFilter:FolderNameFilter
-HeaderMatchingFilter = afew.filters.HeaderMatchingFilter:HeaderMatchingFilter
-InboxFilter = afew.filters.InboxFilter:InboxFilter
-KillThreadsFilter = afew.filters.KillThreadsFilter:KillThreadsFilter
-ListMailsFilter = afew.filters.ListMailsFilter:ListMailsFilter
-MeFilter = afew.filters.MeFilter:MeFilter
-PropagateTagsByRegexInThreadFilter = afew.filters.PropagateTagsByRegexInThreadFilter:PropagateTagsByRegexInThreadFilter
-PropagateTagsInThreadFilter = afew.filters.PropagateTagsInThreadFilter:PropagateTagsInThreadFilter
-SentMailsFilter = afew.filters.SentMailsFilter:SentMailsFilter
-SpamFilter = afew.filters.SpamFilter:SpamFilter
-
-[console_scripts]
-afew = afew.commands:main
-
diff -Nru afew-3.0.1/afew.egg-info/PKG-INFO afew-3.0.1.post63/afew.egg-info/PKG-INFO
--- afew-3.0.1/afew.egg-info/PKG-INFO	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew.egg-info/PKG-INFO	1969-12-31 20:00:00.000000000 -0400
@@ -1,20 +0,0 @@
-Metadata-Version: 1.1
-Name: afew
-Version: 3.0.1
-Summary: An initial tagging script for notmuch mail
-Home-page: https://github.com/afewmail/afew
-Author: UNKNOWN
-Author-email: UNKNOWN
-License: ISC
-Description: UNKNOWN
-Platform: UNKNOWN
-Classifier: License :: OSI Approved :: ISC License (ISCL)
-Classifier: Development Status :: 4 - Beta
-Classifier: Environment :: Console
-Classifier: Intended Audience :: End Users/Desktop
-Classifier: Programming Language :: Python
-Classifier: Topic :: Communications :: Email
-Classifier: Topic :: Communications :: Email :: Filters
-Classifier: Topic :: Utilities
-Classifier: Topic :: Database
-Provides: afew
diff -Nru afew-3.0.1/afew.egg-info/requires.txt afew-3.0.1.post63/afew.egg-info/requires.txt
--- afew-3.0.1/afew.egg-info/requires.txt	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew.egg-info/requires.txt	1969-12-31 20:00:00.000000000 -0400
@@ -1,3 +0,0 @@
-notmuch
-chardet
-dkimpy
diff -Nru afew-3.0.1/afew.egg-info/SOURCES.txt afew-3.0.1.post63/afew.egg-info/SOURCES.txt
--- afew-3.0.1/afew.egg-info/SOURCES.txt	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew.egg-info/SOURCES.txt	1969-12-31 20:00:00.000000000 -0400
@@ -1,57 +0,0 @@
-.gitignore
-LICENSE
-NEWS.md
-README.rst
-setup.py
-.github/workflows/build.yml
-afew/Database.py
-afew/FilterRegistry.py
-afew/MailMover.py
-afew/NotmuchSettings.py
-afew/Settings.py
-afew/__init__.py
-afew/__main__.py
-afew/commands.py
-afew/configparser.py
-afew/files.py
-afew/main.py
-afew/utils.py
-afew/version.py
-afew.egg-info/PKG-INFO
-afew.egg-info/SOURCES.txt
-afew.egg-info/dependency_links.txt
-afew.egg-info/entry_points.txt
-afew.egg-info/requires.txt
-afew.egg-info/top_level.txt
-afew/defaults/afew.config
-afew/filters/ArchiveSentMailsFilter.py
-afew/filters/BaseFilter.py
-afew/filters/DKIMValidityFilter.py
-afew/filters/DMARCReportInspectionFilter.py
-afew/filters/FolderNameFilter.py
-afew/filters/HeaderMatchingFilter.py
-afew/filters/InboxFilter.py
-afew/filters/KillThreadsFilter.py
-afew/filters/ListMailsFilter.py
-afew/filters/MeFilter.py
-afew/filters/PropagateTagsByRegexInThreadFilter.py
-afew/filters/PropagateTagsInThreadFilter.py
-afew/filters/SentMailsFilter.py
-afew/filters/SpamFilter.py
-afew/filters/__init__.py
-afew/tests/__init__.py
-afew/tests/test_dkimvalidityfilter.py
-afew/tests/test_mailmover.py
-afew/tests/test_settings.py
-afew/tests/test_utils.py
-docs/commandline.rst
-docs/conf.py
-docs/configuration.rst
-docs/extending.rst
-docs/filters.rst
-docs/implementation.rst
-docs/index.rst
-docs/installation.rst
-docs/move_mode.rst
-docs/quickstart.rst
-docs/_static/.keep
\ No newline at end of file
diff -Nru afew-3.0.1/afew.egg-info/top_level.txt afew-3.0.1.post63/afew.egg-info/top_level.txt
--- afew-3.0.1/afew.egg-info/top_level.txt	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/afew.egg-info/top_level.txt	1969-12-31 20:00:00.000000000 -0400
@@ -1 +0,0 @@
-afew
diff -Nru afew-3.0.1/debian/changelog afew-3.0.1.post63/debian/changelog
--- afew-3.0.1/debian/changelog	2025-02-15 20:54:57.000000000 -0400
+++ afew-3.0.1.post63/debian/changelog	2025-03-15 11:07:48.000000000 -0300
@@ -1,3 +1,12 @@
+afew (3.0.1.post63-0.1) unstable; urgency=medium
+
+  * Non-maintainer upload
+  * New upstream snaphot (upstream commit 65227fa)
+  * Apply upstream pull request #350 (needed for python 3.12)
+  * Apply upstream pull request #320 (Closes: #1098664).
+
+ -- David Bremner <brem...@debian.org>  Sat, 15 Mar 2025 11:07:48 -0300
+
 afew (3.0.1-6) unstable; urgency=medium
 
   * Team Upload
diff -Nru afew-3.0.1/debian/control afew-3.0.1.post63/debian/control
--- afew-3.0.1/debian/control	2025-02-15 20:33:02.000000000 -0400
+++ afew-3.0.1.post63/debian/control	2025-03-15 11:07:48.000000000 -0300
@@ -11,7 +11,7 @@
                python3-dkim,
                python3-dnspython,
                python3-freezegun,
-               python3-notmuch,
+               python3-notmuch2,
                python3-pytest,
                python3-setuptools,
                python3-setuptools-scm,
diff -Nru afew-3.0.1/debian/patches/apply-upstream-pull-request-320.patch afew-3.0.1.post63/debian/patches/apply-upstream-pull-request-320.patch
--- afew-3.0.1/debian/patches/apply-upstream-pull-request-320.patch	1969-12-31 20:00:00.000000000 -0400
+++ afew-3.0.1.post63/debian/patches/apply-upstream-pull-request-320.patch	2025-03-15 11:07:48.000000000 -0300
@@ -0,0 +1,671 @@
+From: David Bremner <brem...@debian.org>
+Date: Sat, 15 Mar 2025 11:05:38 -0300
+Subject: Apply upstream pull request 320
+
+This is labelled draft upstream, but seems to be tested by mjg.
+---
+ .github/workflows/build.yml                        |  6 ++---
+ afew/Database.py                                   | 31 ++++++++++------------
+ afew/MailMover.py                                  |  8 +++---
+ afew/files.py                                      | 10 +++----
+ afew/filters/BaseFilter.py                         | 20 +++++++-------
+ afew/filters/DKIMValidityFilter.py                 | 10 ++++---
+ afew/filters/DMARCReportInspectionFilter.py        |  4 +--
+ afew/filters/FolderNameFilter.py                   | 10 ++++---
+ afew/filters/HeaderMatchingFilter.py               |  8 +++---
+ afew/filters/KillThreadsFilter.py                  |  2 +-
+ afew/filters/MeFilter.py                           |  2 +-
+ afew/filters/PropagateTagsByRegexInThreadFilter.py |  4 +--
+ afew/filters/PropagateTagsInThreadFilter.py        |  2 +-
+ afew/filters/SentMailsFilter.py                    |  5 +++-
+ afew/tests/test_dkimvalidityfilter.py              | 20 +++++++-------
+ afew/tests/test_headermatchingfilter.py            | 18 ++++++-------
+ afew/tests/test_mailmover.py                       | 15 ++++++-----
+ afew/utils.py                                      | 12 ++++++---
+ docs/extending.rst                                 |  2 +-
+ setup.py                                           |  2 +-
+ 20 files changed, 103 insertions(+), 88 deletions(-)
+
+diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
+index 2951a12..d24577f 100644
+--- a/.github/workflows/build.yml
++++ b/.github/workflows/build.yml
+@@ -9,7 +9,7 @@ jobs:
+       matrix:
+         python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+     name: Build (Python ${{ matrix.python-version }})
+-    runs-on: ubuntu-20.04
++    runs-on: ubuntu-21.04
+     steps:
+       - uses: actions/checkout@v2
+         with:
+@@ -24,11 +24,11 @@ jobs:
+         shell: bash
+         run: |
+           sudo apt-get update
+-          sudo apt-get install -y notmuch python3-notmuch python3-venv flake8
++          sudo apt-get install -y notmuch python3-notmuch2 python3-venv flake8
+           python3 -m venv env
+           source ./env/bin/activate
+           pip install setuptools setuptools_scm pytest dkimpy
+-          ln -s /usr/lib/python3/dist-packages/notmuch ./env/lib/python*/site-packages
++          ln -s /usr/lib/python3/dist-packages/notmuch2 ./env/lib/python*/site-packages
+       - name: flake8 lint
+         run: |
+           source ./env/bin/activate
+diff --git a/afew/Database.py b/afew/Database.py
+index ee9812e..208ed2b 100644
+--- a/afew/Database.py
++++ b/afew/Database.py
+@@ -5,7 +5,7 @@ import os
+ import time
+ import logging
+ 
+-import notmuch
++import notmuch2
+ 
+ from afew.NotmuchSettings import notmuch_settings, get_notmuch_new_tags
+ 
+@@ -46,19 +46,18 @@ class Database:
+         """
+         self.close()
+ 
+-    def open(self, rw=False, retry_for=180, retry_delay=1, create=False):
++    def open(self, rw=False, retry_for=180, retry_delay=1):
+         if rw:
+-            if self.handle and self.handle.mode == notmuch.Database.MODE.READ_WRITE:
++            if self.handle and self.handle.mode == notmuch2.Database.MODE.READ_WRITE:
+                 return self.handle
+ 
+             start_time = time.time()
+             while True:
+                 try:
+-                    self.handle = notmuch.Database(self.db_path,
+-                                                   mode=notmuch.Database.MODE.READ_WRITE,
+-                                                   create=create)
++                    self.handle = notmuch2.Database(self.db_path,
++                                                    mode=notmuch2.Database.MODE.READ_WRITE)
+                     break
+-                except notmuch.NotmuchError:
++                except notmuch2.NotmuchError:
+                     time_left = int(retry_for - (time.time() - start_time))
+ 
+                     if time_left <= 0:
+@@ -71,7 +70,8 @@ class Database:
+                     time.sleep(retry_delay)
+         else:
+             if not self.handle:
+-                self.handle = notmuch.Database(self.db_path, create=create)
++                self.handle = notmuch2.Database(self.db_path,
++                                                mode=notmuch2.Database.MODE.READ_ONLY)
+ 
+         return self.handle
+ 
+@@ -93,7 +93,7 @@ class Database:
+         :rtype:   :class:`notmuch.Query`
+         """
+         logging.debug('Executing query %r' % query)
+-        return notmuch.Query(self.open(), query)
++        return notmuch2.Database.messages(self.open(), query)
+ 
+     def get_messages(self, query, full_thread=False):
+         """
+@@ -106,10 +106,10 @@ class Database:
+         :returns: an iterator over :class:`notmuch.Message` objects
+         """
+         if not full_thread:
+-            for message in self.do_query(query).search_messages():
++            for message in self.do_query(query):
+                 yield message
+         else:
+-            for thread in self.do_query(query).search_threads():
++            for thread in self.do_query(query):
+                 for message in self.walk_thread(thread):
+                     yield message
+ 
+@@ -163,16 +163,13 @@ class Database:
+         """
+         # TODO: it would be nice to update notmuchs directory index here
+         handle = self.open(rw=True)
+-        if hasattr(notmuch.Database, 'index_file'):
+-            message, status = handle.index_file(path, sync_maildir_flags=sync_maildir_flags)
+-        else:
+-            message, status = handle.add_message(path, sync_maildir_flags=sync_maildir_flags)
++        message, duplicate = handle.add(path, sync_flags=sync_maildir_flags)
+ 
+-        if status != notmuch.STATUS.DUPLICATE_MESSAGE_ID:
++        if not duplicate:
+             logging.info('Found new mail in {}'.format(path))
+ 
+             for tag in get_notmuch_new_tags():
+-                message.add_tag(tag)
++                message.tags.add(tag)
+ 
+             if new_mail_handler:
+                 new_mail_handler(message)
+diff --git a/afew/MailMover.py b/afew/MailMover.py
+index 81caec0..5b11394 100644
+--- a/afew/MailMover.py
++++ b/afew/MailMover.py
+@@ -8,8 +8,6 @@ import uuid
+ from datetime import date, datetime, timedelta
+ from subprocess import check_call, CalledProcessError, DEVNULL
+ 
+-import notmuch
+-
+ from afew.Database import Database
+ from afew.utils import get_message_summary
+ 
+@@ -21,7 +19,7 @@ class MailMover(Database):
+ 
+     def __init__(self, max_age=0, rename=False, dry_run=False, notmuch_args='', quiet=False):
+         super().__init__()
+-        self.db = notmuch.Database(self.db_path)
++        self.db = Database()
+         self.query = 'folder:"{folder}" AND {subquery}'
+         if max_age:
+             days = timedelta(int(max_age))
+@@ -61,11 +59,11 @@ class MailMover(Database):
+             main_query = self.query.format(
+                 folder=maildir.replace("\"", "\\\""), subquery=query)
+             logging.debug("query: {}".format(main_query))
+-            messages = notmuch.Query(self.db, main_query).search_messages()
++            messages = self.db.get_messages(main_query)
+             for message in messages:
+                 # a single message (identified by Message-ID) can be in several
+                 # places; only touch the one(s) that exists in this maildir
+-                all_message_fnames = message.get_filenames()
++                all_message_fnames = (str(name) for name in message.filenames())
+                 to_move_fnames = [name for name in all_message_fnames
+                                   if maildir in name]
+                 if not to_move_fnames:
+diff --git a/afew/files.py b/afew/files.py
+index 8186c09..475b65a 100644
+--- a/afew/files.py
++++ b/afew/files.py
+@@ -8,7 +8,7 @@ import logging
+ import platform
+ import queue
+ import threading
+-import notmuch
++import notmuch2
+ import pyinotify
+ import ctypes
+ import contextlib
+@@ -43,19 +43,19 @@ class EventHandler(pyinotify.ProcessEvent):
+         def new_mail(message):
+             for filter_ in self.options.enable_filters:
+                 try:
+-                    filter_.run('id:"{}"'.format(message.get_message_id()))
++                    filter_.run('id:"{}"'.format(message.messageid))
+                     filter_.commit(self.options.dry_run)
+                 except Exception as e:
+                     logging.warning('Error processing mail with filter {!r}: {}'.format(filter_.message, e))
+ 
+         try:
+             self.database.add_message(event.pathname,
+-                                      sync_maildir_flags=True,
++                                      sync_flags=True,
+                                       new_mail_handler=new_mail)
+-        except notmuch.FileError as e:
++        except notmuch2.FileError as e:
+             logging.warning('Error opening mail file: {}'.format(e))
+             return
+-        except notmuch.FileNotEmailError as e:
++        except notmuch2.FileNotEmailError as e:
+             logging.warning('File does not look like an email: {}'.format(e))
+             return
+         else:
+diff --git a/afew/filters/BaseFilter.py b/afew/filters/BaseFilter.py
+index b4e52ea..3803d7b 100644
+--- a/afew/filters/BaseFilter.py
++++ b/afew/filters/BaseFilter.py
+@@ -55,27 +55,27 @@ class Filter:
+             self.handle_message(message)
+ 
+     def handle_message(self, message):
+-        if not self._tag_blacklist.intersection(message.get_tags()):
++        if not self._tag_blacklist.intersection(message.tags):
+             self.remove_tags(message, *self._tags_to_remove)
+             self.add_tags(message, *self._tags_to_add)
+ 
+     def add_tags(self, message, *tags):
+         if tags:
+             self.log.debug('Adding tags %s to id:%s' % (', '.join(tags),
+-                                                        message.get_message_id()))
+-            self._add_tags[message.get_message_id()].update(tags)
++                                                        message.messageid))
++            self._add_tags[message.messageid].update(tags)
+ 
+     def remove_tags(self, message, *tags):
+         if tags:
+             filtered_tags = list(tags)
+             self.log.debug('Removing tags %s from id:%s' % (', '.join(filtered_tags),
+-                                                            message.get_message_id()))
+-            self._remove_tags[message.get_message_id()].update(filtered_tags)
++                                                            message.messageid))
++            self._remove_tags[message.messageid].update(filtered_tags)
+ 
+     def flush_tags(self, message):
+         self.log.debug('Removing all tags from id:%s' %
+-                       message.get_message_id())
+-        self._flush_tags.append(message.get_message_id())
++                       message.messageid)
++        self._flush_tags.append(message.messageid)
+ 
+     def commit(self, dry_run=True):
+         dirty_messages = set()
+@@ -93,15 +93,15 @@ class Filter:
+             db = self.database.open(rw=True)
+ 
+             for message_id in dirty_messages:
+-                message = db.find_message(message_id)
++                message = db.find(message_id)
+ 
+                 if message_id in self._flush_tags:
+                     message.remove_all_tags()
+ 
+                 for tag in self._add_tags.get(message_id, []):
+-                    message.add_tag(tag)
++                    message.tags.add(tag)
+ 
+                 for tag in self._remove_tags.get(message_id, []):
+-                    message.remove_tag(tag)
++                    message.tags.remove(tag)
+ 
+         self.flush_changes()
+diff --git a/afew/filters/DKIMValidityFilter.py b/afew/filters/DKIMValidityFilter.py
+index ad7be1a..46f8138 100644
+--- a/afew/filters/DKIMValidityFilter.py
++++ b/afew/filters/DKIMValidityFilter.py
+@@ -50,14 +50,18 @@ class DKIMValidityFilter(Filter):
+             self.__module__, self.__class__.__name__))
+ 
+     def handle_message(self, message):
+-        if message.get_header(self.header):
++        try:
++            selfhead = message.header(self.header)
++        except LookupError:
++            selfhead = ''
++        if selfhead:
+             try:
+-                dkim_ok = all(map(verify_dkim, message.get_filenames()))
++                dkim_ok = all(map(verify_dkim, message.filenames()))
+             except DKIMVerifyError as verify_error:
+                 self.log.warning(
+                     "Failed to verify DKIM of '%s': %s "
+                     "(marked as 'dkim-fail')",
+-                    message.get_message_id(),
++                    message.messageid,
+                     verify_error
+                 )
+                 dkim_ok = False
+diff --git a/afew/filters/DMARCReportInspectionFilter.py b/afew/filters/DMARCReportInspectionFilter.py
+index f3cbda1..24d423b 100644
+--- a/afew/filters/DMARCReportInspectionFilter.py
++++ b/afew/filters/DMARCReportInspectionFilter.py
+@@ -147,7 +147,7 @@ class DMARCReportInspectionFilter(Filter):
+             self.__module__, self.__class__.__name__))
+ 
+     def handle_message(self, message):
+-        if not self.dmarc_subject.match(message.get_header('Subject')):
++        if not self.dmarc_subject.match(message.header('Subject')):
+             return
+ 
+         auth_results = {'dkim': True, 'spf': True}
+@@ -165,6 +165,6 @@ class DMARCReportInspectionFilter(Filter):
+         except DMARCInspectionError as inspection_error:
+             self.log.error(
+                 "Failed to verify DMARC report of '%s': %s (not tagging)",
+-                message.get_message_id(),
++                message.messageid,
+                 inspection_error
+             )
+diff --git a/afew/filters/FolderNameFilter.py b/afew/filters/FolderNameFilter.py
+index af8ed78..6b2a535 100644
+--- a/afew/filters/FolderNameFilter.py
++++ b/afew/filters/FolderNameFilter.py
+@@ -24,8 +24,8 @@ class FolderNameFilter(Filter):
+     def handle_message(self, message):
+         # Find all the dirs in the mail directory that this message
+         # belongs to
+-        maildirs = [re.match(self.__filename_pattern, filename)
+-                    for filename in message.get_filenames()]
++        maildirs = [re.match(self.__filename_pattern, str(filename))
++                    for filename in message.filenames()]
+         maildirs = filter(None, maildirs)
+         if maildirs:
+             # Make the folders relative to mail_root and split them.
+@@ -34,8 +34,12 @@ class FolderNameFilter(Filter):
+             folders = set([folder
+                            for folder_group in folder_groups
+                            for folder in folder_group])
++            try:
++                subject = message.header('subject')
++            except LookupError:
++                subject = ''
+             self.log.debug('found folders {} for message {!r}'.format(
+-                folders, message.get_header('subject')))
++                folders, subject))
+ 
+             # remove blacklisted folders
+             clean_folders = folders - self.__folder_blacklist
+diff --git a/afew/filters/HeaderMatchingFilter.py b/afew/filters/HeaderMatchingFilter.py
+index 4cbcf86..6bd6bbb 100644
+--- a/afew/filters/HeaderMatchingFilter.py
++++ b/afew/filters/HeaderMatchingFilter.py
+@@ -6,7 +6,7 @@
+ 
+ from afew.filters.BaseFilter import Filter
+ 
+-from notmuch.errors import NullPointerError
++from notmuch2._errors import NullPointerError
+ 
+ import re
+ 
+@@ -23,14 +23,14 @@ class HeaderMatchingFilter(Filter):
+ 
+     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()):
++            if not self._tag_blacklist.intersection(message.tags):
+                 try:
+-                    value = message.get_header(self.header)
++                    value = message.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:
++                except (NullPointerError, LookupError):
+                     pass
+diff --git a/afew/filters/KillThreadsFilter.py b/afew/filters/KillThreadsFilter.py
+index a9187bb..08a5b9f 100644
+--- a/afew/filters/KillThreadsFilter.py
++++ b/afew/filters/KillThreadsFilter.py
+@@ -9,7 +9,7 @@ class KillThreadsFilter(Filter):
+     query = 'NOT tag:killed'
+ 
+     def handle_message(self, message):
+-        query = self.database.get_messages('thread:"%s" AND tag:killed' % message.get_thread_id())
++        query = self.database.get_messages('thread:"%s" AND tag:killed' % message.threadid)
+ 
+         if len(list(query)):
+             self.add_tags(message, 'killed')
+diff --git a/afew/filters/MeFilter.py b/afew/filters/MeFilter.py
+index 38b1c97..6ddded3 100644
+--- a/afew/filters/MeFilter.py
++++ b/afew/filters/MeFilter.py
+@@ -26,5 +26,5 @@ class MeFilter(Filter):
+         self.me_tag = me_tag
+ 
+     def handle_message(self, message):
+-        if not self._tag_blacklist.intersection(message.get_tags()):
++        if not self._tag_blacklist.intersection(message.tags):
+             self.add_tags(message, self.me_tag)
+diff --git a/afew/filters/PropagateTagsByRegexInThreadFilter.py b/afew/filters/PropagateTagsByRegexInThreadFilter.py
+index 7587c67..ea59590 100644
+--- a/afew/filters/PropagateTagsByRegexInThreadFilter.py
++++ b/afew/filters/PropagateTagsByRegexInThreadFilter.py
+@@ -39,7 +39,7 @@ class PropagateTagsByRegexInThreadFilter(Filter):
+     """
+ 
+     def handle_message(self, message):
+-        thread_query = 'thread:"%s"' % (message.get_thread_id(),)
++        thread_query = 'thread:"%s"' % (message.threadid,)
+         if self._filter:
+             query = self.database.get_messages("(%s) AND (%s)" % (thread_query, self._filter))
+         else:
+@@ -50,7 +50,7 @@ class PropagateTagsByRegexInThreadFilter(Filter):
+         messages = list(query)
+ 
+         # flatten  tags
+-        tags_in_thread_t = {m.get_tags() for m in messages}  # a set of Tags instances
++        tags_in_thread_t = {m.tags for m in messages}  # a set of Tags instances
+         tags_in_thread = set(_flatten(tags_in_thread_t))
+ 
+         # filter tags
+diff --git a/afew/filters/PropagateTagsInThreadFilter.py b/afew/filters/PropagateTagsInThreadFilter.py
+index a87c34e..f25da48 100644
+--- a/afew/filters/PropagateTagsInThreadFilter.py
++++ b/afew/filters/PropagateTagsInThreadFilter.py
+@@ -20,7 +20,7 @@ class PropagateTagsInThreadFilter(Filter):
+ 
+     def handle_message(self, message):
+         for tag in self._propagate_tags:
+-            tag_query = 'thread:"%s" AND is:"%s"' % (message.get_thread_id(), tag)
++            tag_query = 'thread:"%s" AND is:"%s"' % (message.threadid, tag)
+             if self._filter:
+                 query = self.database.get_messages("(%s) AND (%s)" % (tag_query, self._filter))
+             else:
+diff --git a/afew/filters/SentMailsFilter.py b/afew/filters/SentMailsFilter.py
+index 78ce3bf..a948bc3 100644
+--- a/afew/filters/SentMailsFilter.py
++++ b/afew/filters/SentMailsFilter.py
+@@ -38,7 +38,10 @@ class SentMailsFilter(Filter):
+             self.add_tags(message, self.sent_tag)
+         if self.to_transforms:
+             for header in ('To', 'Cc', 'Bcc'):
+-                email = self.__get_bare_email(message.get_header(header))
++                try:
++                    email = self.__get_bare_email(message.header(header))
++                except LookupError:
++                    email = ''
+                 for tag in self.__pick_tags(email):
+                     self.add_tags(message, tag)
+                 else:
+diff --git a/afew/tests/test_dkimvalidityfilter.py b/afew/tests/test_dkimvalidityfilter.py
+index 2596728..9e39334 100644
+--- a/afew/tests/test_dkimvalidityfilter.py
++++ b/afew/tests/test_dkimvalidityfilter.py
+@@ -39,19 +39,19 @@ def _make_message():
+ 
+     Mocked methods:
+ 
+-    - `get_header()` returns non-empty string. When testing with mocked
++    - `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
++    - `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.
++    - `messageid` returns some generated message ID.
+     """
+     message = mock.Mock()
+-    message.get_header.return_value = 'sig'
+-    message.get_filenames.return_value = ['a']
+-    message.get_message_id.return_value = make_msgid()
++    message.header.return_value = 'sig'
++    message.filenames.return_value = ['a']
++    message.messageid = make_msgid()
+     return message
+ 
+ 
+@@ -65,7 +65,7 @@ class TestDKIMValidityFilter(unittest.TestCase):
+         """
+         dkim_filter, tags = _make_dkim_validity_filter()
+         message = _make_message()
+-        message.get_header.return_value = False
++        message.header.return_value = False
+ 
+         with mock.patch('afew.filters.DKIMValidityFilter.dkim.verify') \
+                 as dkim_verify:
+@@ -82,7 +82,7 @@ class TestDKIMValidityFilter(unittest.TestCase):
+         """
+         dkim_filter, tags = _make_dkim_validity_filter()
+         message = _make_message()
+-        message.get_filenames.return_value = ['a', 'b', 'c']
++        message.filenames.return_value = ['a', 'b', 'c']
+ 
+         with mock.patch('afew.filters.DKIMValidityFilter.dkim.verify') \
+                 as dkim_verify:
+@@ -99,7 +99,7 @@ class TestDKIMValidityFilter(unittest.TestCase):
+         """
+         dkim_filter, tags = _make_dkim_validity_filter()
+         message = _make_message()
+-        message.get_filenames.return_value = ['a', 'b', 'c']
++        message.filenames.return_value = ['a', 'b', 'c']
+ 
+         with mock.patch('afew.filters.DKIMValidityFilter.dkim.verify') \
+                 as dkim_verify:
+@@ -116,7 +116,7 @@ class TestDKIMValidityFilter(unittest.TestCase):
+         """
+         dkim_filter, tags = _make_dkim_validity_filter()
+         message = _make_message()
+-        message.get_filenames.return_value = ['a', 'b', 'c']
++        message.filenames.return_value = ['a', 'b', 'c']
+ 
+         with mock.patch('afew.filters.DKIMValidityFilter.dkim.verify') \
+                 as dkim_verify:
+diff --git a/afew/tests/test_headermatchingfilter.py b/afew/tests/test_headermatchingfilter.py
+index 8b09a67..fee1f4b 100644
+--- a/afew/tests/test_headermatchingfilter.py
++++ b/afew/tests/test_headermatchingfilter.py
+@@ -7,7 +7,7 @@ from unittest import mock
+ from afew.Database import Database
+ from afew.filters.HeaderMatchingFilter import HeaderMatchingFilter
+ 
+-from notmuch.errors import NullPointerError
++from notmuch2._errors import NullPointerError
+ 
+ 
+ class _AddTags:  # pylint: disable=too-few-public-methods
+@@ -38,23 +38,23 @@ def _make_message(should_fail):
+ 
+     Mocked methods:
+ 
+-    - `get_header()` returns non-empty string. When testing with mocked
++    - `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
++    - `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` returns some generated message ID.
+     """
+     message = mock.Mock()
+     if should_fail:
+-        message.get_header.side_effect = NullPointerError
++        message.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()
++        message.header.return_value = 'header'
++    message.filenames.return_value = ['a']
++    message.tags = ['a']
++    message.messageid = make_msgid()
+     return message
+ 
+ 
+diff --git a/afew/tests/test_mailmover.py b/afew/tests/test_mailmover.py
+index b4f240e..a8ffdc7 100644
+--- a/afew/tests/test_mailmover.py
++++ b/afew/tests/test_mailmover.py
+@@ -8,6 +8,7 @@ import os
+ import shutil
+ import tempfile
+ import unittest
++import notmuch2
+ 
+ from afew.Database import Database
+ from afew.NotmuchSettings import notmuch_settings, write_notmuch_settings
+@@ -35,7 +36,7 @@ def create_mail(msg, maildir, notmuch_db, tags, old=False):
+     fname = os.path.join(maildir._path, maildir._lookup(message_key))
+     notmuch_msg = notmuch_db.add_message(fname)
+     for tag in tags:
+-        notmuch_msg.add_tag(tag, False)
++        notmuch_msg.tags.add(tag)
+ 
+     # Remove the angle brackets automatically added around the message ID by make_msgid.
+     stripped_msgid = email_message['Message-ID'].strip('<>')
+@@ -55,7 +56,7 @@ class TestMailMover(unittest.TestCase):
+         write_notmuch_settings()
+ 
+         # Create notmuch database
+-        Database().open(create=True).close()
++        notmuch2.Database.create().close()
+ 
+         self.root = mailbox.Maildir(self.test_dir)
+         self.inbox = self.root.add_folder('inbox')
+@@ -88,10 +89,12 @@ class TestMailMover(unittest.TestCase):
+ 
+     @staticmethod
+     def get_folder_content(db, folder):
+-        return {
+-            (os.path.basename(msg.get_message_id()), msg.get_part(1).decode())
+-            for msg in db.do_query('folder:{}'.format(folder)).search_messages()
+-        }
++        ret = set()
++        for msg in db.open().messages('folder:{}'.format(folder)):
++            with open(msg.path) as f:
++                ret.add((os.path.basename(msg.messageid),
++                         email.message_from_file(f).get_payload()))
++        return ret
+ 
+     def test_all_rule_cases(self):
+         from afew import MailMover
+diff --git a/afew/utils.py b/afew/utils.py
+index 8a9f491..04b41e9 100644
+--- a/afew/utils.py
++++ b/afew/utils.py
+@@ -6,15 +6,21 @@ from datetime import datetime
+ 
+ 
+ def get_message_summary(message):
+-    when = datetime.fromtimestamp(float(message.get_date()))
++    when = datetime.fromtimestamp(float(message.date))
+     sender = get_sender(message)
+-    subject = message.get_header('Subject')
++    try:
++        subject = message.header('Subject')
++    except LookupError:
++        subject = ''
+     return '[{date}] {sender} | {subject}'.format(date=when, sender=sender,
+                                                   subject=subject)
+ 
+ 
+ def get_sender(message):
+-    sender = message.get_header('From')
++    try:
++        sender = message.header('From')
++    except LookupError:
++        sender = ''
+     name_match = re.search(r'(.+) <.+@.+\..+>', sender)
+     if name_match:
+         sender = name_match.group(1)
+diff --git a/docs/extending.rst b/docs/extending.rst
+index 844f9e6..1d6012a 100644
+--- a/docs/extending.rst
++++ b/docs/extending.rst
+@@ -36,7 +36,7 @@ we ensure we don't bother looking at messages we've already looked at.
+ 
+ The `handle_message()` method is the key one to implement.  This will be called
+ for each message that matches the query.  The argument is a `notmuch message object`_
+-and the key methods used by the afew filters are `get_header()`, `get_filename()`
++and the key methods used by the afew filters are `header()`, `filename()`
+ and `get_thread()`.
+ 
+ .. _notmuch message object: http://pythonhosted.org/notmuch/#message-a-single-message
+diff --git a/setup.py b/setup.py
+index 2c4b558..78933e8 100755
+--- a/setup.py
++++ b/setup.py
+@@ -8,7 +8,7 @@ from setuptools import setup, find_packages
+ 
+ def get_requires():
+     if os.environ.get('TRAVIS') != 'true' and os.environ.get('READTHEDOCS') != 'True':
+-        yield 'notmuch'
++        yield 'notmuch2'
+     yield 'chardet'
+     yield 'dkimpy'
+ 
diff -Nru afew-3.0.1/debian/patches/fix-invalid-escape-sequence.patch afew-3.0.1.post63/debian/patches/fix-invalid-escape-sequence.patch
--- afew-3.0.1/debian/patches/fix-invalid-escape-sequence.patch	2025-02-15 20:18:08.000000000 -0400
+++ afew-3.0.1.post63/debian/patches/fix-invalid-escape-sequence.patch	1969-12-31 20:00:00.000000000 -0400
@@ -1,23 +0,0 @@
-From: Guillaume Seren <guillaumese...@gmail.com>
-Date: Sun, 21 Aug 2022 14:00:36 +0200
-Subject: [PATCH] flake8: Fix W605 invalid escape sequence '\.'
-
-Origin: upstream, https://github.com/afewmail/afew/commit/86fc5091c7154862409cd201d1b83f26e8511a2c
-Bug-Debian: https://bugs.debian.org/1085322
----
- afew/files.py | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/afew/files.py b/afew/files.py
-index 557e343..d3ae031 100644
---- a/afew/files.py
-+++ b/afew/files.py
-@@ -22,7 +22,7 @@ class EventHandler(pyinotify.ProcessEvent):
-         self.database = database
-         super().__init__()
- 
--    ignore_re = re.compile('(/xapian/.*(base.|tmp)$)|(\.lock$)|(/dovecot)')
-+    ignore_re = re.compile(r'(/xapian/.*(base.|tmp)$)|(\.lock$)|(/dovecot)')
- 
-     def process_IN_DELETE(self, event):
-         if self.ignore_re.search(event.pathname):
diff -Nru afew-3.0.1/debian/patches/fix-settings-file-reading-method-in-sett.patch afew-3.0.1.post63/debian/patches/fix-settings-file-reading-method-in-sett.patch
--- afew-3.0.1/debian/patches/fix-settings-file-reading-method-in-sett.patch	1969-12-31 20:00:00.000000000 -0400
+++ afew-3.0.1.post63/debian/patches/fix-settings-file-reading-method-in-sett.patch	2025-03-15 11:07:48.000000000 -0300
@@ -0,0 +1,25 @@
+From: Gabriele Mongiano <hom3rs...@gmail.com>
+Date: Fri, 8 Nov 2024 15:22:15 +0100
+Subject: Fix settings file reading method in Settings.py
+
+- Updated method from `readfp` to `read_file` for compatibility with Python 3.
+- No change in functionality; improves code compliance with current Python standards.
+
+(this is upstream PR 350)
+---
+ afew/Settings.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/afew/Settings.py b/afew/Settings.py
+index 44d6f3c..86a543b 100644
+--- a/afew/Settings.py
++++ b/afew/Settings.py
+@@ -18,7 +18,7 @@ settings = ConfigParser()
+ # preserve the capitalization of the keys.
+ settings.optionxform = str
+ 
+-settings.readfp(open(os.path.join(os.path.dirname(__file__), 'defaults', 'afew.config')))
++settings.read_file(open(os.path.join(os.path.dirname(__file__), 'defaults', 'afew.config')))
+ settings.read(os.path.join(user_config_dir, 'config'))
+ 
+ # All the values for keys listed here are interpreted as ;-delimited lists
diff -Nru afew-3.0.1/debian/patches/series afew-3.0.1.post63/debian/patches/series
--- afew-3.0.1/debian/patches/series	2025-02-15 20:18:08.000000000 -0400
+++ afew-3.0.1.post63/debian/patches/series	2025-03-15 11:07:48.000000000 -0300
@@ -1 +1,2 @@
-fix-invalid-escape-sequence.patch
+apply-upstream-pull-request-320.patch
+fix-settings-file-reading-method-in-sett.patch
diff -Nru afew-3.0.1/docs/conf.py afew-3.0.1.post63/docs/conf.py
--- afew-3.0.1/docs/conf.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/docs/conf.py	2025-03-15 10:46:03.000000000 -0300
@@ -12,7 +12,8 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import sys, os
+import sys
+import os
 from pkg_resources import get_distribution
 
 # If extensions (or modules to document with autodoc) are in another directory,
@@ -42,7 +43,7 @@
 # -- General configuration -----------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
@@ -79,37 +80,37 @@
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 exclude_patterns = []
 
 # The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = 'sphinx'
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 
 # -- Options for HTML output ---------------------------------------------------
@@ -121,26 +122,26 @@
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -149,44 +150,44 @@
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Output file base name for HTML help builder.
 htmlhelp_basename = 'afewdoc'
@@ -195,10 +196,10 @@
 # -- Options for LaTeX output --------------------------------------------------
 
 # The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
 
 # The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
@@ -209,26 +210,26 @@
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 
 # -- Options for manual page output --------------------------------------------
diff -Nru afew-3.0.1/docs/filters.rst afew-3.0.1.post63/docs/filters.rst
--- afew-3.0.1/docs/filters.rst	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/docs/filters.rst	2025-03-15 10:46:03.000000000 -0300
@@ -26,7 +26,7 @@
 DKIMValidityFilter
 ------------------
 
-This filter verifies DKIM signatures of E-Mails with DKIM header, and adds `dkin-ok` or `dkin-fail` tags.
+This filter verifies DKIM signatures of E-Mails with DKIM header, and adds `dkim-ok` or `dkim-fail` tags.
 
 DMARCReportInspectionFilter
 ---------------------------
@@ -145,7 +145,7 @@
 
     [HeaderMatchingFilter.3]
     header = X-Redmine-Project
-    pattern = (?P<project>.*)
+    pattern = (?P<project>.+)
     tags = +redmine;+{project}
 
 SpamFilter and ListMailsFilter are implemented using HeaderMatchingFilter, and are
diff -Nru afew-3.0.1/.github/workflows/build.yml afew-3.0.1.post63/.github/workflows/build.yml
--- afew-3.0.1/.github/workflows/build.yml	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/.github/workflows/build.yml	2025-03-15 10:46:03.000000000 -0300
@@ -7,24 +7,32 @@
   build-ubuntu:
     strategy:
       matrix:
-        python: [3.6, 3.7, 3.8]
-    name: Build (Python ${{ matrix.python }})
-    runs-on: ubuntu-18.04
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+    name: Build (Python ${{ matrix.python-version }})
+    runs-on: ubuntu-20.04
     steps:
       - uses: actions/checkout@v2
-      - name: Set up Python
+        with:
+          fetch-depth: 0
+      - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v1
         with:
-          python-version: '${{ matrix.python }}'
+          python-version: '${{ matrix.python-version }}'
+      - name: Display Python version
+        run: python -c "import sys; print(sys.version)"
       - name: Install dependencies
         shell: bash
         run: |
           sudo apt-get update
-          sudo apt-get install -y notmuch python3-notmuch python3-venv
+          sudo apt-get install -y notmuch python3-notmuch python3-venv flake8
           python3 -m venv env
           source ./env/bin/activate
-          pip install setuptools pytest dkimpy
+          pip install setuptools setuptools_scm pytest dkimpy
           ln -s /usr/lib/python3/dist-packages/notmuch ./env/lib/python*/site-packages
+      - name: flake8 lint
+        run: |
+          source ./env/bin/activate
+          flake8 --ignore=E501,W504 afew/
       - name: Tests
         run: |
           source ./env/bin/activate
@@ -38,20 +46,9 @@
         run: |
           source ./env/bin/activate
           python setup.py install
-      - name: Generate coverage report
-        run: |
-          source ./env/bin/activate
-          pip install pytest-cov
-          pytest --cov=./ --cov-report=xml
-      - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v1
-        with:
-          file: ./coverage.xml
-          flags: unittests
-          name: codecov-umbrella
-          fail_ci_if_error: true
       - name: Docs
         run: |
           source ./env/bin/activate
           pip install sphinx
-          python setup.py build_sphinx -b html,man
+          sphinx-build -b html docs $(mktemp -d)
+          sphinx-build -b man docs $(mktemp -d)
diff -Nru afew-3.0.1/.github/workflows/release.yml afew-3.0.1.post63/.github/workflows/release.yml
--- afew-3.0.1/.github/workflows/release.yml	1969-12-31 20:00:00.000000000 -0400
+++ afew-3.0.1.post63/.github/workflows/release.yml	2025-03-15 10:46:03.000000000 -0300
@@ -0,0 +1,31 @@
+name: Release
+
+on:
+  release:
+    types:
+      - published
+
+jobs:
+  release:
+    needs: [build-ubuntu]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.x'
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install setuptools setuptools_scm wheel twine
+      - name: Build and publish (testpypi)
+        env:
+          TWINE_USERNAME: __token__
+          TWINE_PASSWORD: ${{ secrets.testpypi_token }}
+          TWINE_REPOSITORY: testpypi
+        run: |
+          python setup.py sdist
+          twine upload dist/*
diff -Nru afew-3.0.1/NEWS.md afew-3.0.1.post63/NEWS.md
--- afew-3.0.1/NEWS.md	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/NEWS.md	2025-03-15 10:46:03.000000000 -0300
@@ -1,3 +1,26 @@
+afew 3.1.0 (unreleased)
+=======================
+
+Python 3.6 support dropped
+
+  afew stopped supporting the older python version 3.6.
+
+Handle DMARC report with empty spf or dkim XML nodes
+
+DMARC Filter: allow to define subject regexp
+
+  Some DMARC report mail have a prefix before "Dmarc Report" in the subject
+  and where not checked by the plugin.
+  Afew now allows the user to define the suject regexp in the config file.
+
+Get notmuch database path using Database wrapper
+
+  This allows FolderNameFilter to work with a relative path in database.path of notmuch config file.
+
+HeaderMatchingFilter: do not convert user supplied tags
+
+  This prevents afew to lowercase the tags defined by the user, allowing to have non lowercase tags.
+
 afew 3.0.0 (2020-03-10)
 =======================
 
diff -Nru afew-3.0.1/PKG-INFO afew-3.0.1.post63/PKG-INFO
--- afew-3.0.1/PKG-INFO	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/PKG-INFO	1969-12-31 20:00:00.000000000 -0400
@@ -1,20 +0,0 @@
-Metadata-Version: 1.1
-Name: afew
-Version: 3.0.1
-Summary: An initial tagging script for notmuch mail
-Home-page: https://github.com/afewmail/afew
-Author: UNKNOWN
-Author-email: UNKNOWN
-License: ISC
-Description: UNKNOWN
-Platform: UNKNOWN
-Classifier: License :: OSI Approved :: ISC License (ISCL)
-Classifier: Development Status :: 4 - Beta
-Classifier: Environment :: Console
-Classifier: Intended Audience :: End Users/Desktop
-Classifier: Programming Language :: Python
-Classifier: Topic :: Communications :: Email
-Classifier: Topic :: Communications :: Email :: Filters
-Classifier: Topic :: Utilities
-Classifier: Topic :: Database
-Provides: afew
diff -Nru afew-3.0.1/README.rst afew-3.0.1.post63/README.rst
--- afew-3.0.1/README.rst	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/README.rst	2025-03-15 10:46:03.000000000 -0300
@@ -2,7 +2,7 @@
 afew
 ====
 
-|GithubTag| |CodeCov| |CI Status|
+|GithubTag| |CI Status|
 
 About
 -----
@@ -23,7 +23,7 @@
 configurable rules that can contain arbitrary notmuch queries to match against
 any searchable attributes.
 
-fyi: afew plays nicely with alot, a GUI for notmuch mail ;)
+fyi: afew plays nicely with alot, a TUI for notmuch mail ;)
 
 * https://github.com/pazz/alot
 
@@ -32,9 +32,9 @@
 IRC
 ---
 
-Feel free to ask your questions and discuss usage in the `#afewmail IRC Channel`_ on freenode.
+Feel free to ask your questions and discuss usage in the `#afewmail IRC Channel`_ on Libera.Chat.
 
-.. _#afewmail IRC Channel: http://webchat.freenode.net/?channels=#afewmail
+.. _#afewmail IRC Channel: http://web.libera.chat/?channels=#afewmail
 
 
 Features
@@ -51,7 +51,6 @@
 * can move mails based on arbitrary notmuch queries, so your sorting
   may show on your traditional mail client (well, almost ;))
 * has a ``--dry-run`` mode for safe testing
-* works with python 3.6+
 
 
 
@@ -69,7 +68,5 @@
 
 .. |GithubTag| image:: https://img.shields.io/github/tag/afewmail/afew.svg
     :target: https://github.com/afewmail/afew/releases
-.. |CodeCov| image:: https://codecov.io/gh/afewmail/afew/branch/master/graph/badge.svg
-    :target: https://codecov.io/gh/afewmail/afew
 .. |CI Status| image:: https://github.com/afewmail/afew/workflows/CI/badge.svg
     :target: https://github.com/afewmail/afew/actions
diff -Nru afew-3.0.1/.readthedocs.yaml afew-3.0.1.post63/.readthedocs.yaml
--- afew-3.0.1/.readthedocs.yaml	1969-12-31 20:00:00.000000000 -0400
+++ afew-3.0.1.post63/.readthedocs.yaml	2025-03-15 10:46:03.000000000 -0300
@@ -0,0 +1,22 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+  configuration: docs/conf.py
+
+# We recommend specifying your dependencies to enable reproducible builds:
+# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
+# python:
+#   install:
+#   - requirements: docs/requirements.txt
diff -Nru afew-3.0.1/setup.cfg afew-3.0.1.post63/setup.cfg
--- afew-3.0.1/setup.cfg	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/setup.cfg	1969-12-31 20:00:00.000000000 -0400
@@ -1,4 +0,0 @@
-[egg_info]
-tag_build = 
-tag_date = 0
-
diff -Nru afew-3.0.1/setup.py afew-3.0.1.post63/setup.py
--- afew-3.0.1/setup.py	2021-01-14 04:42:55.000000000 -0400
+++ afew-3.0.1.post63/setup.py	2025-03-15 10:46:03.000000000 -0300
@@ -12,6 +12,8 @@
     yield 'chardet'
     yield 'dkimpy'
 
+with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'README.rst'), encoding='utf-8') as f:
+    long_description = f.read()
 
 setup(
     name='afew',
@@ -19,6 +21,8 @@
     description="An initial tagging script for notmuch mail",
     url="https://github.com/afewmail/afew";,
     license="ISC",
+    long_description=long_description,
+    long_description_content_type="text/x-rst",
     setup_requires=['setuptools_scm'],
     packages=find_packages(),
     test_suite='afew.tests',

Reply via email to