1 # Copyright (C) 2010-2016 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """REST for members."""
28 from mailman
.app
.membership
import add_member
, delete_member
29 from mailman
.interfaces
.action
import Action
30 from mailman
.interfaces
.address
import IAddress
31 from mailman
.interfaces
.listmanager
import IListManager
32 from mailman
.interfaces
.member
import (
33 AlreadySubscribedError
, DeliveryMode
, MemberRole
, MembershipError
,
34 MembershipIsBannedError
, MissingPreferredAddressError
)
35 from mailman
.interfaces
.registrar
import IRegistrar
36 from mailman
.interfaces
.subscriptions
import (
37 ISubscriptionService
, RequestRecord
, TokenOwner
)
38 from mailman
.interfaces
.user
import IUser
, UnverifiedAddressError
39 from mailman
.interfaces
.usermanager
import IUserManager
40 from mailman
.rest
.helpers
import (
41 CollectionMixin
, NotFound
, accepted
, bad_request
, child
, conflict
,
42 created
, etag
, no_content
, not_found
, okay
)
43 from mailman
.rest
.preferences
import Preferences
, ReadOnlyPreferences
44 from mailman
.rest
.validator
import (
45 Validator
, enum_validator
, subscriber_validator
)
47 from zope
.component
import getUtility
51 class _MemberBase(CollectionMixin
):
52 """Shared base class for member representations."""
54 def _resource_as_dict(self
, member
):
55 """See `CollectionMixin`."""
56 enum
, dot
, role
= str(member
.role
).partition('.')
57 # The member will always have a member id and an address id. It will
58 # only have a user id if the address is linked to a user.
59 # E.g. nonmembers we've only seen via postings to lists they are not
60 # subscribed to will not have a user id. The user_id and the
61 # member_id are UUIDs. In API 3.0 we use the integer equivalent of
62 # the UID in the URL, but in API 3.1 we use the hex equivalent. See
63 # issue #121 for details.
64 member_id
= self
.api
.from_uuid(member
.member_id
)
66 address
=self
.api
.path_to(
67 'addresses/{}'.format(member
.address
.email
)),
68 delivery_mode
=member
.delivery_mode
,
69 email
=member
.address
.email
,
70 list_id
=member
.list_id
,
72 moderation_action
=member
.moderation_action
,
74 self_link
=self
.api
.path_to('members/{}'.format(member_id
)),
76 # Add the user link if there is one.
79 user_id
= self
.api
.from_uuid(user
.user_id
)
80 response
['user'] = self
.api
.path_to('users/{}'.format(user_id
))
83 def _get_collection(self
, request
):
84 """See `CollectionMixin`."""
85 return list(getUtility(ISubscriptionService
))
89 class MemberCollection(_MemberBase
):
90 """Abstract class for supporting submemberships.
92 This is used for example to return a resource representing all the
93 memberships of a mailing list, or all memberships for a specific email
96 def _get_collection(self
, request
):
97 """See `CollectionMixin`."""
98 raise NotImplementedError
100 def on_get(self
, request
, response
):
101 """roster/[members|owners|moderators]"""
102 resource
= self
._make
_collection
(request
)
103 okay(response
, etag(resource
))
107 class AMember(_MemberBase
):
110 def __init__(self
, api
, member_id
):
111 # The member_id is either the member's UUID or the string
112 # representation of the member's UUID.
114 service
= getUtility(ISubscriptionService
)
116 self
._member
= service
.get_member(api
.to_uuid(member_id
))
118 # The string argument could not be converted to a UUID.
121 def on_get(self
, request
, response
):
122 """Return a single member end-point."""
123 if self
._member
is None:
126 okay(response
, self
._resource
_as
_json
(self
._member
))
129 def preferences(self
, request
, segments
):
130 """/members/<id>/preferences"""
131 if len(segments
) != 0:
132 return NotFound(), []
133 if self
._member
is None:
134 return NotFound(), []
135 member_id
= self
.api
.from_uuid(self
._member
.member_id
)
137 self
._member
.preferences
, 'members/{}'.format(member_id
))
141 def all(self
, request
, segments
):
142 """/members/<id>/all/preferences"""
143 if len(segments
) == 0:
144 return NotFound(), []
145 if self
._member
is None:
146 return NotFound(), []
147 child
= ReadOnlyPreferences(
149 'members/{}/all'.format(
150 self
.api
.from_uuid(self
._member
.member_id
)))
153 def on_delete(self
, request
, response
):
154 """Delete the member (i.e. unsubscribe)."""
155 # Leaving a list is a bit different than deleting a moderator or
156 # owner. Handle the former case first. For now too, we will not send
157 # an admin or user notification.
158 if self
._member
is None:
161 mlist
= getUtility(IListManager
).get_by_list_id(self
._member
.list_id
)
162 if self
._member
.role
is MemberRole
.member
:
163 delete_member(mlist
, self
._member
.address
.email
, False, False)
165 self
._member
.unsubscribe()
168 def on_patch(self
, request
, response
):
169 """Patch the membership.
171 This is how subscription changes are done.
173 if self
._member
is None:
179 delivery_mode
=enum_validator(DeliveryMode
),
180 moderation_action
=enum_validator(Action
),
181 _optional
=('address', 'delivery_mode', 'moderation_action'),
183 except ValueError as error
:
184 bad_request(response
, str(error
))
186 if 'address' in values
:
187 email
= values
['address']
188 address
= getUtility(IUserManager
).get_address(email
)
190 bad_request(response
, b
'Address not registered')
193 self
._member
.address
= address
194 except (MembershipError
, UnverifiedAddressError
) as error
:
195 bad_request(response
, str(error
))
197 if 'delivery_mode' in values
:
198 self
._member
.preferences
.delivery_mode
= values
['delivery_mode']
199 if 'moderation_action' in values
:
200 self
._member
.moderation_action
= values
['moderation_action']
205 class AllMembers(_MemberBase
):
208 def on_post(self
, request
, response
):
209 """Create a new member."""
211 validator
= Validator(
213 subscriber
=subscriber_validator(self
.api
),
215 delivery_mode
=enum_validator(DeliveryMode
),
216 role
=enum_validator(MemberRole
),
220 _optional
=('delivery_mode', 'display_name', 'role',
221 'pre_verified', 'pre_confirmed', 'pre_approved'))
222 arguments
= validator(request
)
223 except ValueError as error
:
224 bad_request(response
, str(error
))
226 # Dig the mailing list out of the arguments.
227 list_id
= arguments
.pop('list_id')
228 mlist
= getUtility(IListManager
).get_by_list_id(list_id
)
230 bad_request(response
, b
'No such list')
232 # Figure out what kind of subscriber is being registered. Either it's
233 # a user via their preferred email address or it's an explicit address.
234 # If it's a UUID, then it must be associated with an existing user.
235 subscriber
= arguments
.pop('subscriber')
236 user_manager
= getUtility(IUserManager
)
237 # We use the display name if there is one.
238 display_name
= arguments
.pop('display_name', '')
239 if isinstance(subscriber
, UUID
):
240 user
= user_manager
.get_user_by_id(subscriber
)
242 bad_request(response
, b
'No such user')
246 # This must be an email address. See if there's an existing
247 # address object associated with this email.
248 address
= user_manager
.get_address(subscriber
)
250 # Create a new address, which of course will not be validated.
251 address
= user_manager
.create_address(
252 subscriber
, display_name
)
254 # What role are we subscribing? Regular members go through the
255 # subscription policy workflow while owners, moderators, and
256 # nonmembers go through the legacy API for now.
257 role
= arguments
.pop('role', MemberRole
.member
)
258 if role
is MemberRole
.member
:
259 # Get the pre_ flags for the subscription workflow.
260 pre_verified
= arguments
.pop('pre_verified', False)
261 pre_confirmed
= arguments
.pop('pre_confirmed', False)
262 pre_approved
= arguments
.pop('pre_approved', False)
263 # Now we can run the registration process until either the
264 # subscriber is subscribed, or the workflow is paused for
265 # verification, confirmation, or approval.
266 registrar
= IRegistrar(mlist
)
268 token
, token_owner
, member
= registrar
.register(
270 pre_verified
=pre_verified
,
271 pre_confirmed
=pre_confirmed
,
272 pre_approved
=pre_approved
)
273 except AlreadySubscribedError
:
274 conflict(response
, b
'Member already subscribed')
276 except MissingPreferredAddressError
:
277 bad_request(response
, b
'User has no preferred address')
280 assert token_owner
is TokenOwner
.no_one
, token_owner
281 # The subscription completed. Let's get the resulting member
282 # and return the location to the new member. Member ids are
283 # UUIDs and need to be converted to URLs because JSON doesn't
284 # directly support UUIDs.
285 member_id
= self
.api
.from_uuid(member
.member_id
)
286 location
= self
.api
.path_to('members/{}'.format(member_id
))
287 created(response
, location
)
289 # The member could not be directly subscribed because there are
290 # some out-of-band steps that need to be completed. E.g. the user
291 # must confirm their subscription or the moderator must approve
292 # it. In this case, an HTTP 202 Accepted is exactly the code that
293 # we should use, and we'll return both the confirmation token and
294 # the "token owner" so the client knows who should confirm it.
295 assert token
is not None, token
296 assert token_owner
is not TokenOwner
.no_one
, token_owner
297 assert member
is None, member
298 content
= dict(token
=token
, token_owner
=token_owner
.name
)
299 accepted(response
, etag(content
))
301 # 2015-04-15 BAW: We're subscribing some role other than a regular
302 # member. Use the legacy API for this for now.
303 assert role
in (MemberRole
.owner
,
304 MemberRole
.moderator
,
305 MemberRole
.nonmember
)
306 # 2015-04-15 BAW: We're limited to using an email address with this
307 # legacy API, so if the subscriber is a user, the user must have a
308 # preferred address, which we'll use, even though it will subscribe
309 # the explicit address. It is an error if the user does not have a
312 # If the subscriber is an address object, just use that.
313 if IUser
.providedBy(subscriber
):
314 if subscriber
.preferred_address
is None:
315 bad_request(response
, b
'User without preferred address')
317 email
= subscriber
.preferred_address
.email
319 assert IAddress
.providedBy(subscriber
)
320 email
= subscriber
.email
321 delivery_mode
= arguments
.pop('delivery_mode', DeliveryMode
.regular
)
322 record
= RequestRecord(email
, display_name
, delivery_mode
)
324 member
= add_member(mlist
, record
, role
)
325 except MembershipIsBannedError
:
326 bad_request(response
, b
'Membership is banned')
328 # The subscription completed. Let's get the resulting member
329 # and return the location to the new member. Member ids are
330 # UUIDs and need to be converted to URLs because JSON doesn't
331 # directly support UUIDs.
332 member_id
= self
.api
.from_uuid(member
.member_id
)
333 location
= self
.api
.path_to('members/{}'.format(member_id
))
334 created(response
, location
)
336 def on_get(self
, request
, response
):
338 resource
= self
._make
_collection
(request
)
339 okay(response
, etag(resource
))
343 class _FoundMembers(MemberCollection
):
344 """The found members collection."""
346 def __init__(self
, members
, api
):
348 self
._members
= members
351 def _get_collection(self
, request
):
352 """See `CollectionMixin`."""
356 class FindMembers(_MemberBase
):
359 def on_get(self
, request
, response
):
360 return self
._find
(request
, response
)
362 def on_post(self
, request
, response
):
363 return self
._find
(request
, response
)
365 def _find(self
, request
, response
):
367 service
= getUtility(ISubscriptionService
)
368 validator
= Validator(
371 role
=enum_validator(MemberRole
),
375 _optional
=('list_id', 'subscriber', 'role', 'page', 'count'))
377 data
= validator(request
)
378 except ValueError as error
:
379 bad_request(response
, str(error
))
381 # Remove any optional pagination query elements; they will be
383 data
.pop('page', None)
384 data
.pop('count', None)
385 members
= service
.find_members(**data
)
386 resource
= _FoundMembers(members
, self
.api
)
387 okay(response
, etag(resource
._make
_collection
(request
)))