1 # Copyright (C) 2010-2019 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."""
20 from lazr
.config
import as_boolean
21 from mailman
.app
.membership
import add_member
, delete_member
22 from mailman
.interfaces
.action
import Action
23 from mailman
.interfaces
.address
import IAddress
24 from mailman
.interfaces
.listmanager
import IListManager
25 from mailman
.interfaces
.member
import (
26 AlreadySubscribedError
, DeliveryMode
, MemberRole
, MembershipError
,
27 MembershipIsBannedError
, MissingPreferredAddressError
)
28 from mailman
.interfaces
.subscriptions
import (
29 ISubscriptionManager
, ISubscriptionService
, RequestRecord
,
30 SubscriptionPendingError
, TokenOwner
)
31 from mailman
.interfaces
.user
import IUser
, UnverifiedAddressError
32 from mailman
.interfaces
.usermanager
import IUserManager
33 from mailman
.rest
.helpers
import (
34 CollectionMixin
, NotFound
, accepted
, bad_request
, child
, conflict
,
35 created
, etag
, no_content
, not_found
, okay
)
36 from mailman
.rest
.preferences
import Preferences
, ReadOnlyPreferences
37 from mailman
.rest
.validator
import (
38 Validator
, enum_validator
, subscriber_validator
)
39 from public
import public
41 from zope
.component
import getUtility
44 class _MemberBase(CollectionMixin
):
45 """Shared base class for member representations."""
47 def _resource_as_dict(self
, member
):
48 """See `CollectionMixin`."""
49 enum
, dot
, role
= str(member
.role
).partition('.')
50 # The member will always have a member id and an address id. It will
51 # only have a user id if the address is linked to a user.
52 # E.g. nonmembers we've only seen via postings to lists they are not
53 # subscribed to will not have a user id. The user_id and the
54 # member_id are UUIDs. In API 3.0 we use the integer equivalent of
55 # the UID in the URL, but in API 3.1 we use the hex equivalent. See
56 # issue #121 for details.
57 member_id
= self
.api
.from_uuid(member
.member_id
)
59 address
=self
.api
.path_to(
60 'addresses/{}'.format(member
.address
.email
)),
61 delivery_mode
=member
.delivery_mode
,
62 email
=member
.address
.email
,
63 list_id
=member
.list_id
,
66 self_link
=self
.api
.path_to('members/{}'.format(member_id
)),
68 # Add the moderation action if overriding the list's default.
69 if member
.moderation_action
is not None:
70 response
['moderation_action'] = member
.moderation_action
71 # Add display_name if it is present
72 if member
.display_name
is not None:
73 response
['display_name'] = member
.display_name
74 # Add the user link if there is one.
77 user_id
= self
.api
.from_uuid(user
.user_id
)
78 response
['user'] = self
.api
.path_to('users/{}'.format(user_id
))
81 def _get_collection(self
, request
):
82 """See `CollectionMixin`."""
83 return list(getUtility(ISubscriptionService
))
87 class MemberCollection(_MemberBase
):
88 """Abstract class for supporting submemberships.
90 This is used for example to return a resource representing all the
91 memberships of a mailing list, or all memberships for a specific email
94 def _get_collection(self
, request
):
95 """See `CollectionMixin`."""
96 raise NotImplementedError
98 def on_get(self
, request
, response
):
99 """roster/[members|owners|moderators]"""
100 resource
= self
._make
_collection
(request
)
101 okay(response
, etag(resource
))
105 class AMember(_MemberBase
):
108 def __init__(self
, member_id
):
109 # The member_id is either the member's UUID or the string
110 # representation of the member's UUID.
111 service
= getUtility(ISubscriptionService
)
112 self
._member
_id
= member_id
113 self
._member
= (None if member_id
is None
114 else service
.get_member(member_id
))
116 def on_get(self
, request
, response
):
117 """Return a single member end-point."""
118 if self
._member
is None:
121 okay(response
, self
._resource
_as
_json
(self
._member
))
124 def preferences(self
, context
, segments
):
125 """/members/<id>/preferences"""
126 if len(segments
) != 0:
127 return NotFound(), []
128 if self
._member
is None:
129 return NotFound(), []
130 member_id
= self
.api
.from_uuid(self
._member
_id
)
132 self
._member
.preferences
, 'members/{}'.format(member_id
))
136 def all(self
, context
, segments
):
137 """/members/<id>/all/preferences"""
138 if len(segments
) == 0:
139 return NotFound(), []
140 if self
._member
is None:
141 return NotFound(), []
142 member_id
= self
.api
.from_uuid(self
._member
_id
)
143 child
= ReadOnlyPreferences(
144 self
._member
, 'members/{}/all'.format(member_id
))
147 def on_delete(self
, request
, response
):
148 """Delete the member (i.e. unsubscribe)."""
149 # Leaving a list is a bit different than deleting a moderator or
150 # owner. Handle the former case first. For now too, we will not send
151 # an admin or user notification.
152 if self
._member
is None:
155 mlist
= getUtility(IListManager
).get_by_list_id(self
._member
.list_id
)
156 if self
._member
.role
is MemberRole
.member
:
157 delete_member(mlist
, self
._member
.address
.email
, False, False)
159 self
._member
.unsubscribe()
162 def on_patch(self
, request
, response
):
163 """Patch the membership.
165 This is how subscription changes are done.
167 if self
._member
is None:
173 delivery_mode
=enum_validator(DeliveryMode
),
174 moderation_action
=enum_validator(Action
, allow_blank
=True),
175 _optional
=('address', 'delivery_mode', 'moderation_action'),
177 except ValueError as error
:
178 bad_request(response
, str(error
))
180 if 'address' in values
:
181 email
= values
['address']
182 address
= getUtility(IUserManager
).get_address(email
)
184 bad_request(response
, b
'Address not registered')
187 self
._member
.address
= address
188 except (MembershipError
, UnverifiedAddressError
) as error
:
189 bad_request(response
, str(error
))
191 if 'delivery_mode' in values
:
192 self
._member
.preferences
.delivery_mode
= values
['delivery_mode']
193 if 'moderation_action' in values
:
194 self
._member
.moderation_action
= values
['moderation_action']
199 class AllMembers(_MemberBase
):
202 def on_post(self
, request
, response
):
203 """Create a new member."""
205 validator
= Validator(
207 subscriber
=subscriber_validator(self
.api
),
209 delivery_mode
=enum_validator(DeliveryMode
),
210 role
=enum_validator(MemberRole
),
211 pre_verified
=as_boolean
,
212 pre_confirmed
=as_boolean
,
213 pre_approved
=as_boolean
,
214 _optional
=('delivery_mode', 'display_name', 'role',
215 'pre_verified', 'pre_confirmed', 'pre_approved'))
216 arguments
= validator(request
)
217 except ValueError as error
:
218 bad_request(response
, str(error
))
220 # Dig the mailing list out of the arguments.
221 list_id
= arguments
.pop('list_id')
222 mlist
= getUtility(IListManager
).get_by_list_id(list_id
)
224 bad_request(response
, b
'No such list')
226 # Figure out what kind of subscriber is being registered. Either it's
227 # a user via their preferred email address or it's an explicit address.
228 # If it's a UUID, then it must be associated with an existing user.
229 subscriber
= arguments
.pop('subscriber')
230 user_manager
= getUtility(IUserManager
)
231 # We use the display name if there is one.
232 display_name
= arguments
.pop('display_name', '')
233 if isinstance(subscriber
, UUID
):
234 user
= user_manager
.get_user_by_id(subscriber
)
236 bad_request(response
, b
'No such user')
240 # This must be an email address. See if there's an existing
241 # address object associated with this email.
242 address
= user_manager
.get_address(subscriber
)
244 # Create a new address, which of course will not be validated.
245 address
= user_manager
.create_address(
246 subscriber
, display_name
)
248 # What role are we subscribing? Regular members go through the
249 # subscription policy workflow while owners, moderators, and
250 # nonmembers go through the legacy API for now.
251 role
= arguments
.pop('role', MemberRole
.member
)
252 if role
is MemberRole
.member
:
253 # Get the pre_ flags for the subscription workflow.
254 pre_verified
= arguments
.pop('pre_verified', False)
255 pre_confirmed
= arguments
.pop('pre_confirmed', False)
256 pre_approved
= arguments
.pop('pre_approved', False)
257 # Now we can run the registration process until either the
258 # subscriber is subscribed, or the workflow is paused for
259 # verification, confirmation, or approval.
260 registrar
= ISubscriptionManager(mlist
)
262 token
, token_owner
, member
= registrar
.register(
264 pre_verified
=pre_verified
,
265 pre_confirmed
=pre_confirmed
,
266 pre_approved
=pre_approved
)
267 except AlreadySubscribedError
:
268 conflict(response
, b
'Member already subscribed')
270 except MissingPreferredAddressError
:
271 bad_request(response
, b
'User has no preferred address')
273 except MembershipIsBannedError
:
274 bad_request(response
, b
'Membership is banned')
276 except SubscriptionPendingError
:
277 conflict(response
, b
'Subscription request already pending')
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 except AlreadySubscribedError
:
329 bad_request(response
,
330 '{} is already an {} of {}'.format(
331 email
, role
.name
, mlist
.fqdn_listname
))
333 # The subscription completed. Let's get the resulting member
334 # and return the location to the new member. Member ids are
335 # UUIDs and need to be converted to URLs because JSON doesn't
336 # directly support UUIDs.
337 member_id
= self
.api
.from_uuid(member
.member_id
)
338 location
= self
.api
.path_to('members/{}'.format(member_id
))
339 created(response
, location
)
341 def on_get(self
, request
, response
):
343 resource
= self
._make
_collection
(request
)
344 okay(response
, etag(resource
))
347 class _FoundMembers(MemberCollection
):
348 """The found members collection."""
350 def __init__(self
, members
, api
):
352 self
._members
= members
355 def _get_collection(self
, request
):
356 """See `CollectionMixin`."""
361 class FindMembers(_MemberBase
):
364 def on_get(self
, request
, response
):
365 return self
._find
(request
, response
)
367 def on_post(self
, request
, response
):
368 return self
._find
(request
, response
)
370 def _find(self
, request
, response
):
372 service
= getUtility(ISubscriptionService
)
373 validator
= Validator(
376 role
=enum_validator(MemberRole
),
380 _optional
=('list_id', 'subscriber', 'role', 'page', 'count'))
382 data
= validator(request
)
383 except ValueError as error
:
384 bad_request(response
, str(error
))
386 # Remove any optional pagination query elements; they will be
388 data
.pop('page', None)
389 data
.pop('count', None)
390 members
= service
.find_members(**data
)
391 resource
= _FoundMembers(members
, self
.api
)
392 okay(response
, etag(resource
._make
_collection
(request
)))