# Copyright (C) 2010-2012 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 mailing lists."""
from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'AList',
'AllLists',
'ListConfiguration',
'ListsForDomain',
]
from operator import attrgetter
from restish import http, resource
from zope.component import getUtility
from mailman.app.lifecycle import create_list, remove_list
from mailman.interfaces.domain import BadDomainSpecificationError
from mailman.interfaces.listmanager import (
IListManager, ListAlreadyExistsError)
from mailman.interfaces.member import MemberRole
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.rest.configuration import ListConfiguration
from mailman.rest.helpers import (
CollectionMixin, etag, no_content, path_to, restish_matcher)
from mailman.rest.members import AMember, MemberCollection
from mailman.rest.moderation import HeldMessages
from mailman.rest.validator import Validator
@restish_matcher
def member_matcher(request, segments):
"""A matcher of member URLs inside mailing lists.
e.g. //aperson@example.org
"""
if len(segments) != 2:
return None
try:
role = MemberRole[segments[0]]
except ValueError:
# Not a valid role.
return None
# No more segments.
# XXX 2010-02-25 barry Matchers are undocumented in restish; they return a
# 3-tuple of (match_args, match_kws, segments).
return (), dict(role=role, email=segments[1]), ()
@restish_matcher
def roster_matcher(request, segments):
"""A matcher of all members URLs inside mailing lists.
e.g. /roster/
"""
if len(segments) != 2 or segments[0] != 'roster':
return None
try:
return (), dict(role=MemberRole[segments[1]]), ()
except ValueError:
# Not a valid role.
return None
@restish_matcher
def config_matcher(request, segments):
"""A matcher for a mailing list's configuration resource.
e.g. /config
e.g. /config/description
"""
if len(segments) < 1 or segments[0] != 'config':
return None
if len(segments) == 1:
return (), {}, ()
if len(segments) == 2:
return (), dict(attribute=segments[1]), ()
# More segments are not allowed.
return None
class _ListBase(resource.Resource, CollectionMixin):
"""Shared base class for mailing list representations."""
def _resource_as_dict(self, mlist):
"""See `CollectionMixin`."""
return dict(
display_name=mlist.display_name,
fqdn_listname=mlist.fqdn_listname,
list_id=mlist.list_id,
list_name=mlist.list_name,
mail_host=mlist.mail_host,
member_count=mlist.members.member_count,
volume=mlist.volume,
self_link=path_to('lists/{0}'.format(mlist.list_id)),
)
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(IListManager))
class AList(_ListBase):
"""A mailing list."""
def __init__(self, list_identifier):
# list-id is preferred, but for backward compatibility, fqdn_listname
# is also accepted. If the string contains '@', treat it as the
# latter.
manager = getUtility(IListManager)
if '@' in list_identifier:
self._mlist = manager.get(list_identifier)
else:
self._mlist = manager.get_by_list_id(list_identifier)
@resource.GET()
def mailing_list(self, request):
"""Return a single mailing list end-point."""
if self._mlist is None:
return http.not_found()
return http.ok([], self._resource_as_json(self._mlist))
@resource.DELETE()
def delete_list(self, request):
"""Delete the named mailing list."""
if self._mlist is None:
return http.not_found()
remove_list(self._mlist)
return no_content()
@resource.child(member_matcher)
def member(self, request, segments, role, email):
"""Return a single member representation."""
if self._mlist is None:
return http.not_found()
members = getUtility(ISubscriptionService).find_members(
email, self._mlist.list_id, role)
if len(members) == 0:
return http.not_found()
assert len(members) == 1, 'Too many matches'
return AMember(members[0].member_id)
@resource.child(roster_matcher)
def roster(self, request, segments, role):
"""Return the collection of all a mailing list's members."""
if self._mlist is None:
return http.not_found()
return MembersOfList(self._mlist, role)
@resource.child(config_matcher)
def config(self, request, segments, attribute=None):
"""Return a mailing list configuration object."""
if self._mlist is None:
return http.not_found()
return ListConfiguration(self._mlist, attribute)
@resource.child()
def held(self, request, segments):
"""Return a list of held messages for the mailign list."""
if self._mlist is None:
return http.not_found()
return HeldMessages(self._mlist)
class AllLists(_ListBase):
"""The mailing lists."""
@resource.POST()
def create(self, request):
"""Create a new mailing list."""
try:
validator = Validator(fqdn_listname=unicode)
mlist = create_list(**validator(request))
except ListAlreadyExistsError:
return http.bad_request([], b'Mailing list exists')
except BadDomainSpecificationError as error:
return http.bad_request([], b'Domain does not exist {0}'.format(
error.domain))
except ValueError as error:
return http.bad_request([], str(error))
# wsgiref wants headers to be bytes, not unicodes.
location = path_to('lists/{0}'.format(mlist.list_id))
# Include no extra headers or body.
return http.created(location, [], None)
@resource.GET()
def collection(self, request):
"""/lists"""
resource = self._make_collection(request)
return http.ok([], etag(resource))
class MembersOfList(MemberCollection):
"""The members of a mailing list."""
def __init__(self, mailing_list, role):
super(MembersOfList, self).__init__()
self._mlist = mailing_list
self._role = role
def _get_collection(self, request):
"""See `CollectionMixin`."""
# Overrides _MemberBase._get_collection() because we only want to
# return the members from the requested roster.
roster = self._mlist.get_roster(self._role)
address_of_member = attrgetter('address.email')
return list(sorted(roster.members, key=address_of_member))
class ListsForDomain(_ListBase):
"""The mailing lists for a particular domain."""
def __init__(self, domain):
self._domain = domain
@resource.GET()
def collection(self, request):
"""/domains//lists"""
resource = self._make_collection(request)
return http.ok([], etag(resource))
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(self._domain.mailing_lists)