Barry Warsaw pushed to branch release-3.0 at mailman / Mailman
Commits:
06778b9d by Barry Warsaw at 2016-03-09T18:31:10-05:00
Fix cross-posting held on more than one list.
Back port to release-3.0 branch.
Closes #176
Also:
* IMessageStore no longer raises a ValueError if the Message-ID already
exists in the store; it just returns None.
* The internal handle_message() function no longer takes a `preserve`
argument, since messages are never removed from the IMessageStore.
- - - - -
cdc89932 by Barry Warsaw at 2016-03-09T18:46:55-05:00
It's X-Message-Id-Hash in 3.0.x
- - - - -
8 changed files:
- src/mailman/app/docs/moderator.rst
- src/mailman/app/moderator.py
- src/mailman/app/tests/test_moderation.py
- src/mailman/chains/tests/test_hold.py
- src/mailman/docs/NEWS.rst
- src/mailman/interfaces/messages.py
- src/mailman/model/messagestore.py
- src/mailman/model/tests/test_messagestore.py
Changes:
=====================================
src/mailman/app/docs/moderator.rst
=====================================
--- a/src/mailman/app/docs/moderator.rst
+++ b/src/mailman/app/docs/moderator.rst
@@ -171,36 +171,11 @@ however the message metadata indicates that the message
has been approved.
version : 3
-Preserving and forwarding the message
--------------------------------------
-
-In addition to any of the above dispositions, the message can also be
-preserved for further study. Ordinarily the message is removed from the
-global message store after its disposition (though approved messages may be
-re-added to the message store later). When handling a message, we can ask for
-a copy to be preserve, which skips deleting the message from the storage.
-::
-
- >>> msg = message_from_string("""\
- ... From: [email protected]
- ... To: [email protected]
- ... Subject: Something important
- ... Message-ID: <dolphin>
- ...
- ... Here's something important about our mailing list.
- ... """)
- >>> id = hold_message(mlist, msg, {}, 'Needs approval')
- >>> handle_message(mlist, id, Action.discard, preserve=True)
-
- >>> from mailman.interfaces.messages import IMessageStore
- >>> from zope.component import getUtility
- >>> message_store = getUtility(IMessageStore)
- >>> print(message_store.get_message_by_id('<dolphin>')['message-id'])
- <dolphin>
+Forwarding the message
+----------------------
-Orthogonal to preservation, the message can also be forwarded to another
-address. This is helpful for getting the message into the inbox of one of the
-moderators.
+The message can be forwarded to another address. This is helpful for getting
+the message into the inbox of one of the moderators.
::
>>> msg = message_from_string("""\
=====================================
src/mailman/app/moderator.py
=====================================
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -103,8 +103,7 @@ def hold_message(mlist, msg, msgdata=None, reason=None):
-def handle_message(mlist, id, action,
- comment=None, preserve=False, forward=None):
+def handle_message(mlist, id, action, comment=None, forward=None):
message_store = getUtility(IMessageStore)
requestdb = IListRequests(mlist)
key, msgdata = requestdb.get_request(id)
@@ -113,9 +112,10 @@ def handle_message(mlist, id, action,
message_id = msgdata['_mod_message_id']
sender = msgdata['_mod_sender']
subject = msgdata['_mod_subject']
+ keep = False
if action in (Action.defer, Action.hold):
# Nothing to do, but preserve the message for later.
- preserve = True
+ keep = True
elif action is Action.discard:
rejection = 'Discarded'
elif action is Action.reject:
@@ -179,9 +179,8 @@ def handle_message(mlist, id, action,
fmsg.set_type('message/rfc822')
fmsg.attach(msg)
fmsg.send(mlist)
- # Delete the message from the message store if it is not being preserved.
- if not preserve:
- message_store.delete_message(message_id)
+ # Delete the request if it's not being kept.
+ if not keep:
requestdb.delete_request(id)
# Log the rejection
if rejection:
=====================================
src/mailman/app/tests/test_moderation.py
=====================================
--- a/src/mailman/app/tests/test_moderation.py
+++ b/src/mailman/app/tests/test_moderation.py
@@ -122,31 +122,12 @@ Message-ID: <alpha>
key, data = self._request_db.get_request(request_id)
self.assertEqual(data['received_time'], received_time)
- def test_non_preserving_disposition(self):
- # By default, disposed messages are not preserved.
- request_id = hold_message(self._mlist, self._msg)
- handle_message(self._mlist, request_id, Action.discard)
- message_store = getUtility(IMessageStore)
- self.assertIsNone(message_store.get_message_by_id('<alpha>'))
-
- def test_preserving_disposition(self):
- # Preserving a message keeps it in the store.
- request_id = hold_message(self._mlist, self._msg)
- handle_message(self._mlist, request_id, Action.discard, preserve=True)
- message_store = getUtility(IMessageStore)
- preserved_message = message_store.get_message_by_id('<alpha>')
- self.assertEqual(preserved_message['message-id'], '<alpha>')
-
- def test_preserve_and_forward(self):
- # We can both preserve and forward the message.
+ def test_forward(self):
+ # We can forward the message to an email address.
request_id = hold_message(self._mlist, self._msg)
handle_message(self._mlist, request_id, Action.discard,
- preserve=True, forward=['[email protected]'])
- # The message is preserved in the store.
- message_store = getUtility(IMessageStore)
- preserved_message = message_store.get_message_by_id('<alpha>')
- self.assertEqual(preserved_message['message-id'], '<alpha>')
- # And the forwarded message lives in the virgin queue.
+ forward=['[email protected]'])
+ # The forwarded message lives in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
self.assertEqual(str(messages[0].msg['subject']),
@@ -163,6 +144,15 @@ Message-ID: <alpha>
handle_message(self._mlist, request_id, Action.discard)
self.assertEqual(self._request_db.count, 0)
+ def test_handled_message_stays_in_store(self):
+ # The message is still available in the store, even when it's been
+ # disposed of.
+ request_id = hold_message(self._mlist, self._msg)
+ handle_message(self._mlist, request_id, Action.discard)
+ self.assertEqual(self._request_db.count, 0)
+ message = getUtility(IMessageStore).get_message_by_id('<alpha>')
+ self.assertEqual(message['subject'], 'hold me')
+
class TestUnsubscription(unittest.TestCase):
=====================================
src/mailman/chains/tests/test_hold.py
=====================================
--- a/src/mailman/chains/tests/test_hold.py
+++ b/src/mailman/chains/tests/test_hold.py
@@ -29,6 +29,7 @@ from mailman.app.lifecycle import create_list
from mailman.chains.hold import autorespond_to_sender
from mailman.core.chains import process as process_chain
from mailman.interfaces.autorespond import IAutoResponseSet, Response
+from mailman.interfaces.messages import IMessageStore
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
LogFileMark, configuration, get_queue_messages,
@@ -164,3 +165,37 @@ A message body.
self.assertEqual(
msg['Subject'],
'[email protected] post from [email protected] requires approval')
+
+ def test_hold_chain_crosspost(self):
+ mlist2 = create_list('[email protected]')
+ msg = mfs("""\
+From: [email protected]
+To: [email protected], [email protected]
+Subject: A message
+Message-ID: <ant>
+MIME-Version: 1.0
+
+A message body.
+""")
+ process_chain(self._mlist, msg, {}, start_chain='hold')
+ process_chain(mlist2, msg, {}, start_chain='hold')
+ items = get_queue_messages('virgin')
+ # There are four items in the virgin queue. Two of them are for the
+ # list owners who need to moderate the held message, and the other is
+ # for anne telling her that her message was held for approval.
+ self.assertEqual(len(items), 4)
+ anne_froms = set()
+ owner_tos = set()
+ for item in items:
+ if item.msg['to'] == '[email protected]':
+ anne_froms.add(item.msg['from'])
+ else:
+ owner_tos.add(item.msg['to'])
+ self.assertEqual(anne_froms, set(['[email protected]',
+ '[email protected]']))
+ self.assertEqual(owner_tos, set(['[email protected]',
+ '[email protected]']))
+ # And the message appears in the store.
+ messages = list(getUtility(IMessageStore).messages)
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0]['message-id'], '<ant>')
=====================================
src/mailman/docs/NEWS.rst
=====================================
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -20,6 +20,7 @@ Bugs
* Trying to subscribe an address as a list owner (or moderator or nonmember)
which is already subscribed with that role produces a server error.
Originally given by Anirudh Dahiya. (Closes #198)
+ * Cross-posting messages held on both lists no longer fails. (Closes #176)
3.0.2 -- "Show Don't Tell"
=====================================
src/mailman/interfaces/messages.py
=====================================
--- a/src/mailman/interfaces/messages.py
+++ b/src/mailman/interfaces/messages.py
@@ -65,10 +65,11 @@ class IMessageStore(Interface):
:param message: An email.message.Message instance containing at least
a unique Message-ID header. The message will be given an
X-Message-ID-Hash header, overriding any existing such header.
- :returns: The calculated X-Message-ID-Hash header.
+ If the message already exists in the store, it is not added again.
+ :returns: The calculated X-Message-ID-Hash header or None if the
+ message already exists in the store.
+ :rtype: str or None
:raises ValueError: if the message is missing a Message-ID header.
- The storage service is also allowed to raise this exception if it
- find, but disallows collisions.
"""
def get_message_by_id(message_id):
=====================================
src/mailman/model/messagestore.py
=====================================
--- a/src/mailman/model/messagestore.py
+++ b/src/mailman/model/messagestore.py
@@ -57,13 +57,11 @@ class MessageStore:
message_id = message_ids[0]
if isinstance(message_id, bytes):
message_id = message_id.decode('ascii')
- # Complain if the Message-ID already exists in the storage.
+ # If the Message-ID already exists in the store, don't store it again.
existing = store.query(Message).filter(
Message.message_id == message_id).first()
if existing is not None:
- raise ValueError(
- 'Message ID already exists in message store: {0}'.format(
- message_id))
+ return None
shaobj = hashlib.sha1(message_id.encode('utf-8'))
hash32 = base64.b32encode(shaobj.digest()).decode('utf-8')
del message['X-Message-ID-Hash']
=====================================
src/mailman/model/tests/test_messagestore.py
=====================================
--- a/src/mailman/model/tests/test_messagestore.py
+++ b/src/mailman/model/tests/test_messagestore.py
@@ -100,3 +100,22 @@ Message-ID: <ant>
os.remove(os.path.join(config.MESSAGES_DIR, row.path))
self._store.delete_message('<ant>')
self.assertEqual(len(list(self._store.messages)), 0)
+
+ def test_add_message_duplicate_okay(self):
+ msg = mfs("""\
+Subject: Once
+Message-ID: <ant>
+
+""")
+ hash32 = self._store.add(msg)
+ stored_msg = self._store.get_message_by_id('<ant>')
+ self.assertEqual(msg['subject'], stored_msg['subject'])
+ self.assertEqual(msg['x-message-id-hash'], hash32)
+ # A second insertion, even if headers change, does not store the
+ # message twice.
+ del msg['subject']
+ msg['Subject'] = 'Twice'
+ hash32 = self._store.add(msg)
+ stored_msg = self._store.get_message_by_id('<ant>')
+ self.assertNotEqual(msg['subject'], stored_msg['subject'])
+ self.assertIsNone(hash32)
View it on GitLab:
https://gitlab.com/mailman/mailman/compare/d251722399a5470fa5c4ea6b9325129d8895b135...cdc89932bbe4a5b318abfd146a68c1ca9fe3fe72
_______________________________________________
Mailman-checkins mailing list
[email protected]
Unsubscribe:
https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org