summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/rest/docs/membership.rst101
-rw-r--r--src/mailman/rest/lists.py24
-rw-r--r--src/mailman/rest/tests/test_lists.py33
3 files changed, 130 insertions, 28 deletions
diff --git a/src/mailman/rest/docs/membership.rst b/src/mailman/rest/docs/membership.rst
index 6ab2a7e8d..0a5fb34d9 100644
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -771,6 +771,53 @@ Elly is no longer a member of the mailing list.
set()
+Mass Unsubscriptions
+====================
+A batch of users can be unsubscribed from the mailing list via the REST API
+just by supplying their email addresses.
+
+ >>> m_list = create_list('mytest@example.com')
+ >>> subscribe(m_list, 'Joe')
+ <Member: Joe Person <jperson@example.com> on mytest@example.com as MemberRole.member>
+ >>> subscribe(m_list, 'Ken')
+ <Member: Ken Person <kperson@example.com> on mytest@example.com as MemberRole.member>
+ >>> subscribe(m_list, 'Mat')
+ <Member: Mat Person <mperson@example.com> on mytest@example.com as MemberRole.member>
+ >>> dump_json('http://localhost:9001/3.0/lists/mytest.example.com'
+ ... '/roster/member', {'emails': ['kperson@example.com',
+ ... 'mperson@example.com',
+ ... 'kperson@example.com',
+ ... 'tim@example.com']}, 'DELETE')
+ http_etag: "..."
+ kperson@example.com: Member already deleted.
+ tim@example.com: No such member.
+ >>> dump_json('http://localhost:9001/3.0/lists/mytest.example.com'
+ ... '/roster/member')
+ entry 0:
+ address: http://localhost:9001/3.0/addresses/jperson@example.com
+ delivery_mode: regular
+ email: jperson@example.com
+ http_etag: "..."
+ list_id: mytest.example.com
+ member_id: 10
+ role: member
+ self_link: http://localhost:9001/3.0/members/10
+ user: http://localhost:9001/3.0/users/7
+ ...
+ total_size: 1
+
+Ken and Mat have been unsubscribed successfully while tim@example.com can't
+be unsubscribed since he was not a member of the list. Similarly Joe can be
+unsubscribed from the mailing list.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/mytest.example.com'
+ ... '/roster/member', {'emails': ['jperson@example.com']}, 'DELETE')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+
Changing delivery address
=========================
@@ -809,10 +856,10 @@ addresses.
email: herb@example.com
http_etag: "..."
list_id: ant.example.com
- member_id: 10
+ member_id: 13
role: member
- self_link: http://localhost:9001/3.0/members/10
- user: http://localhost:9001/3.0/users/7
+ self_link: http://localhost:9001/3.0/members/13
+ user: http://localhost:9001/3.0/users/10
...
entry 9:
address: http://localhost:9001/3.0/addresses/herb@example.com
@@ -820,10 +867,10 @@ addresses.
email: herb@example.com
http_etag: "..."
list_id: bee.example.com
- member_id: 11
+ member_id: 14
role: member
- self_link: http://localhost:9001/3.0/members/11
- user: http://localhost:9001/3.0/users/7
+ self_link: http://localhost:9001/3.0/members/14
+ user: http://localhost:9001/3.0/users/10
http_etag: "..."
start: 0
total_size: 10
@@ -837,13 +884,13 @@ Herb must iterate through his memberships explicitly.
>>> memberships = [entry['self_link'] for entry in content['entries']]
>>> for url in sorted(memberships):
... print(url)
- http://localhost:9001/3.0/members/10
- http://localhost:9001/3.0/members/11
+ http://localhost:9001/3.0/members/13
+ http://localhost:9001/3.0/members/14
For each membership resource, the subscription address is changed by PATCH'ing
the `address` attribute.
- >>> dump_json('http://localhost:9001/3.0/members/10', {
+ >>> dump_json('http://localhost:9001/3.0/members/13', {
... 'address': 'hperson@example.com',
... }, method='PATCH')
content-length: 0
@@ -851,7 +898,7 @@ the `address` attribute.
server: ...
status: 204
- >>> dump_json('http://localhost:9001/3.0/members/11', {
+ >>> dump_json('http://localhost:9001/3.0/members/14', {
... 'address': 'hperson@example.com',
... }, method='PATCH')
content-length: 0
@@ -878,20 +925,20 @@ his membership ids have not changed.
email: hperson@example.com
http_etag: "..."
list_id: ant.example.com
- member_id: 10
+ member_id: 13
role: member
- self_link: http://localhost:9001/3.0/members/10
- user: http://localhost:9001/3.0/users/7
+ self_link: http://localhost:9001/3.0/members/13
+ user: http://localhost:9001/3.0/users/10
entry 1:
address: http://localhost:9001/3.0/addresses/hperson@example.com
delivery_mode: regular
email: hperson@example.com
http_etag: "..."
list_id: bee.example.com
- member_id: 11
+ member_id: 14
role: member
- self_link: http://localhost:9001/3.0/members/11
- user: http://localhost:9001/3.0/users/7
+ self_link: http://localhost:9001/3.0/members/14
+ user: http://localhost:9001/3.0/users/10
http_etag: "..."
start: 0
total_size: 2
@@ -900,7 +947,7 @@ When changing his subscription address, Herb may also decide to change his
mode of delivery.
::
- >>> dump_json('http://localhost:9001/3.0/members/11', {
+ >>> dump_json('http://localhost:9001/3.0/members/14', {
... 'address': 'herb@example.com',
... 'delivery_mode': 'mime_digests',
... }, method='PATCH')
@@ -917,10 +964,10 @@ mode of delivery.
email: herb@example.com
http_etag: "..."
list_id: bee.example.com
- member_id: 11
+ member_id: 14
role: member
- self_link: http://localhost:9001/3.0/members/11
- user: http://localhost:9001/3.0/users/7
+ self_link: http://localhost:9001/3.0/members/14
+ user: http://localhost:9001/3.0/users/10
http_etag: "..."
start: 0
total_size: 1
@@ -933,22 +980,22 @@ The moderation action for a member can be changed by PATCH'ing the
`moderation_action` attribute. When the member action falls back to the list
default, there is no such attribute in the resource.
- >>> dump_json('http://localhost:9001/3.0/members/10')
+ >>> dump_json('http://localhost:9001/3.0/members/13')
address: http://localhost:9001/3.0/addresses/hperson@example.com
delivery_mode: regular
email: hperson@example.com
- http_etag: "6d544208d60e578189fc023748f1ec467290abb5"
+ http_etag: "..."
list_id: ant.example.com
- member_id: 10
+ member_id: 13
role: member
- self_link: http://localhost:9001/3.0/members/10
- user: http://localhost:9001/3.0/users/7
+ self_link: http://localhost:9001/3.0/members/13
+ user: http://localhost:9001/3.0/users/10
Patching the moderation action both changes it for the given user, and adds
the attribute to the member's resource.
::
- >>> dump_json('http://localhost:9001/3.0/members/10', {
+ >>> dump_json('http://localhost:9001/3.0/members/13', {
... 'moderation_action': 'hold',
... }, method='PATCH')
content-length: 0
@@ -956,7 +1003,7 @@ the attribute to the member's resource.
server: ...
status: 204
- >>> dump_json('http://localhost:9001/3.0/members/10')
+ >>> dump_json('http://localhost:9001/3.0/members/13')
address: http://localhost:9001/3.0/addresses/hperson@example.com
...
moderation_action: hold
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 1e19babbb..4f0c56569 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -39,7 +39,7 @@ from mailman.rest.listconf import ListConfiguration
from mailman.rest.members import AMember, MemberCollection
from mailman.rest.post_moderation import HeldMessages
from mailman.rest.sub_moderation import SubscriptionRequests
-from mailman.rest.validator import Validator
+from mailman.rest.validator import Validator, list_of_strings_validator
from zope.component import getUtility
@@ -248,6 +248,28 @@ class MembersOfList(MemberCollection):
list_id=self._mlist.list_id,
role=self._role)
+ def on_delete(self, request, response):
+ """Delete the members of the named mailing list."""
+ status = {}
+ success = []
+ fail = []
+ try:
+ validator = Validator(emails=list_of_strings_validator)
+ arguments = validator(request)
+ emails = arguments.pop('emails')
+ except ValueError:
+ return bad_request(response, b'Invalid Input.')
+ success, fail = getUtility(ISubscriptionService).unsubscribe_members(
+ self._mlist.list_id, emails)
+ if len(fail) == 0:
+ return no_content(response)
+ for email in fail:
+ if email in success:
+ status[email] = 'Member already deleted.'
+ else:
+ status[email] = 'No such member.'
+ okay(response, etag(status))
+
@public
class ListsForDomain(_ListBase):
diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py
index 08c29151f..207743e50 100644
--- a/src/mailman/rest/tests/test_lists.py
+++ b/src/mailman/rest/tests/test_lists.py
@@ -213,6 +213,39 @@ class TestLists(unittest.TestCase):
'/owner/nobody@example.com')
self.assertEqual(cm.exception.code, 404)
+ def test_list_mass_unsubscribe(self):
+ with transaction():
+ aperson = self._usermanager.create_address('aperson@test.com')
+ bperson = self._usermanager.create_address('bperson@test.com')
+ cperson = self._usermanager.create_address('cperson@test.com')
+ mlist = create_list('testlist@example.com')
+ mlist.subscribe(aperson)
+ mlist.subscribe(bperson)
+ mlist.subscribe(cperson)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/lists/bogus.example.com'
+ '/roster/member', None, 'DELETE')
+ self.assertEqual(cm.exception.code, 404)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/lists/testlist.example.com'
+ '/roster/member', None, 'DELETE')
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason, b'Invalid Input.')
+ resource, response = call_api(
+ 'http://127.0.0.1:9001/3.0/lists/testlist.example.com'
+ '/roster/member', {'emails': ['aperson@test.com']}, 'DELETE')
+ self.assertEqual(response.status, 204)
+ resource, response = call_api(
+ 'http://127.0.0.1:9001/3.0/lists/testlist.example.com'
+ '/roster/member', {'emails': ['bperson@test.com',
+ 'cperson@test.com',
+ 'bperson@test.com',
+ 'bogus@test.com']}, 'DELETE')
+ self.assertEqual(response.status, 200)
+ self.assertEqual(resource['bperson@test.com'],
+ 'Member already deleted.')
+ self.assertEqual(resource['bogus@test.com'], 'No such member.')
+
class TestListArchivers(unittest.TestCase):
"""Test corner cases for list archivers."""