summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/app/docs/chains.txt94
-rw-r--r--src/mailman/chains/accept.py12
-rw-r--r--src/mailman/chains/base.py18
-rw-r--r--src/mailman/chains/discard.py16
-rw-r--r--src/mailman/chains/hold.py9
-rw-r--r--src/mailman/chains/reject.py10
-rw-r--r--src/mailman/testing/helpers.py25
7 files changed, 122 insertions, 62 deletions
diff --git a/src/mailman/app/docs/chains.txt b/src/mailman/app/docs/chains.txt
index f9ed156b1..927004e27 100644
--- a/src/mailman/app/docs/chains.txt
+++ b/src/mailman/app/docs/chains.txt
@@ -35,18 +35,16 @@ The Discard chain simply throws the message away.
... An important message.
... """)
+ >>> def print_msgid(event):
+ ... print '{0}: {1}'.format(
+ ... event.chain.name.upper(), event.msg.get('message-id', 'n/a'))
+
>>> from mailman.core.chains import process
+ >>> from mailman.testing.helpers import event_subscribers
- # XXX This checks the vette log file because there is no other evidence
- # that this chain has done anything.
- >>> import os
- >>> fp = open(os.path.join(config.LOG_DIR, 'vette'))
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'discard')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... DISCARD: <first>
- <BLANKLINE>
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'discard')
+ DISCARD: <first>
The Reject chain
@@ -62,19 +60,18 @@ this action.
reject
>>> print chain.description
Reject/bounce a message and stop processing.
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'reject')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... REJECT: <first>
+
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'reject')
+ REJECT: <first>
The bounce message is now sitting in the Virgin queue.
- >>> virginq = config.switchboards['virgin']
- >>> len(virginq.files)
+ >>> from mailman.testing.helpers import get_queue_messages
+ >>> qfiles = get_queue_messages('virgin')
+ >>> len(qfiles)
1
- >>> qmsg, qdata = virginq.dequeue(virginq.files[0])
- >>> print qmsg.as_string()
+ >>> print qfiles[0].msg.as_string()
Subject: My first post
From: _xtest-owner@example.com
To: aperson@example.com
@@ -109,30 +106,20 @@ sender and the list moderators.
>>> print chain.description
Hold a message and stop processing.
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'hold')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... HOLD: _xtest@example.com post from aperson@example.com held,
- message-id=<first>: n/a
- <BLANKLINE>
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'hold')
+ HOLD: <first>
There are now two messages in the Virgin queue, one to the list moderators and
one to the original author.
- >>> len(virginq.files)
+ >>> qfiles = get_queue_messages('virgin', sort_on='to')
+ >>> len(qfiles)
2
- >>> qfiles = []
- >>> for filebase in virginq.files:
- ... qmsg, qdata = virginq.dequeue(filebase)
- ... virginq.finish(filebase)
- ... qfiles.append(qmsg)
- >>> from operator import itemgetter
- >>> qfiles.sort(key=itemgetter('to'))
This message is addressed to the mailing list moderators.
- >>> print qfiles[0].as_string()
+ >>> print qfiles[0].msg.as_string()
Subject: _xtest@example.com post from aperson@example.com requires approval
From: _xtest-owner@example.com
To: _xtest-owner@example.com
@@ -186,7 +173,7 @@ This message is addressed to the mailing list moderators.
This message is addressed to the sender of the message.
- >>> print qfiles[1].as_string()
+ >>> print qfiles[1].msg.as_string()
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
@@ -220,7 +207,7 @@ first item is a type code and the second item is a message id.
>>> import re
>>> cookie = None
- >>> for line in qfiles[1].get_payload().splitlines():
+ >>> for line in qfiles[1].msg.get_payload().splitlines():
... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line)
... if mo:
... cookie = mo.group('cookie')
@@ -269,17 +256,14 @@ processed and sent on to the list membership.
accept
>>> print chain.description
Accept a message.
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {}, 'accept')
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... ACCEPT: <first>
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {}, 'accept')
+ ACCEPT: <first>
- >>> pipelineq = config.switchboards['pipeline']
- >>> len(pipelineq.files)
+ >>> qfiles = get_queue_messages('pipeline')
+ >>> len(qfiles)
1
- >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
- >>> print qmsg.as_string()
+ >>> print qfiles[0].msg.as_string()
From: aperson@example.com
To: _xtest@example.com
Subject: My first post
@@ -314,14 +298,14 @@ the Hold handler from previous versions of Mailman.
The previously created message is innocuous enough that it should pass through
all default rules. This message will end up in the pipeline queue.
- >>> file_pos = fp.tell()
- >>> process(mlist, msg, {})
- >>> fp.seek(file_pos)
- >>> print 'LOG:', fp.read()
- LOG: ... ACCEPT: <first>
+ >>> with event_subscribers(print_msgid):
+ ... process(mlist, msg, {})
+ ACCEPT: <first>
- >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
- >>> print qmsg.as_string()
+ >>> qfiles = get_queue_messages('pipeline')
+ >>> len(qfiles)
+ 1
+ >>> print qfiles[0].msg.as_string()
From: aperson@example.com
To: _xtest@example.com
Subject: My first post
@@ -338,9 +322,9 @@ all default rules. This message will end up in the pipeline queue.
In addition, the message metadata now contains lists of all rules that have
hit and all rules that have missed.
- >>> sorted(qdata['rule_hits'])
+ >>> sorted(qfiles[0].msgdata['rule_hits'])
[]
- >>> for rule_name in sorted(qdata['rule_misses']):
+ >>> for rule_name in sorted(qfiles[0].msgdata['rule_misses']):
... print rule_name
administrivia
approved
diff --git a/src/mailman/chains/accept.py b/src/mailman/chains/accept.py
index 55d5c8c45..929d4b7da 100644
--- a/src/mailman/chains/accept.py
+++ b/src/mailman/chains/accept.py
@@ -22,11 +22,15 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'AcceptChain',
+ 'AcceptNotification',
]
+
import logging
-from mailman.chains.base import TerminalChainBase
+from zope.event import notify
+
+from mailman.chains.base import ChainNotification, TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
@@ -36,6 +40,11 @@ SEMISPACE = '; '
+class AcceptNotification(ChainNotification):
+ """A notification event signaling that a message is being accepted."""
+
+
+
class AcceptChain(TerminalChainBase):
"""Accept the message for posting."""
@@ -56,3 +65,4 @@ class AcceptChain(TerminalChainBase):
accept_queue = config.switchboards['pipeline']
accept_queue.enqueue(msg, msgdata)
log.info('ACCEPT: %s', msg.get('message-id', 'n/a'))
+ notify(AcceptNotification(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/base.py b/src/mailman/chains/base.py
index 04c156eae..e8b90537a 100644
--- a/src/mailman/chains/base.py
+++ b/src/mailman/chains/base.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'Chain',
+ 'ChainNotification',
'Link',
'TerminalChainBase',
]
@@ -50,7 +51,7 @@ class Link:
class TerminalChainBase:
"""A base chain that always matches and executes a method.
- The method is called 'process' and must be provided by the subclass.
+ The method is called '_process()' and must be provided by the subclass.
"""
implements(IChain, IChainIterator)
@@ -58,6 +59,10 @@ class TerminalChainBase:
"""Process the message for the given mailing list.
This must be overridden by subclasses.
+
+ :param mlist: The mailing list.
+ :param msg: The message.
+ :param msgdata: The message metadata.
"""
raise NotImplementedError
@@ -120,3 +125,14 @@ class ChainIterator:
def __iter__(self):
"""See `IChainIterator`."""
return self._chain.get_iterator()
+
+
+
+class ChainNotification:
+ """Base class for chain notification events."""
+
+ def __init__(self, mlist, msg, msgdata, chain):
+ self.mlist = mlist
+ self.msg = msg
+ self.msgdata = msgdata
+ self.chain = chain
diff --git a/src/mailman/chains/discard.py b/src/mailman/chains/discard.py
index a5eeabc89..1c4d396c6 100644
--- a/src/mailman/chains/discard.py
+++ b/src/mailman/chains/discard.py
@@ -22,12 +22,14 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'DiscardChain',
+ 'DiscardNotification',
]
import logging
+from zope.event import notify
-from mailman.chains.base import TerminalChainBase
+from mailman.chains.base import ChainNotification, TerminalChainBase
from mailman.core.i18n import _
@@ -35,6 +37,11 @@ log = logging.getLogger('mailman.vette')
+class DiscardNotification(ChainNotification):
+ """A notification event signaling that a message is being discarded."""
+
+
+
class DiscardChain(TerminalChainBase):
"""Discard a message."""
@@ -42,6 +49,11 @@ class DiscardChain(TerminalChainBase):
description = _('Discard a message and stop processing.')
def _process(self, mlist, msg, msgdata):
- """See `TerminalChainBase`."""
+ """See `TerminalChainBase`.
+
+ This writes a log message, fires a Zope event and then throws the
+ message away.
+ """
log.info('DISCARD: %s', msg.get('message-id', 'n/a'))
+ notify(DiscardNotification(mlist, msg, msgdata, self))
# Nothing more needs to happen.
diff --git a/src/mailman/chains/hold.py b/src/mailman/chains/hold.py
index 3491f2e91..6755fe904 100644
--- a/src/mailman/chains/hold.py
+++ b/src/mailman/chains/hold.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'HoldChain',
+ 'HoldNotification',
]
@@ -31,12 +32,13 @@ from email.mime.message import MIMEMessage
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from zope.component import getUtility
+from zope.event import notify
from zope.interface import implements
from mailman.Utils import maketext, oneline, wrap
from mailman.app.moderator import hold_message
from mailman.app.replybot import can_acknowledge
-from mailman.chains.base import TerminalChainBase
+from mailman.chains.base import ChainNotification, TerminalChainBase
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import UserNotification
@@ -56,6 +58,10 @@ class HeldMessagePendable(dict):
PEND_KEY = 'held message'
+class HoldNotification(ChainNotification):
+ """A notification event signaling that a message is being held."""
+
+
def autorespond_to_sender(mlist, sender, lang=None):
"""Should Mailman automatically respond to this sender?
@@ -241,3 +247,4 @@ also appear in the first line of the body of the reply.""")),
log.info('HOLD: %s post from %s held, message-id=%s: %s',
mlist.fqdn_listname, msg.sender,
msg.get('message-id', 'n/a'), reason)
+ notify(HoldNotification(mlist, msg, msgdata, self))
diff --git a/src/mailman/chains/reject.py b/src/mailman/chains/reject.py
index 92720d931..3dbf21869 100644
--- a/src/mailman/chains/reject.py
+++ b/src/mailman/chains/reject.py
@@ -22,13 +22,15 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'RejectChain',
+ 'RejectNotification',
]
import logging
+from zope.event import notify
from mailman.app.bounces import bounce_message
-from mailman.chains.base import TerminalChainBase
+from mailman.chains.base import ChainNotification, TerminalChainBase
from mailman.core.i18n import _
@@ -37,6 +39,11 @@ SEMISPACE = '; '
+class RejectNotification(ChainNotification):
+ """A notification event signaling that a message is being rejected."""
+
+
+
class RejectChain(TerminalChainBase):
"""Reject/bounce a message."""
@@ -57,3 +64,4 @@ class RejectChain(TerminalChainBase):
# XXX Exception/reason
bounce_message(mlist, msg)
log.info('REJECT: %s', msg.get('message-id', 'n/a'))
+ notify(RejectNotification(mlist, msg, msgdata, self))
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 0897d1d74..b9936ac71 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -23,6 +23,7 @@ __metaclass__ = type
__all__ = [
'TestableMaster',
'digest_mbox',
+ 'event_subscribers',
'get_lmtp_client',
'get_queue_messages',
'make_testable_runner',
@@ -40,6 +41,9 @@ import smtplib
import datetime
import threading
+from contextlib import contextmanager
+from zope import event
+
from mailman.bin.master import Loop as Master
from mailman.config import config
from mailman.utilities.mailbox import Mailbox
@@ -88,10 +92,12 @@ class _Bag:
setattr(self, key, value)
-def get_queue_messages(queue_name):
+def get_queue_messages(queue_name, sort_on=None):
"""Return and clear all the messages in the given queue.
:param queue_name: A string naming a queue.
+ :param sort_on: The message header to sort on. If None (the default),
+ no sorting is performed.
:return: A list of 2-tuples where each item contains the message and
message metadata.
"""
@@ -101,6 +107,8 @@ def get_queue_messages(queue_name):
msg, msgdata = queue.dequeue(filebase)
messages.append(_Bag(msg=msg, msgdata=msgdata))
queue.finish(filebase)
+ if sort_on is not None:
+ messages.sort(key=lambda item: item.msg[sort_on])
return messages
@@ -229,3 +237,18 @@ def wait_for_webservice():
time.sleep(0.1)
else:
break
+
+
+
+@contextmanager
+def event_subscribers(*subscribers):
+ """Temporarily set the Zope event subscribers list.
+
+ :param subscribers: A sequence of event subscribers.
+ :type subscribers: sequence of callables, each receiving one argument, the
+ event.
+ """
+ old_subscribers = event.subscribers[:]
+ event.subscribers = list(subscribers)
+ yield
+ event.subscribers[:] = old_subscribers