summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2012-01-30 10:37:16 -0500
committerBarry Warsaw2012-01-30 10:37:16 -0500
commitdf6ec9f2960f1de89acc17ec28c3fe170a32e1dd (patch)
treec212f56d3cb6510362b9f21c8dd625aec450bdd7 /src
parent78b9ea398e6671d94f958e625b640383f1d43a75 (diff)
downloadmailman-df6ec9f2960f1de89acc17ec28c3fe170a32e1dd.tar.gz
mailman-df6ec9f2960f1de89acc17ec28c3fe170a32e1dd.tar.zst
mailman-df6ec9f2960f1de89acc17ec28c3fe170a32e1dd.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/docs/chains.rst4
-rw-r--r--src/mailman/app/moderator.py18
-rw-r--r--src/mailman/app/tests/test_moderation.py14
-rw-r--r--src/mailman/bin/checkdbs.py9
-rw-r--r--src/mailman/config/configure.zcml15
-rw-r--r--src/mailman/docs/NEWS.rst13
-rw-r--r--src/mailman/interfaces/requests.py19
-rw-r--r--src/mailman/model/docs/requests.rst5
-rw-r--r--src/mailman/model/requests.py17
-rw-r--r--src/mailman/model/tests/test_listmanager.py4
-rw-r--r--src/mailman/model/tests/test_requests.py66
-rw-r--r--src/mailman/rest/docs/moderation.rst131
-rw-r--r--src/mailman/rest/lists.py8
-rw-r--r--src/mailman/rest/moderation.py116
-rw-r--r--src/mailman/rest/tests/test_moderation.py107
-rw-r--r--src/mailman/rest/tests/test_users.py2
16 files changed, 493 insertions, 55 deletions
diff --git a/src/mailman/app/docs/chains.rst b/src/mailman/app/docs/chains.rst
index 8a8ac0cc2..7096cc17c 100644
--- a/src/mailman/app/docs/chains.rst
+++ b/src/mailman/app/docs/chains.rst
@@ -226,8 +226,8 @@ first item is a type code and the second item is a message id.
The message itself is held in the message store.
::
- >>> from mailman.interfaces.requests import IRequests
- >>> list_requests = getUtility(IRequests).get_list_requests(mlist)
+ >>> from mailman.interfaces.requests import IListRequests
+ >>> list_requests = IListRequests(mlist)
>>> rkey, rdata = list_requests.get_request(data['id'])
>>> from mailman.interfaces.messages import IMessageStore
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index 11eccd495..3c3603619 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -49,7 +49,7 @@ from mailman.interfaces.listmanager import ListDeletingEvent
from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, NotAMemberError)
from mailman.interfaces.messages import IMessageStore
-from mailman.interfaces.requests import IRequests, RequestType
+from mailman.interfaces.requests import IListRequests, RequestType
from mailman.utilities.datetime import now
from mailman.utilities.i18n import make
@@ -100,7 +100,7 @@ def hold_message(mlist, msg, msgdata=None, reason=None):
msgdata['_mod_reason'] = reason
msgdata['_mod_hold_date'] = now().isoformat()
# Now hold this request. We'll use the message_id as the key.
- requestsdb = getUtility(IRequests).get_list_requests(mlist)
+ requestsdb = IListRequests(mlist)
request_id = requestsdb.hold_request(
RequestType.held_message, message_id, msgdata)
return request_id
@@ -110,14 +110,14 @@ def hold_message(mlist, msg, msgdata=None, reason=None):
def handle_message(mlist, id, action,
comment=None, preserve=False, forward=None):
message_store = getUtility(IMessageStore)
- requestdb = getUtility(IRequests).get_list_requests(mlist)
+ requestdb = IListRequests(mlist)
key, msgdata = requestdb.get_request(id)
# Handle the action.
rejection = None
message_id = msgdata['_mod_message_id']
sender = msgdata['_mod_sender']
subject = msgdata['_mod_subject']
- if action is Action.defer:
+ if action in (Action.defer, Action.hold):
# Nothing to do, but preserve the message for later.
preserve = True
elif action is Action.discard:
@@ -205,7 +205,7 @@ def hold_subscription(mlist, address, realname, password, mode, language):
delivery_mode=str(mode),
language=language)
# Now hold this request. We'll use the address as the key.
- requestsdb = getUtility(IRequests).get_list_requests(mlist)
+ requestsdb = IListRequests(mlist)
request_id = requestsdb.hold_request(
RequestType.subscription, address, data)
vlog.info('%s: held subscription request from %s',
@@ -231,7 +231,7 @@ def hold_subscription(mlist, address, realname, password, mode, language):
def handle_subscription(mlist, id, action, comment=None):
- requestdb = getUtility(IRequests).get_list_requests(mlist)
+ requestdb = IListRequests(mlist)
if action is Action.defer:
# Nothing to do.
return
@@ -277,7 +277,7 @@ def handle_subscription(mlist, id, action, comment=None):
def hold_unsubscription(mlist, address):
data = dict(address=address)
- requestsdb = getUtility(IRequests).get_list_requests(mlist)
+ requestsdb = IListRequests(mlist)
request_id = requestsdb.hold_request(
RequestType.unsubscription, address, data)
vlog.info('%s: held unsubscription request from %s',
@@ -303,7 +303,7 @@ def hold_unsubscription(mlist, address):
def handle_unsubscription(mlist, id, action, comment=None):
- requestdb = getUtility(IRequests).get_list_requests(mlist)
+ requestdb = IListRequests(mlist)
key, data = requestdb.get_request(id)
address = data['address']
if action is Action.defer:
@@ -368,6 +368,6 @@ def handle_ListDeletingEvent(event):
return
# Get the held requests database for the mailing list. Since the mailing
# list is about to get deleted, we can delete all associated requests.
- requestsdb = getUtility(IRequests).get_list_requests(event.mailing_list)
+ requestsdb = IListRequests(event.mailing_list)
for request in requestsdb.held_requests:
requestsdb.delete_request(request.id)
diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py
index 2a75043d5..262aa4480 100644
--- a/src/mailman/app/tests/test_moderation.py
+++ b/src/mailman/app/tests/test_moderation.py
@@ -29,6 +29,7 @@ import unittest
from mailman.app.lifecycle import create_list
from mailman.app.moderator import handle_message, hold_message
from mailman.interfaces.action import Action
+from mailman.interfaces.requests import IListRequests
from mailman.runners.incoming import IncomingRunner
from mailman.runners.outgoing import OutgoingRunner
from mailman.runners.pipeline import PipelineRunner
@@ -95,3 +96,16 @@ Message-ID: <alpha>
# envelope.
self.assertEqual(message['x-mailfrom'], 'test-bounces@example.com')
self.assertEqual(message['x-rcptto'], 'bart@example.com')
+
+ def test_hold_action_alias_for_defer(self):
+ # In handle_message(), the 'hold' action is the same as 'defer' for
+ # purposes of this API.
+ request_id = hold_message(self._mlist, self._msg)
+ handle_message(self._mlist, request_id, Action.defer)
+ # The message is still in the pending requests.
+ requests_db = IListRequests(self._mlist)
+ key, data = requests_db.get_request(request_id)
+ self.assertEqual(key, '<alpha>')
+ handle_message(self._mlist, request_id, Action.hold)
+ key, data = requests_db.get_request(request_id)
+ self.assertEqual(key, '<alpha>')
diff --git a/src/mailman/bin/checkdbs.py b/src/mailman/bin/checkdbs.py
index 023dddbda..42d5fc091 100644
--- a/src/mailman/bin/checkdbs.py
+++ b/src/mailman/bin/checkdbs.py
@@ -20,7 +20,6 @@ import time
import optparse
from email.Charset import Charset
-from zope.component import getUtility
from mailman import MailList
from mailman import Utils
@@ -29,7 +28,7 @@ from mailman.configuration import config
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.initialize import initialize
-from mailman.interfaces.requests import IRequests
+from mailman.interfaces.requests import IListRequests, RequestType
from mailman.version import MAILMAN_VERSION
# Work around known problems with some RedHat cron daemons
@@ -63,7 +62,7 @@ def pending_requests(mlist):
lcset = mlist.preferred_language.charset
pending = []
first = True
- requestsdb = config.db.get_list_requests(mlist)
+ requestsdb = IListRequests(mlist)
for request in requestsdb.of_type(RequestType.subscription):
if first:
pending.append(_('Pending subscriptions:'))
@@ -128,7 +127,7 @@ def auto_discard(mlist):
# Discard old held messages
discard_count = 0
expire = config.days(mlist.max_days_to_hold)
- requestsdb = config.db.get_list_requests(mlist)
+ requestsdb = IListRequests(mlist)
heldmsgs = list(requestsdb.of_type(RequestType.held_message))
if expire and heldmsgs:
for request in heldmsgs:
@@ -158,7 +157,7 @@ def main():
# The list must be locked in order to open the requests database
mlist = MailList.MailList(name)
try:
- count = getUtility(IRequests).get_list_requests(mlist).count
+ count = IListRequests(mlist).count
# While we're at it, let's evict yesterday's autoresponse data
midnight_today = midnight()
evictions = []
diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml
index bc08eb8c5..aad79adaf 100644
--- a/src/mailman/config/configure.zcml
+++ b/src/mailman/config/configure.zcml
@@ -6,14 +6,20 @@
<adapter
for="mailman.interfaces.mailinglist.IMailingList"
- factory="mailman.model.autorespond.AutoResponseSet"
provides="mailman.interfaces.autorespond.IAutoResponseSet"
+ factory="mailman.model.autorespond.AutoResponseSet"
/>
<adapter
for="mailman.interfaces.mailinglist.IMailingList"
- factory="mailman.model.mailinglist.AcceptableAliasSet"
provides="mailman.interfaces.mailinglist.IAcceptableAliasSet"
+ factory="mailman.model.mailinglist.AcceptableAliasSet"
+ />
+
+ <adapter
+ for="mailman.interfaces.mailinglist.IMailingList"
+ provides="mailman.interfaces.requests.IListRequests"
+ factory="mailman.model.requests.ListRequests"
/>
<utility
@@ -62,11 +68,6 @@
/>
<utility
- factory="mailman.model.requests.Requests"
- provides="mailman.interfaces.requests.IRequests"
- />
-
- <utility
factory="mailman.styles.manager.StyleManager"
provides="mailman.interfaces.styles.IStyleManager"
/>
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 4411166fd..f101cae06 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -21,6 +21,13 @@ Architecture
* Dynamically calculate the `List-Id` header instead of storing it in the
database. This means it cannot be changed.
+REST
+----
+ * Held messages can now be moderated through the REST API. Mailing list
+ resources now accept a `held` path component. GETing this returns all held
+ messages for the mailing list. POSTing to a specific request id under this
+ url can dispose of the message using `Action` enums.
+
Interfaces
----------
* Add property `IUserManager.members` to return all `IMembers` in the system.
@@ -28,6 +35,12 @@ Interfaces
every mailing list as (list_name, mail_host).
* Remove previously deprecated `IListManager.get_mailing_lists()`.
* `IMailTransportAgentAliases` now explicitly accepts duck-typed arguments.
+ * `IRequests` interface is removed. Now just use adaptation from
+ `IListRequests` directly (which takes an `IMailingList` object).
+ * `handle_message()` now allows for `Action.hold` which is synonymous with
+ `Action.defer` (since the message is already being held).
+ * `IListRequests.get_request()` now takes an optional `request_type`
+ argument to narrow the search for the given request.
Commands
--------
diff --git a/src/mailman/interfaces/requests.py b/src/mailman/interfaces/requests.py
index 55fb6395c..6e3162274 100644
--- a/src/mailman/interfaces/requests.py
+++ b/src/mailman/interfaces/requests.py
@@ -26,7 +26,6 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'IListRequests',
- 'IRequests',
'RequestType',
]
@@ -87,10 +86,14 @@ class IListRequests(Interface):
Only items with a matching `type' are returned.
"""
- def get_request(request_id):
+ def get_request(request_id, request_type):
"""Get the data associated with the request id, or None.
:param request_id: The unique id for the request.
+ :type request_id: int
+ :param request_type: Optional request type that the requested id must
+ match, otherwise no match is returned.
+ :type request_type: `RequestType`
:return: A 2-tuple of the key and data originally held, or None if the
`request_id` is not in the database.
"""
@@ -101,15 +104,3 @@ class IListRequests(Interface):
:param request_id: The unique id for the request.
:raises KeyError: If `request_id` is not in the database.
"""
-
-
-
-class IRequests(Interface):
- """The requests database."""
-
- def get_list_requests(mailing_list):
- """Return the `IListRequests` object for the given mailing list.
-
- :param mailing_list: An `IMailingList`.
- :return: An `IListRequests` object for the mailing list.
- """
diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst
index 1bc66c40a..ab92917cd 100644
--- a/src/mailman/model/docs/requests.rst
+++ b/src/mailman/model/docs/requests.rst
@@ -37,12 +37,11 @@ A set of requests are always related to a particular mailing list, so given a
mailing list you need to get its requests object.
::
- >>> from mailman.interfaces.requests import IListRequests, IRequests
- >>> from zope.component import getUtility
+ >>> from mailman.interfaces.requests import IListRequests
>>> from zope.interface.verify import verifyObject
>>> mlist = create_list('test@example.com')
- >>> requests = getUtility(IRequests).get_list_requests(mlist)
+ >>> requests = IListRequests(mlist)
>>> verifyObject(IListRequests, requests)
True
>>> requests.mailing_list
diff --git a/src/mailman/model/requests.py b/src/mailman/model/requests.py
index bd7fe3796..4a3efa67f 100644
--- a/src/mailman/model/requests.py
+++ b/src/mailman/model/requests.py
@@ -15,13 +15,12 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Implementations of the IRequests and IListRequests interfaces."""
+"""Implementations of the pending requests interfaces."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
- 'Requests',
]
@@ -34,7 +33,7 @@ from mailman.config import config
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.pending import IPendable, IPendings
-from mailman.interfaces.requests import IListRequests, IRequests, RequestType
+from mailman.interfaces.requests import IListRequests, RequestType
@@ -91,10 +90,12 @@ class ListRequests:
config.db.store.add(request)
return request.id
- def get_request(self, request_id):
+ def get_request(self, request_id, request_type=None):
result = config.db.store.get(_Request, request_id)
if result is None:
return None
+ if request_type is not None and result.request_type != request_type:
+ return None
if result.data_hash is None:
return result.key, result.data_hash
pendable = getUtility(IPendings).confirm(
@@ -113,14 +114,6 @@ class ListRequests:
-class Requests:
- implements(IRequests)
-
- def get_list_requests(self, mailing_list):
- return ListRequests(mailing_list)
-
-
-
class _Request(Model):
"""Table for mailing list hold requests."""
diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py
index 2022ce738..624f232d4 100644
--- a/src/mailman/model/tests/test_listmanager.py
+++ b/src/mailman/model/tests/test_listmanager.py
@@ -34,7 +34,7 @@ from mailman.interfaces.listmanager import (
IListManager, ListCreatedEvent, ListCreatingEvent, ListDeletedEvent,
ListDeletingEvent)
from mailman.interfaces.messages import IMessageStore
-from mailman.interfaces.requests import IRequests
+from mailman.interfaces.requests import IListRequests
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import (
@@ -120,7 +120,7 @@ Message-ID: <argon>
getUtility(IListManager).delete(self._ant)
# This is a hack. ListRequests don't access self._mailinglist in
# their get_request() method.
- requestsdb = getUtility(IRequests).get_list_requests(None)
+ requestsdb = IListRequests(self._bee)
request = requestsdb.get_request(request_id)
self.assertEqual(request, None)
saved_message = getUtility(IMessageStore).get_message_by_id('<argon>')
diff --git a/src/mailman/model/tests/test_requests.py b/src/mailman/model/tests/test_requests.py
new file mode 100644
index 000000000..a85add748
--- /dev/null
+++ b/src/mailman/model/tests/test_requests.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the various pending requests interfaces."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.app.moderator import hold_message
+from mailman.interfaces.requests import IListRequests, RequestType
+from mailman.testing.helpers import specialized_message_from_string as mfs
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestRequests(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('ant@example.com')
+ self._msg = mfs("""\
+From: anne@example.com
+To: ant@example.com
+Subject: Something
+Message-ID: <alpha>
+
+Something else.
+""")
+
+ def test_get_request_with_type(self):
+ # get_request() takes an optional request type.
+ request_id = hold_message(self._mlist, self._msg)
+ requests_db = IListRequests(self._mlist)
+ # Submit a request with a non-matching type. This should return None
+ # as if there were no matches.
+ response = requests_db.get_request(
+ request_id, RequestType.subscription)
+ self.assertEqual(response, None)
+ # Submit the same request with a matching type.
+ key, data = requests_db.get_request(
+ request_id, RequestType.held_message)
+ self.assertEqual(key, '<alpha>')
+ # It should also succeed with no optional request type given.
+ key, data = requests_db.get_request(request_id)
+ self.assertEqual(key, '<alpha>')
diff --git a/src/mailman/rest/docs/moderation.rst b/src/mailman/rest/docs/moderation.rst
new file mode 100644
index 000000000..44a2183ec
--- /dev/null
+++ b/src/mailman/rest/docs/moderation.rst
@@ -0,0 +1,131 @@
+=======================
+Held message moderation
+=======================
+
+Held messages can be moderated through the REST API. A mailing list starts
+out with no held messages.
+
+ >>> ant = create_list('ant@example.com')
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held')
+ http_etag: "..."
+ start: 0
+ total_size: 0
+
+When a message gets held for moderator approval, it shows up in this list.
+::
+
+ >>> msg = message_from_string("""\
+ ... From: anne@example.com
+ ... To: ant@example.com
+ ... Subject: Something
+ ... Message-ID: <alpha>
+ ...
+ ... Something else.
+ ... """)
+
+ >>> from mailman.app.moderator import hold_message
+ >>> request_id = hold_message(ant, msg, {'extra': 7}, 'Because')
+ >>> transaction.commit()
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held')
+ entry 0:
+ data: {u'_mod_subject': u'Something',
+ u'_mod_message_id': u'<alpha>',
+ u'extra': 7,
+ u'_mod_fqdn_listname': u'ant@example.com',
+ u'_mod_hold_date': u'2005-08-01T07:49:23',
+ u'_mod_reason': u'Because',
+ u'_mod_sender': u'anne@example.com'}
+ http_etag: "..."
+ id: 1
+ key: <alpha>
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+You can get an individual held message by providing the *request id* for that
+message. This will include the text of the message.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1')
+ data: {u'_mod_subject': u'Something',
+ u'_mod_message_id': u'<alpha>',
+ u'extra': 7,
+ u'_mod_fqdn_listname': u'ant@example.com',
+ u'_mod_hold_date': u'2005-08-01T07:49:23',
+ u'_mod_reason': u'Because',
+ u'_mod_sender': u'anne@example.com'}
+ http_etag: "..."
+ id: 1
+ key: <alpha>
+ msg:
+ From: anne@example.com
+ To: ant@example.com
+ Subject: Something
+ Message-ID: <alpha>
+ X-Message-ID-Hash: GCSMSG43GYWWVUMO6F7FBUSSPNXQCJ6M
+ <BLANKLINE>
+ Something else.
+ <BLANKLINE>
+
+Individual messages can be moderated through the API by POSTing back to the
+held message's resource. The POST data requires an action of one of the
+following:
+
+ * discard - throw the message away.
+ * reject - bounces the message back to the original author.
+ * defer - defer any action on the message (continue to hold it)
+ * accept - accept the message for posting.
+
+Let's see what happens when the above message is deferred.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1', {
+ ... 'action': 'defer',
+ ... })
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+The message is still in the moderation queue.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1')
+ data: {u'_mod_subject': u'Something',
+ u'_mod_message_id': u'<alpha>',
+ u'extra': 7,
+ u'_mod_fqdn_listname': u'ant@example.com',
+ u'_mod_hold_date': u'2005-08-01T07:49:23',
+ u'_mod_reason': u'Because',
+ u'_mod_sender': u'anne@example.com'}
+ http_etag: "..."
+ id: 1
+ key: <alpha>
+ msg: From: anne@example.com
+ To: ant@example.com
+ Subject: Something
+ Message-ID: <alpha>
+ X-Message-ID-Hash: GCSMSG43GYWWVUMO6F7FBUSSPNXQCJ6M
+ <BLANKLINE>
+ Something else.
+ <BLANKLINE>
+
+The held message can be discarded.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1', {
+ ... 'action': 'discard',
+ ... })
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+After which, the message is gone from the moderation queue.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 404: 404 Not Found
+
+- Hold another message
+- Show accept
+- Show reject? - probably not as we're just into testing app.moderator
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 9d3865d00..0103022e7 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -42,6 +42,7 @@ from mailman.rest.configuration import ListConfiguration
from mailman.rest.helpers import (
CollectionMixin, etag, no_content, path_to, restish_matcher)
from mailman.rest.members import AMember, MemberCollection
+from mailman.rest.moderation import HeldMessages
from mailman.rest.validator import Validator
@@ -166,6 +167,13 @@ class AList(_ListBase):
return http.not_found()
return ListConfiguration(self._mlist, attribute)
+ @resource.child()
+ def held(self, request, segments):
+ """Return a list of held messages for the mailign list."""
+ if self._mlist is None:
+ return http.not_found()
+ return HeldMessages(self._mlist)
+
class AllLists(_ListBase):
diff --git a/src/mailman/rest/moderation.py b/src/mailman/rest/moderation.py
new file mode 100644
index 000000000..7075a75be
--- /dev/null
+++ b/src/mailman/rest/moderation.py
@@ -0,0 +1,116 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""REST API for Message moderation."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'HeldMessage',
+ 'HeldMessages',
+ ]
+
+
+from restish import http, resource
+from zope.component import getUtility
+
+from mailman.app.moderator import handle_message
+from mailman.interfaces.action import Action
+from mailman.interfaces.messages import IMessageStore
+from mailman.interfaces.requests import IListRequests, RequestType
+from mailman.rest.helpers import CollectionMixin, etag, no_content
+from mailman.rest.validator import Validator, enum_validator
+
+
+
+class HeldMessage(resource.Resource, CollectionMixin):
+ """Resource for moderating a held message."""
+
+ def __init__(self, mlist, request_id):
+ self._mlist = mlist
+ self._request_id = request_id
+
+ @resource.GET()
+ def details(self, request):
+ requests = IListRequests(self._mlist)
+ try:
+ request_id = int(self._request_id)
+ except ValueError:
+ return http.bad_request()
+ results = requests.get_request(request_id, RequestType.held_message)
+ if results is None:
+ return http.not_found()
+ key, data = results
+ msg = getUtility(IMessageStore).get_message_by_id(key)
+ resource = dict(
+ key=key,
+ data=data,
+ msg=msg.as_string(),
+ id=request_id,
+ )
+ return http.ok([], etag(resource))
+
+ @resource.POST()
+ def moderate(self, request):
+ try:
+ validator = Validator(action=enum_validator(Action))
+ arguments = validator(request)
+ except ValueError as error:
+ return http.bad_request([], str(error))
+ requests = IListRequests(self._mlist)
+ try:
+ request_id = int(self._request_id)
+ except ValueError:
+ return http.bad_request()
+ results = requests.get_request(request_id, RequestType.held_message)
+ if results is None:
+ return http.not_found()
+ handle_message(self._mlist, request_id, **arguments)
+ return no_content()
+
+
+
+class HeldMessages(resource.Resource, CollectionMixin):
+ """Resource for messages held for moderation."""
+
+ def __init__(self, mlist):
+ self._mlist = mlist
+
+ def _resource_as_dict(self, req):
+ """See `CollectionMixin`."""
+ key, data = self._requests.get_request(req.id)
+ return dict(
+ key=key,
+ data=data,
+ id=req.id,
+ )
+
+ def _get_collection(self, request):
+ requests = IListRequests(self._mlist)
+ self._requests = requests
+ return list(requests.of_type(RequestType.held_message))
+
+ @resource.GET()
+ def requests(self, request):
+ """/lists/listname/held"""
+ resource = self._make_collection(request)
+ return http.ok([], etag(resource))
+
+ @resource.child('{id}')
+ def message(self, request, segments, **kw):
+ return HeldMessage(self._mlist, kw['id'])
diff --git a/src/mailman/rest/tests/test_moderation.py b/src/mailman/rest/tests/test_moderation.py
new file mode 100644
index 000000000..79b0c8b80
--- /dev/null
+++ b/src/mailman/rest/tests/test_moderation.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2012 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""REST moderation tests."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ ]
+
+
+import unittest
+
+from urllib2 import HTTPError
+
+from mailman.app.lifecycle import create_list
+from mailman.app.moderator import hold_message, hold_subscription
+from mailman.config import config
+from mailman.interfaces.member import DeliveryMode
+from mailman.testing.helpers import (
+ call_api, specialized_message_from_string as mfs)
+from mailman.testing.layers import RESTLayer
+
+
+
+class TestModeration(unittest.TestCase):
+ layer = RESTLayer
+
+ def setUp(self):
+ self._mlist = create_list('ant@example.com')
+ self._msg = mfs("""\
+From: anne@example.com
+To: ant@example.com
+Subject: Something
+Message-ID: <alpha>
+
+Something else.
+""")
+ config.db.commit()
+
+ def test_not_found(self):
+ # When a bogus mailing list is given, 404 should result.
+ try:
+ # For Python 2.6
+ call_api('http://localhost:9001/3.0/lists/bee@example.com/held')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 404)
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_bad_request_id(self):
+ # Bad request when request_id is not an integer.
+ try:
+ # For Python 2.6
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant@example.com/held/bogus')
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 400)
+ else:
+ raise AssertionError('Expected HTTPError')
+
+ def test_subscription_request_as_held_message(self):
+ # Provide the request id of a subscription request using the held
+ # message API returns a not-found even though the request id is
+ # in the database.
+ held_id = hold_message(self._mlist, self._msg)
+ subscribe_id = hold_subscription(
+ self._mlist, 'bperson@example.net', 'Bart Person', 'xyz',
+ DeliveryMode.regular, 'en')
+ config.db.store.commit()
+ url = 'http://localhost:9001/3.0/lists/ant@example.com/held/{0}'
+ try:
+ call_api(url.format(subscribe_id))
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 404)
+ else:
+ raise AssertionError('Expected HTTPError')
+ # But using the held_id returns a valid response.
+ response, content = call_api(url.format(held_id))
+ self.assertEqual(response['key'], '<alpha>')
+
+ def test_bad_action(self):
+ # POSTing to a held message with a bad action.
+ held_id = hold_message(self._mlist, self._msg)
+ url = 'http://localhost:9001/3.0/lists/ant@example.com/held/{0}'
+ try:
+ call_api(url.format(held_id), {'action': 'bogus'})
+ except HTTPError as exc:
+ self.assertEqual(exc.code, 400)
+ self.assertEqual(exc.msg, 'Cannot convert parameters: action')
+ else:
+ raise AssertionError('Expected HTTPError')
diff --git a/src/mailman/rest/tests/test_users.py b/src/mailman/rest/tests/test_users.py
index 3c1c40fd3..1630eb96a 100644
--- a/src/mailman/rest/tests/test_users.py
+++ b/src/mailman/rest/tests/test_users.py
@@ -17,7 +17,7 @@
"""REST user tests."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [