Barry Warsaw pushed to branch master at mailman / Mailman

Commits:
472eb976 by Barry Warsaw at 2016-03-20T21:31:50-04:00
Close #208

Don't let crashes in IArchiver plugins break handlers or runners.

- - - - -


8 changed files:

- src/mailman/docs/NEWS.rst
- src/mailman/handlers/decorate.py
- src/mailman/handlers/rfc_2369.py
- src/mailman/handlers/tests/test_decorate.py
- src/mailman/handlers/tests/test_rfc_2369.py
- src/mailman/model/mailinglist.py
- src/mailman/runners/archive.py
- src/mailman/runners/tests/test_archiver.py


Changes:

=====================================
src/mailman/docs/NEWS.rst
=====================================
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -71,6 +71,8 @@ Bugs
  * Cross-posting messages held on both lists no longer fails.  (Closes #176)
  * Don't let unknown charsets crash the "approved" rule.  Given by Aurélien
    Bompard.  (Closes #203)
+ * Don't let crashes in IArchiver plugins break handlers or runners.
+   (Closes #208)
 
 Configuration
 -------------


=====================================
src/mailman/handlers/decorate.py
=====================================
--- a/src/mailman/handlers/decorate.py
+++ b/src/mailman/handlers/decorate.py
@@ -40,6 +40,7 @@ from zope.interface import implementer
 
 
 log = logging.getLogger('mailman.error')
+alog = logging.getLogger('mailman.archiver')
 
 
 
@@ -64,8 +65,14 @@ def process(mlist, msg, msgdata):
     # the $<archive-name>_url placeholder for every enabled archiver.
     for archiver in IListArchiverSet(mlist).archivers:
         if archiver.is_enabled:
-            # Get the permalink of the message from the archiver.
-            archive_url = archiver.system_archiver.permalink(mlist, msg)
+            # Get the permalink of the message from the archiver.  Watch out
+            # for exceptions in the archiver plugin.
+            try:
+                archive_url = archiver.system_archiver.permalink(mlist, msg)
+            except Exception:
+                alog.exception('Exception in "{}" archiver'.format(
+                    archiver.system_archiver.name))
+                archive_url = None
             if archive_url is not None:
                 placeholder = '{}_url'.format(archiver.system_archiver.name)
                 d[placeholder] = archive_url


=====================================
src/mailman/handlers/rfc_2369.py
=====================================
--- a/src/mailman/handlers/rfc_2369.py
+++ b/src/mailman/handlers/rfc_2369.py
@@ -22,6 +22,8 @@ __all__ = [
     ]
 
 
+import logging
+
 from email.utils import formataddr
 from mailman.core.i18n import _
 from mailman.handlers.cook_headers import uheader
@@ -33,6 +35,8 @@ from zope.interface import implementer
 
 CONTINUATION = ',\n\t'
 
+log = logging.getLogger('mailman.archiver')
+
 
 
 def process(mlist, msg, msgdata):
@@ -85,11 +89,22 @@ def process(mlist, msg, msgdata):
             for archiver in archiver_set.archivers:
                 if not archiver.is_enabled:
                     continue
-                archiver_url = archiver.system_archiver.list_url(mlist)
+                # Watch out for exceptions in the archiver plugin.
+                try:
+                    archiver_url = archiver.system_archiver.list_url(mlist)
+                except Exception:
+                    log.exception('Exception in "{}" archiver'.format(
+                        archiver.system_archiver.name))
+                    archiver_url = None
                 if archiver_url is not None:
                     headers.append(('List-Archive',
                                     '<{}>'.format(archiver_url)))
-                permalink = archiver.system_archiver.permalink(mlist, msg)
+                try:
+                    permalink = archiver.system_archiver.permalink(mlist, msg)
+                except Exception:
+                    log.exception('Exception in "{}" archiver'.format(
+                        archiver.system_archiver.name))
+                    permalink = None
                 if permalink is not None:
                     headers.append(('Archived-At', '<{}>'.format(permalink)))
     # XXX RFC 2369 also defines a List-Owner header which we are not currently


=====================================
src/mailman/handlers/tests/test_decorate.py
=====================================
--- a/src/mailman/handlers/tests/test_decorate.py
+++ b/src/mailman/handlers/tests/test_decorate.py
@@ -18,6 +18,7 @@
 """Test the decorate handler."""
 
 __all__ = [
+    'TestBrokenPermalink',
     'TestDecorate',
     ]
 
@@ -29,7 +30,8 @@ from mailman.app.lifecycle import create_list
 from mailman.config import config
 from mailman.handlers import decorate
 from mailman.interfaces.archiver import IArchiver
-from mailman.testing.helpers import specialized_message_from_string as mfs
+from mailman.testing.helpers import (
+    LogFileMark, specialized_message_from_string as mfs)
 from mailman.testing.layers import ConfigLayer
 from tempfile import TemporaryDirectory
 from zope.interface import implementer
@@ -48,6 +50,16 @@ class TestArchiver:
         return 'http://example.com/link_to_message'
 
 
+@implementer(IArchiver)
+class BrokenArchiver:
+    name = 'broken'
+    is_enabled = True
+
+    @staticmethod
+    def permalink(mlist, msg):
+        raise RuntimeError('Cannot get permalink')
+
+
 
 class TestDecorate(unittest.TestCase):
     """Test the cook_headers handler."""
@@ -116,3 +128,46 @@ This is a test message.
         decorate.process(self._mlist, self._msg, {})
         self.assertIn('http://example.com/link_to_message',
                       self._msg.as_string())
+
+
+class TestBrokenPermalink(unittest.TestCase):
+    layer = ConfigLayer
+
+    def setUp(self):
+        self._mlist = create_list('a...@example.com')
+        self._msg = mfs("""\
+To: a...@example.com
+From: aper...@example.com
+Message-ID: <alpha>
+Content-Type: text/plain;
+
+This is a test message.
+""")
+        temporary_dir = TemporaryDirectory()
+        self.addCleanup(temporary_dir.cleanup)
+        template_dir = temporary_dir.name
+        config.push('archiver', """\
+        [paths.testing]
+        template_dir: {}
+        [archiver.testarchiver]
+        class: mailman.handlers.tests.test_decorate.BrokenArchiver
+        enable: yes
+        """.format(template_dir))
+        self.addCleanup(config.pop, 'archiver')
+
+    def test_broken_permalink(self):
+        # GL issue #208 - IArchive messages raise exceptions, breaking the
+        # rfc-2369 handler and shunting messages.
+        site_dir = os.path.join(config.TEMPLATE_DIR, 'site', 'en')
+        os.makedirs(site_dir)
+        footer_path = os.path.join(site_dir, 'myfooter.txt')
+        with open(footer_path, 'w', encoding='utf-8') as fp:
+            print('${broken_url}', file=fp)
+        self._mlist.footer_uri = 'mailman:///myfooter.txt'
+        self._mlist.preferred_language = 'en'
+        mark = LogFileMark('mailman.archiver')
+        decorate.process(self._mlist, self._msg, {})
+        log_messages = mark.read()
+        self.assertNotIn('http:', self._msg.as_string())
+        self.assertIn('Exception in "broken" archiver', log_messages)
+        self.assertIn('RuntimeError: Cannot get permalink', log_messages)


=====================================
src/mailman/handlers/tests/test_rfc_2369.py
=====================================
--- a/src/mailman/handlers/tests/test_rfc_2369.py
+++ b/src/mailman/handlers/tests/test_rfc_2369.py
@@ -28,7 +28,8 @@ from mailman.app.lifecycle import create_list
 from mailman.config import config
 from mailman.handlers import rfc_2369
 from mailman.interfaces.archiver import ArchivePolicy, IArchiver
-from mailman.testing.helpers import specialized_message_from_string as mfs
+from mailman.testing.helpers import (
+    LogFileMark, specialized_message_from_string as mfs)
 from mailman.testing.layers import ConfigLayer
 from urllib.parse import urljoin
 from zope.interface import implementer
@@ -56,6 +57,23 @@ class DummyArchiver:
         return None
 
 
+@implementer(IArchiver)
+class BrokenArchiver:
+    """An archiver that has some broken methods."""
+
+    name = 'broken'
+
+    def list_url(self, mlist):
+        raise RuntimeError('Cannot get list URL')
+
+    def permalink(self, mlist, msg):
+        raise RuntimeError('Cannot get permalink')
+
+    @staticmethod
+    def archive_message(mlist, message):
+        raise RuntimeError('Cannot archive message')
+
+
 
 class TestRFC2369(unittest.TestCase):
     """Test the rfc_2369 handler."""
@@ -123,3 +141,24 @@ Dummy text
         rfc_2369.process(self._mlist, self._msg, {})
         self.assertNotIn('List-Archive', self._msg)
         self.assertNotIn('Archived-At', self._msg)
+
+    def test_broken_archiver(self):
+        # GL issue #208 - IArchive messages raise exceptions, breaking the
+        # rfc-2369 handler and shunting messages.
+        config.push('archiver', """
+        [archiver.broken]
+        class: {}.BrokenArchiver
+        enable: yes
+        """.format(BrokenArchiver.__module__))
+        self.addCleanup(config.pop, 'archiver')
+        mark = LogFileMark('mailman.archiver')
+        rfc_2369.process(self._mlist, self._msg, {})
+        log_messages = mark.read()
+        # Because .list_url() was broken, there will be no List-Archive header.
+        self.assertIsNone(self._msg.get('list-archive'))
+        self.assertIn('Exception in "broken" archiver', log_messages)
+        self.assertIn('RuntimeError: Cannot get list URL', log_messages)
+        # Because .permalink() was broken, there will be no Archived-At header.
+        self.assertIsNone(self._msg.get('archived-at'))
+        self.assertIn('Exception in "broken" archiver', log_messages)
+        self.assertIn('RuntimeError: Cannot get permalink', log_messages)


=====================================
src/mailman/model/mailinglist.py
=====================================
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -605,7 +605,7 @@ class ListArchiverSet:
         for archiver_name in system_archivers:
             exists = store.query(ListArchiver).filter(
                 ListArchiver.mailing_list == mailing_list,
-                ListArchiver.name == archiver_name).first()
+                ListArchiver.name == archiver_name).one_or_none()
             if exists is None:
                 store.add(ListArchiver(mailing_list, archiver_name,
                                        system_archivers[archiver_name]))
@@ -621,7 +621,7 @@ class ListArchiverSet:
     def get(self, store, archiver_name):
         return store.query(ListArchiver).filter(
             ListArchiver.mailing_list == self._mailing_list,
-            ListArchiver.name == archiver_name).first()
+            ListArchiver.name == archiver_name).one_or_none()
 
 
 @implementer(IHeaderMatch)


=====================================
src/mailman/runners/archive.py
=====================================
--- a/src/mailman/runners/archive.py
+++ b/src/mailman/runners/archive.py
@@ -35,7 +35,7 @@ from mailman.utilities.datetime import RFC822_DATE_FMT, now
 from mailman.interfaces.mailinglist import IListArchiverSet
 
 
-log = logging.getLogger('mailman.error')
+log = logging.getLogger('mailman.archiver')
 
 
 
@@ -106,4 +106,5 @@ class ArchiveRunner(Runner):
             try:
                 archiver.system_archiver.archive_message(mlist, msg_copy)
             except Exception:
-                log.exception('Broken archiver: %s' % archiver.name)
+                log.exception('Exception in "{}" archiver'.format(
+                    archiver.name))


=====================================
src/mailman/runners/tests/test_archiver.py
=====================================
--- a/src/mailman/runners/tests/test_archiver.py
+++ b/src/mailman/runners/tests/test_archiver.py
@@ -32,7 +32,7 @@ from mailman.interfaces.archiver import IArchiver
 from mailman.interfaces.mailinglist import IListArchiverSet
 from mailman.runners.archive import ArchiveRunner
 from mailman.testing.helpers import (
-    configuration, make_testable_runner,
+    LogFileMark, configuration, get_queue_messages, make_testable_runner,
     specialized_message_from_string as mfs)
 from mailman.testing.layers import ConfigLayer
 from mailman.utilities.datetime import RFC822_DATE_FMT, factory, now
@@ -63,6 +63,23 @@ class DummyArchiver:
         return path
 
 
+@implementer(IArchiver)
+class BrokenArchiver:
+    """An archiver that has some broken methods."""
+
+    name = 'broken'
+
+    def list_url(self, mlist):
+        raise RuntimeError('Cannot get list URL')
+
+    def permalink(self, mlist, msg):
+        raise RuntimeError('Cannot get permalink')
+
+    @staticmethod
+    def archive_message(mlist, message):
+        raise RuntimeError('Cannot archive message')
+
+
 
 class TestArchiveRunner(unittest.TestCase):
     """Test the archive runner."""
@@ -77,6 +94,9 @@ class TestArchiveRunner(unittest.TestCase):
         [archiver.dummy]
         class: mailman.runners.tests.test_archiver.DummyArchiver
         enable: no
+        [archiver.broken]
+        class: mailman.runners.tests.test_archiver.BrokenArchiver
+        enable: no
         [archiver.prototype]
         enable: no
         [archiver.mhonarc]
@@ -84,6 +104,7 @@ class TestArchiveRunner(unittest.TestCase):
         [archiver.mail_archive]
         enable: no
         """)
+        self.addCleanup(config.pop, 'dummy')
         self._archiveq = config.switchboards['archive']
         self._msg = mfs("""\
 From: aper...@example.com
@@ -97,9 +118,6 @@ First post!
         self._runner = make_testable_runner(ArchiveRunner)
         IListArchiverSet(self._mlist).get('dummy').is_enabled = True
 
-    def tearDown(self):
-        config.pop('dummy')
-
     @configuration('archiver.dummy', enable='yes')
     def test_archive_runner(self):
         # Ensure that the archive runner ends up archiving the message.
@@ -247,3 +265,21 @@ First post!
             listid=self._mlist.list_id)
         self._runner.run()
         self.assertEqual(os.listdir(config.MESSAGES_DIR), [])
+
+    @configuration('archiver.broken', enable='yes')
+    def test_broken_archiver(self):
+        # GL issue #208 - IArchive messages raise exceptions, breaking the
+        # rfc-2369 handler and shunting messages.
+        mark = LogFileMark('mailman.archiver')
+        self._archiveq.enqueue(
+            self._msg, {},
+            listid=self._mlist.list_id,
+            received_time=now())
+        IListArchiverSet(self._mlist).get('broken').is_enabled = True
+        self._runner.run()
+        # The archiver is broken, so there are no messages on the file system,
+        # but there is a log message and the message was not shunted.
+        log_messages = mark.read()
+        self.assertIn('Exception in "broken" archiver', log_messages)
+        self.assertIn('RuntimeError: Cannot archive message', log_messages)
+        self.assertEqual(len(get_queue_messages('shunt')), 0)



View it on GitLab: 
https://gitlab.com/mailman/mailman/commit/472eb9765f4163aad785f483584ff607f01fdeeb
_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: 
https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to