# Copyright (C) 2010-2015 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 .
"""REST for members."""
__all__ = [
'AMember',
'AllMembers',
'FindMembers',
'MemberCollection',
]
from mailman.app.membership import delete_member
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import IListManager, NoSuchListError
from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, MemberRole, MembershipError,
NotAMemberError)
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.interfaces.user import UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.rest.helpers import (
CollectionMixin, NotFound, bad_request, child, conflict, created, etag,
no_content, not_found, okay, paginate, path_to)
from mailman.rest.preferences import Preferences, ReadOnlyPreferences
from mailman.rest.validator import (
Validator, enum_validator, subscriber_validator)
from operator import attrgetter
from uuid import UUID
from zope.component import getUtility
class _MemberBase(CollectionMixin):
"""Shared base class for member representations."""
def _resource_as_dict(self, member):
"""See `CollectionMixin`."""
enum, dot, role = str(member.role).partition('.')
# The member will always have a member id and an address id. It will
# only have a user id if the address is linked to a user.
# E.g. nonmembers we've only seen via postings to lists they are not
# subscribed to will not have a user id. The user_id and the
# member_id are UUIDs. We need to use the integer equivalent in the
# URL.
member_id = member.member_id.int
response = dict(
list_id=member.list_id,
email=member.address.email,
role=role,
address=path_to('addresses/{}'.format(member.address.email)),
self_link=path_to('members/{}'.format(member_id)),
delivery_mode=member.delivery_mode,
member_id=member_id,
)
# Add the user link if there is one.
user = member.user
if user is not None:
response['user'] = path_to('users/{}'.format(user.user_id.int))
return response
@paginate
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(ISubscriptionService))
class MemberCollection(_MemberBase):
"""Abstract class for supporting submemberships.
This is used for example to return a resource representing all the
memberships of a mailing list, or all memberships for a specific email
address.
"""
def _get_collection(self, request):
"""See `CollectionMixin`."""
raise NotImplementedError
def on_get(self, request, response):
"""roster/[members|owners|moderators]"""
resource = self._make_collection(request)
okay(response, etag(resource))
class AMember(_MemberBase):
"""A member."""
def __init__(self, member_id_string):
# REST gives us the member id as the string of an int; we have to
# convert it to a UUID.
try:
member_id = UUID(int=int(member_id_string))
except ValueError:
# The string argument could not be converted to an integer.
self._member = None
else:
service = getUtility(ISubscriptionService)
self._member = service.get_member(member_id)
def on_get(self, request, response):
"""Return a single member end-point."""
if self._member is None:
not_found(response)
else:
okay(response, self._resource_as_json(self._member))
@child()
def preferences(self, request, segments):
"""/members//preferences"""
if len(segments) != 0:
return NotFound(), []
if self._member is None:
return NotFound(), []
child = Preferences(
self._member.preferences,
'members/{0}'.format(self._member.member_id.int))
return child, []
@child()
def all(self, request, segments):
"""/members//all/preferences"""
if len(segments) == 0:
return NotFound(), []
if self._member is None:
return NotFound(), []
child = ReadOnlyPreferences(
self._member,
'members/{0}/all'.format(self._member.member_id.int))
return child, []
def on_delete(self, request, response):
"""Delete the member (i.e. unsubscribe)."""
# Leaving a list is a bit different than deleting a moderator or
# owner. Handle the former case first. For now too, we will not send
# an admin or user notification.
if self._member is None:
not_found(response)
return
mlist = getUtility(IListManager).get_by_list_id(self._member.list_id)
if self._member.role is MemberRole.member:
try:
delete_member(mlist, self._member.address.email, False, False)
except NotAMemberError:
not_found(response)
return
else:
self._member.unsubscribe()
no_content(response)
def on_patch(self, request, response):
"""Patch the membership.
This is how subscription changes are done.
"""
if self._member is None:
not_found(response)
return
try:
values = Validator(
address=str,
delivery_mode=enum_validator(DeliveryMode),
_optional=('address', 'delivery_mode'))(request)
except ValueError as error:
bad_request(response, str(error))
return
if 'address' in values:
email = values['address']
address = getUtility(IUserManager).get_address(email)
if address is None:
bad_request(response, b'Address not registered')
return
try:
self._member.address = address
except (MembershipError, UnverifiedAddressError) as error:
bad_request(response, str(error))
return
if 'delivery_mode' in values:
self._member.preferences.delivery_mode = values['delivery_mode']
no_content(response)
class AllMembers(_MemberBase):
"""The members."""
def on_post(self, request, response):
"""Create a new member."""
service = getUtility(ISubscriptionService)
try:
validator = Validator(
list_id=str,
subscriber=subscriber_validator,
display_name=str,
delivery_mode=enum_validator(DeliveryMode),
role=enum_validator(MemberRole),
_optional=('delivery_mode', 'display_name', 'role'))
member = service.join(**validator(request))
except AlreadySubscribedError:
conflict(response, b'Member already subscribed')
except NoSuchListError:
bad_request(response, b'No such list')
except InvalidEmailAddressError:
bad_request(response, b'Invalid email address')
except ValueError as error:
bad_request(response, str(error))
else:
# The member_id are UUIDs. We need to use the integer equivalent
# in the URL.
member_id = member.member_id.int
location = path_to('members/{0}'.format(member_id))
created(response, location)
def on_get(self, request, response):
"""/members"""
resource = self._make_collection(request)
okay(response, etag(resource))
class _FoundMembers(MemberCollection):
"""The found members collection."""
def __init__(self, members):
super(_FoundMembers, self).__init__()
self._members = members
def _get_collection(self, request):
"""See `CollectionMixin`."""
address_of_member = attrgetter('address.email')
return list(sorted(self._members, key=address_of_member))
class FindMembers(_MemberBase):
"""/members/find"""
def on_post(self, request, response):
"""Find a member"""
service = getUtility(ISubscriptionService)
validator = Validator(
list_id=str,
subscriber=str,
role=enum_validator(MemberRole),
_optional=('list_id', 'subscriber', 'role'))
try:
members = service.find_members(**validator(request))
except ValueError as error:
bad_request(response, str(error))
else:
resource = _FoundMembers(members)._make_collection(request)
okay(response, etag(resource))