Barry Warsaw pushed to branch master at mailman / Mailman
Commits:
ad53d761 by Aurélien Bompard at 2016-01-13T14:20:16-05:00
Expose the ban list on the REST API
- - - - -
fe57b60b by Aurélien Bompard at 2016-01-13T14:20:16-05:00
Add the self_link for bans and factor some code
- - - - -
95446742 by Barry Warsaw at 2016-01-13T16:53:20-05:00
Clean up the branch for landing.
* Add NEWS
* Update copyright years.
* Adjust for new IAPI.path_to() interface.
* Style.
* Remove some unreachable code.
* Boost coverage.
* Change some Bad Requests into Not Founds.
* Do not include `list_id` in resource JSON for global bans.
* Rephrase some doctests.
- - - - -
8 changed files:
- src/mailman/docs/NEWS.rst
- src/mailman/interfaces/bans.py
- src/mailman/model/bans.py
- + src/mailman/rest/bans.py
- src/mailman/rest/docs/membership.rst
- src/mailman/rest/lists.py
- src/mailman/rest/root.py
- + src/mailman/rest/tests/test_bans.py
Changes:
=====================================
src/mailman/docs/NEWS.rst
=====================================
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -131,6 +131,8 @@ REST
values accessible through the list's configuraiton resource. POSTing to
the resource with either ``send=True``, ``bump=True``, or both invokes the
given action.
+ * Global and list-centric bans can now be managed through the REST API.
+ Given by Aurélien Bompard.
Other
-----
=====================================
src/mailman/interfaces/bans.py
=====================================
--- a/src/mailman/interfaces/bans.py
+++ b/src/mailman/interfaces/bans.py
@@ -97,3 +97,10 @@ class IBanManager(Interface):
or not.
:rtype: bool
"""
+
+ def __iter__():
+ """Iterate over all banned addresses.
+
+ :return: The list of all banned addresses.
+ :rtype: list of `str`
+ """
=====================================
src/mailman/model/bans.py
=====================================
--- a/src/mailman/model/bans.py
+++ b/src/mailman/model/bans.py
@@ -111,3 +111,8 @@ class BanManager:
re.match(ban.email, email, re.IGNORECASE) is not None):
return True
return False
+
+ @dbconnection
+ def __iter__(self, store):
+ """See `IBanManager`."""
+ yield from store.query(Ban).filter_by(list_id=self._list_id)
=====================================
src/mailman/rest/bans.py
=====================================
--- /dev/null
+++ b/src/mailman/rest/bans.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2016 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 for banned emails."""
+
+__all__ = [
+ 'BannedEmail',
+ 'BannedEmails',
+ ]
+
+
+from mailman.interfaces.bans import IBanManager
+from mailman.rest.helpers import (
+ CollectionMixin, bad_request, child, created, etag, no_content, not_found,
+ okay)
+from mailman.rest.validator import Validator
+
+
+class _BannedBase:
+ """Common base class."""
+
+ def __init__(self, mlist):
+ self._mlist = mlist
+ self.ban_manager = IBanManager(self._mlist)
+
+ def _location(self, email):
+ if self._mlist is None:
+ base_location = ''
+ else:
+ base_location = 'lists/{}/'.format(self._mlist.list_id)
+ return self.api.path_to('{}bans/{}'.format(base_location, email))
+
+
+class BannedEmail(_BannedBase):
+ """A banned email."""
+
+ def __init__(self, mlist, email):
+ super().__init__(mlist)
+ self._email = email
+
+ def on_get(self, request, response):
+ """Get a banned email."""
+ if self.ban_manager.is_banned(self._email):
+ resource = dict(
+ email=self._email,
+ self_link=self._location(self._email),
+ )
+ if self._mlist is not None:
+ resource['list_id'] = self._mlist.list_id
+ okay(response, etag(resource))
+ else:
+ not_found(response, 'Email is not banned: {}'.format(self._email))
+
+ def on_delete(self, request, response):
+ """Remove an email from the ban list."""
+ if self.ban_manager.is_banned(self._email):
+ self.ban_manager.unban(self._email)
+ no_content(response)
+ else:
+ not_found(response, 'Email is not banned: {}'.format(self._email))
+
+
+class BannedEmails(_BannedBase, CollectionMixin):
+ """The list of all banned emails."""
+
+ def _resource_as_dict(self, ban):
+ """See `CollectionMixin`."""
+ resource = dict(
+ email=ban.email,
+ self_link=self._location(ban.email),
+ )
+ if ban.list_id is not None:
+ resource['list_id'] = ban.list_id
+ return resource
+
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ return list(self.ban_manager)
+
+ def on_get(self, request, response):
+ """/bans"""
+ resource = self._make_collection(request)
+ okay(response, etag(resource))
+
+ def on_post(self, request, response):
+ """Ban some email from subscribing."""
+ validator = Validator(email=str)
+ try:
+ email = validator(request)['email']
+ except ValueError as error:
+ bad_request(response, str(error))
+ return
+ if self.ban_manager.is_banned(email):
+ bad_request(response, b'Address is already banned')
+ else:
+ self.ban_manager.ban(email)
+ created(response, self._location(email))
+
+ @child(r'^(?P<email>[^/]+)')
+ def email(self, request, segments, **kw):
+ return BannedEmail(self._mlist, kw['email'])
=====================================
src/mailman/rest/docs/membership.rst
=====================================
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -965,3 +965,97 @@ The moderation action for a member can be changed by
PATCH'ing the
...
moderation_action: hold
...
+
+
+Handling the list of banned addresses
+=====================================
+
+To ban an address from subscribing you can POST to the ``/bans`` child
+of any list using the REST API.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/bans',
+ ... {'email': '[email protected]'})
+ content-length: 0
+ ...
+ location: .../3.0/lists/ant.example.com/bans/[email protected]
+ ...
+ status: 201
+
+This address is now banned, and you can get the list of banned addresses by
+issuing a GET request on the ``/bans`` child.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/bans')
+ entry 0:
+ email: [email protected]
+ http_etag: "..."
+ list_id: ant.example.com
+ self_link: .../3.0/lists/ant.example.com/bans/[email protected]
+ ...
+
+You can always GET a single banned address.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com'
+ ... '/bans/[email protected]')
+ email: [email protected]
+ http_etag: "..."
+ list_id: ant.example.com
+ self_link: .../3.0/lists/ant.example.com/bans/[email protected]
+
+Unbanning addresses is also possible by issuing a DELETE request.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com'
+ ... '/bans/[email protected]',
+ ... method='DELETE')
+ content-length: 0
+ ...
+ status: 204
+
+After unbanning, the address is not shown in the ban list anymore.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com/bans')
+ http_etag: "..."
+ start: 0
+ total_size: 0
+
+Global bans prevent an address from subscribing to any mailing list, and they
+can be added via the top-level ``bans`` resource.
+
+ >>> dump_json('http://localhost:9001/3.0/bans',
+ ... {'email': '[email protected]'})
+ content-length: 0
+ ...
+ location: http://localhost:9001/3.0/bans/[email protected]
+ ...
+ status: 201
+
+Note that entries in the global bans do not have a ``list_id`` field.
+::
+
+ >>> dump_json('http://localhost:9001/3.0/bans')
+ entry 0:
+ email: [email protected]
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/bans/[email protected]
+ ...
+
+ >>> dump_json('http://localhost:9001/3.0/bans/[email protected]')
+ email: [email protected]
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/bans/[email protected]
+
+As with list-centric bans, you can delete a global ban.
+
+ >>> dump_json('http://localhost:9001/3.0/bans/[email protected]',
+ ... method='DELETE')
+ content-length: 0
+ ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/bans/[email protected]')
+ Traceback (most recent call last):
+ ...
+ urllib.error.HTTPError: HTTP Error 404: ...
+ >>> dump_json('http://localhost:9001/3.0/bans')
+ http_etag: "..."
+ start: 0
+ total_size: 0
=====================================
src/mailman/rest/lists.py
=====================================
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -39,6 +39,7 @@ from mailman.interfaces.mailinglist import IListArchiverSet
from mailman.interfaces.member import MemberRole
from mailman.interfaces.styles import IStyleManager
from mailman.interfaces.subscriptions import ISubscriptionService
+from mailman.rest.bans import BannedEmails
from mailman.rest.listconf import ListConfiguration
from mailman.rest.helpers import (
CollectionMixin, GetterSetter, NotFound, accepted, bad_request, child,
@@ -198,6 +199,13 @@ class AList(_ListBase):
return NotFound(), []
return ListDigest(self._mlist)
+ @child()
+ def bans(self, request, segments):
+ """Return a collection of mailing list's banned addresses."""
+ if self._mlist is None:
+ return NotFound(), []
+ return BannedEmails(self._mlist)
+
class AllLists(_ListBase):
=====================================
src/mailman/rest/root.py
=====================================
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -32,6 +32,7 @@ from mailman.core.system import system
from mailman.interfaces.listmanager import IListManager
from mailman.model.uid import UID
from mailman.rest.addresses import AllAddresses, AnAddress
+from mailman.rest.bans import BannedEmail, BannedEmails
from mailman.rest.domains import ADomain, AllDomains
from mailman.rest.helpers import (
BadRequest, NotFound, child, etag, no_content, not_found, okay)
@@ -290,6 +291,17 @@ class TopLevel:
return BadRequest(), []
@child()
+ def bans(self, request, segments):
+ """/<api>/bans
+ /<api>/bans/<email>
+ """
+ if len(segments) == 0:
+ return BannedEmails(None)
+ else:
+ email = segments.pop(0)
+ return BannedEmail(None, email), segments
+
+ @child()
def reserved(self, request, segments):
"""/<api>/reserved/[...]"""
return Reserved(segments), []
=====================================
src/mailman/rest/tests/test_bans.py
=====================================
--- /dev/null
+++ b/src/mailman/rest/tests/test_bans.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2016 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 address bans."""
+
+__all__ = [
+ 'TestBans',
+ ]
+
+
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.database.transaction import transaction
+from mailman.interfaces.bans import IBanManager
+from mailman.testing.layers import RESTLayer
+from mailman.testing.helpers import call_api
+from urllib.error import HTTPError
+
+
+class TestBans(unittest.TestCase):
+ layer = RESTLayer
+
+ def setUp(self):
+ with transaction():
+ self._mlist = create_list('[email protected]')
+
+ def test_get_missing_banned_address(self):
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/lists/ant.example.com'
+ '/bans/[email protected]')
+ self.assertEqual(cm.exception.code, 404)
+ self.assertEqual(cm.exception.reason,
+ b'Email is not banned: [email protected]')
+
+ def test_delete_missing_banned_address(self):
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/lists/ant.example.com'
+ '/bans/[email protected]',
+ method='DELETE')
+ self.assertEqual(cm.exception.code, 404)
+ self.assertEqual(cm.exception.reason,
+ b'Email is not banned: [email protected]')
+
+ def test_not_found_after_unbanning(self):
+ manager = IBanManager(self._mlist)
+ with transaction():
+ manager.ban('[email protected]')
+ url = ('http://localhost:9001/3.0/lists/ant.example.com'
+ '/bans/[email protected]')
+ response, content = call_api(url)
+ self.assertEqual(response['email'], '[email protected]')
+ response, content = call_api(url, method='DELETE')
+ self.assertEqual(content.status, 204)
+ with self.assertRaises(HTTPError) as cm:
+ call_api(url)
+ self.assertEqual(cm.exception.code, 404)
+ self.assertEqual(cm.exception.reason,
+ b'Email is not banned: [email protected]')
+
+ def test_not_found_after_unbanning_global(self):
+ manager = IBanManager(None)
+ with transaction():
+ manager.ban('[email protected]')
+ url = ('http://localhost:9001/3.0/bans/[email protected]')
+ response, content = call_api(url)
+ self.assertEqual(response['email'], '[email protected]')
+ response, content = call_api(url, method='DELETE')
+ self.assertEqual(content.status, 204)
+ with self.assertRaises(HTTPError) as cm:
+ call_api(url)
+ self.assertEqual(cm.exception.code, 404)
+ self.assertEqual(cm.exception.reason,
+ b'Email is not banned: [email protected]')
View it on GitLab:
https://gitlab.com/mailman/mailman/compare/187dad97bf278b0ca9d080774072e8fb235154cc...95446742669349777ee4101237a76395f1dfaa87
_______________________________________________
Mailman-checkins mailing list
[email protected]
Unsubscribe:
https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org