summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2012-04-01 12:52:14 -0600
committerBarry Warsaw2012-04-01 12:52:14 -0600
commit588d80d30aca30d9e70551e169aaa0fbb7e09c9d (patch)
tree33bd30d6a703abaed16b0e4295692292496a3e4a
parent664d7d73e13b649f003f51a727bf39fdfdc2ab65 (diff)
downloadmailman-588d80d30aca30d9e70551e169aaa0fbb7e09c9d.tar.gz
mailman-588d80d30aca30d9e70551e169aaa0fbb7e09c9d.tar.zst
mailman-588d80d30aca30d9e70551e169aaa0fbb7e09c9d.zip
-rw-r--r--src/mailman/config/schema.cfg10
-rw-r--r--src/mailman/docs/NEWS.rst3
-rw-r--r--src/mailman/runners/nntp.py66
-rw-r--r--src/mailman/runners/tests/test_nntp.py152
4 files changed, 197 insertions, 34 deletions
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index 88f378159..44cbf6f4e 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -601,13 +601,15 @@ plain_digest_keep_headers:
[nntp]
# Set these variables if you need to authenticate to your NNTP server for
-# Usenet posting or reading. If no authentication is necessary, specify None
-# for both variables.
-username:
+# Usenet posting or reading. Leave these blank if no authentication is
+# necessary.
+user:
password:
-# Set this if you have an NNTP server you prefer gatewayed lists to use.
+# Host and port of the NNTP server to connect to. Leave these blank to use
+# the default localhost:119.
host:
+port:
# This controls how headers must be cleansed in order to be accepted by your
# NNTP server. Some servers like INN reject messages containing prohibited
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 6a10dcade..13bb919c1 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -32,6 +32,9 @@ Configuration
every `[archiver.<name>]` section. These are used to determine under what
circumstances a message destined for a specific archiver should have its
`Date:` header clobbered. (LP: #963612)
+ * Configuration schema variable changes:
+ [nntp]username -> [nntp]user
+ [nntp]port (added)
Documentation
-------------
diff --git a/src/mailman/runners/nntp.py b/src/mailman/runners/nntp.py
index 11c3942f0..8339c735e 100644
--- a/src/mailman/runners/nntp.py
+++ b/src/mailman/runners/nntp.py
@@ -17,6 +17,14 @@
"""NNTP runner."""
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'NNTPRunner',
+ ]
+
+
import re
import email
import socket
@@ -24,7 +32,6 @@ import logging
import nntplib
from cStringIO import StringIO
-from lazr.config import as_host_port
from mailman.config import config
from mailman.core.runner import Runner
@@ -50,39 +57,46 @@ mcre = re.compile(r"""
-class NewsRunner(Runner):
+class NNTPRunner(Runner):
def _dispose(self, mlist, msg, msgdata):
+ # Get NNTP server connection information.
+ host = config.nntp.host.strip()
+ port = config.nntp.port.strip()
+ if len(port) == 0:
+ port = 119
+ else:
+ try:
+ port = int(port)
+ except (TypeError, ValueError):
+ log.exception('Bad [nntp]port value: {0}'.format(port))
+ port = 119
# Make sure we have the most up-to-date state
- mlist.Load()
if not msgdata.get('prepped'):
prepare_message(mlist, msg, msgdata)
+ # Flatten the message object, sticking it in a StringIO object
+ fp = StringIO(msg.as_string())
+ conn = None
try:
- # Flatten the message object, sticking it in a StringIO object
- fp = StringIO(msg.as_string())
- conn = None
- try:
- try:
- nntp_host, nntp_port = as_host_port(
- mlist.nntp_host, default_port=119)
- conn = nntplib.NNTP(nntp_host, nntp_port,
- readermode=True,
- user=config.nntp.username,
- password=config.nntp.password)
- conn.post(fp)
- except nntplib.error_temp, e:
- log.error('(NNTPDirect) NNTP error for list "%s": %s',
- mlist.internal_name(), e)
- except socket.error, e:
- log.error('(NNTPDirect) socket error for list "%s": %s',
- mlist.internal_name(), e)
- finally:
- if conn:
- conn.quit()
- except Exception, e:
+ conn = nntplib.NNTP(host, port,
+ readermode=True,
+ user=config.nntp.user,
+ password=config.nntp.password)
+ conn.post(fp)
+ except nntplib.error_temp:
+ log.exception('{0} NNTP error for {1}'.format(
+ msg.get('message-id', 'n/a'), mlist.fqdn_listname))
+ except socket.error:
+ log.exception('{0} NNTP socket error for {1}'.format(
+ msg.get('message-id', 'n/a'), mlist.fqdn_listname))
+ except Exception:
# Some other exception occurred, which we definitely did not
# expect, so set this message up for requeuing.
- self._log(e)
+ log.exception('{0} NNTP unexpected exception for {1}'.format(
+ msg.get('message-id', 'n/a'), mlist.fqdn_listname))
return True
+ finally:
+ if conn:
+ conn.quit()
return False
diff --git a/src/mailman/runners/tests/test_nntp.py b/src/mailman/runners/tests/test_nntp.py
index dcd3cfac4..426e829d8 100644
--- a/src/mailman/runners/tests/test_nntp.py
+++ b/src/mailman/runners/tests/test_nntp.py
@@ -21,24 +21,32 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
- 'TestNNTP',
+ 'TestPrepareMessage',
+ 'TestNNTPRunner',
]
+import mock
+import socket
+import nntplib
import unittest
from mailman.app.lifecycle import create_list
+from mailman.config import config
from mailman.interfaces.nntp import NewsModeration
from mailman.runners import nntp
from mailman.testing.helpers import (
+ LogFileMark,
configuration,
+ get_queue_messages,
+ make_testable_runner,
specialized_message_from_string as mfs)
from mailman.testing.layers import ConfigLayer
-class TestNNTP(unittest.TestCase):
- """Test the NNTP runner and related utilities."""
+class TestPrepareMessage(unittest.TestCase):
+ """Test message preparation."""
layer = ConfigLayer
@@ -202,7 +210,7 @@ Testing
self.assertEqual(tos[0], 'test@example.com')
original_tos = self._msg.get_all('x-original-to')
self.assertEqual(len(original_tos), 2)
- self.assertEqual(original_tos,
+ self.assertEqual(original_tos,
['test@example.org', 'test@example.net'])
fakes = self._msg.get_all('x-fake')
self.assertEqual(len(fakes), 1)
@@ -224,3 +232,139 @@ Testing
fakes = self._msg.get_all('x-fake')
self.assertEqual(len(fakes), 3)
self.assertEqual(fakes, ['one', 'two', 'three'])
+
+
+
+class TestNNTPRunner(unittest.TestCase):
+ """The NNTP runner hands messages off to the NNTP server."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._mlist.linked_newsgroup = 'example.test'
+ self._msg = mfs("""\
+From: anne@example.com
+To: test@example.com
+Subject: A newsgroup posting
+Message-ID: <ant>
+
+Testing
+""")
+ self._runner = make_testable_runner(nntp.NNTPRunner, 'nntp')
+ self._nntpq = config.switchboards['nntp']
+
+ @mock.patch('nntplib.NNTP')
+ def test_connect(self, class_mock):
+ # Test connection to the NNTP server with default values.
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._runner.run()
+ class_mock.assert_called_once_with(
+ '', 119, user='', password='', readermode=True)
+
+ @configuration('nntp', user='alpha', password='beta',
+ host='nntp.example.com', port='2112')
+ @mock.patch('nntplib.NNTP')
+ def test_connect_with_configuration(self, class_mock):
+ # Test connection to the NNTP server with specific values.
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._runner.run()
+ class_mock.assert_called_once_with(
+ 'nntp.example.com', 2112,
+ user='alpha', password='beta', readermode=True)
+
+ @mock.patch('nntplib.NNTP')
+ def test_post(self, class_mock):
+ # Test that the message is posted to the NNTP server.
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._runner.run()
+ # Get the mocked instance, which was used in the runner.
+ conn_mock = class_mock()
+ # The connection object's post() method was called once with a
+ # file-like object containing the message's bytes. Read those bytes
+ # and make some simple checks that the message is what we expected.
+ args = conn_mock.post.call_args
+ # One positional argument.
+ self.assertEqual(len(args[0]), 1)
+ # No keyword arguments.
+ self.assertEqual(len(args[1]), 0)
+ msg = mfs(args[0][0].read())
+ self.assertEqual(msg['subject'], 'A newsgroup posting')
+
+ @mock.patch('nntplib.NNTP')
+ def test_connection_got_quit(self, class_mock):
+ # The NNTP connection gets closed after a successful post.
+ # Test that the message is posted to the NNTP server.
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._runner.run()
+ # Get the mocked instance, which was used in the runner.
+ conn_mock = class_mock()
+ # The connection object's post() method was called once with a
+ # file-like object containing the message's bytes. Read those bytes
+ # and make some simple checks that the message is what we expected.
+ conn_mock.quit.assert_called_once_with()
+
+ @mock.patch('nntplib.NNTP', side_effect=nntplib.error_temp)
+ def test_connect_with_nntplib_failure(self, class_mock):
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ mark = LogFileMark('mailman.error')
+ self._runner.run()
+ log_message = mark.readline()[:-1]
+ self.assertTrue(log_message.endswith(
+ 'NNTP error for test@example.com'))
+
+ @mock.patch('nntplib.NNTP', side_effect=socket.error)
+ def test_connect_with_socket_failure(self, class_mock):
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ mark = LogFileMark('mailman.error')
+ self._runner.run()
+ log_message = mark.readline()[:-1]
+ self.assertTrue(log_message.endswith(
+ 'NNTP socket error for test@example.com'))
+
+ @mock.patch('nntplib.NNTP', side_effect=RuntimeError)
+ def test_connect_with_other_failure(self, class_mock):
+ # In this failure mode, the message stays queued, so we can only run
+ # the nntp runner once.
+ def once(runner):
+ # I.e. stop immediately, since the queue will not be empty.
+ return True
+ runner = make_testable_runner(nntp.NNTPRunner, 'nntp', predicate=once)
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ mark = LogFileMark('mailman.error')
+ runner.run()
+ log_message = mark.readline()[:-1]
+ self.assertTrue(log_message.endswith(
+ 'NNTP unexpected exception for test@example.com'))
+ messages = get_queue_messages('nntp')
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].msgdata['listname'], 'test@example.com')
+ self.assertEqual(messages[0].msg['subject'], 'A newsgroup posting')
+
+ @mock.patch('nntplib.NNTP', side_effect=nntplib.error_temp)
+ def test_connection_never_gets_quit_after_failures(self, class_mock):
+ # The NNTP connection doesn't get closed after a unsuccessful
+ # connection, since there's nothing to close.
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._runner.run()
+ # Get the mocked instance, which was used in the runner. Turn off the
+ # exception raising side effect first though!
+ class_mock.side_effect = None
+ conn_mock = class_mock()
+ # The connection object's post() method was called once with a
+ # file-like object containing the message's bytes. Read those bytes
+ # and make some simple checks that the message is what we expected.
+ self.assertEqual(conn_mock.quit.call_count, 0)
+
+ @mock.patch('nntplib.NNTP')
+ def test_connection_got_quit_after_post_failure(self, class_mock):
+ # The NNTP connection does get closed after a unsuccessful post.
+ # Add a side-effect to the instance mock's .post() method.
+ conn_mock = class_mock()
+ conn_mock.post.side_effect = nntplib.error_temp
+ self._nntpq.enqueue(self._msg, {}, listname='test@example.com')
+ self._runner.run()
+ # The connection object's post() method was called once with a
+ # file-like object containing the message's bytes. Read those bytes
+ # and make some simple checks that the message is what we expected.
+ conn_mock.quit.assert_called_once_with()