# Copyright (C) 2010-2016 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 lazr.config import as_boolean from mailman import public from mailman.app.digests import ( bump_digest_number_and_volume, maybe_send_digest_now) from mailman.app.lifecycle import create_list, remove_list from mailman.config import config from mailman.interfaces.domain import BadDomainSpecificationError from mailman.interfaces.listmanager import ( IListManager, ListAlreadyExistsError) from mailman.interfaces.mailinglist import IListArchiverSet from mailman.interfaces.member import MemberRole from mailman.interfaces.styles import IStyleManager from mailman.interfaces.subscriptions import ISubscriptionService from mailman.rest.bans import BannedEmails from mailman.rest.header_matches import HeaderMatches from mailman.rest.helpers import ( CollectionMixin, GetterSetter, NotFound, accepted, bad_request, child, created, etag, no_content, not_found, okay) 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 zope.component import getUtility 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 KeyError: # Not a valid role. return None return (), dict(role=role, email=segments[1]), () 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 KeyError: # Not a valid role. return None 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(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=self.api.path_to('lists/{}'.format(mlist.list_id)), ) def _get_collection(self, request): """See `CollectionMixin`.""" return list(getUtility(IListManager)) @public 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) def on_get(self, request, response): """Return a single mailing list end-point.""" if self._mlist is None: not_found(response) else: okay(response, self._resource_as_json(self._mlist)) def on_delete(self, request, response): """Delete the named mailing list.""" if self._mlist is None: not_found(response) else: remove_list(self._mlist) no_content(response) @child(member_matcher) def member(self, request, segments, role, email): """Return a single member representation.""" if self._mlist is None: return NotFound(), [] member = getUtility(ISubscriptionService).find_member( email, self._mlist.list_id, role) if member is None: return NotFound(), [] return AMember(request.context['api'], member.member_id) @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 NotFound(), [] return MembersOfList(self._mlist, role) @child(config_matcher) def config(self, request, segments, attribute=None): """Return a mailing list configuration object.""" if self._mlist is None: return NotFound(), [] return ListConfiguration(self._mlist, attribute) @child() def held(self, request, segments): """Return a list of held messages for the mailing list.""" if self._mlist is None: return NotFound(), [] return HeldMessages(self._mlist) @child() def requests(self, request, segments): """Return a list of subscription/unsubscription requests.""" if self._mlist is None: return NotFound(), [] return SubscriptionRequests(self._mlist) @child() def archivers(self, request, segments): """Return a representation of mailing list archivers.""" if self._mlist is None: return NotFound(), [] return ListArchivers(self._mlist) @child() def digest(self, request, segments): if self._mlist is None: return NotFound(), [] return ListDigest(self._mlist) @child() def bans(self, request, segments): """Return a collection of mailing list's banned addresses.""" if self._mlist is None: return NotFound(), [] return BannedEmails(self._mlist) @child(r'^header-matches') def header_matches(self, request, segments): """Return a collection of mailing list's header matches.""" if self._mlist is None: return NotFound(), [] return HeaderMatches(self._mlist) @public class AllLists(_ListBase): """The mailing lists.""" def on_post(self, request, response): """Create a new mailing list.""" try: validator = Validator(fqdn_listname=str, style_name=str, _optional=('style_name',)) mlist = create_list(**validator(request)) except ListAlreadyExistsError: bad_request(response, b'Mailing list exists') except BadDomainSpecificationError as error: reason = 'Domain does not exist: {}'.format(error.domain) bad_request(response, reason.encode('utf-8')) else: location = self.api.path_to('lists/{0}'.format(mlist.list_id)) created(response, location) def on_get(self, request, response): """/lists""" resource = self._make_collection(request) okay(response, etag(resource)) @public class MembersOfList(MemberCollection): """The members of a mailing list.""" def __init__(self, mailing_list, role): super().__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. return getUtility(ISubscriptionService).find_members( list_id=self._mlist.list_id, role=self._role) @public class ListsForDomain(_ListBase): """The mailing lists for a particular domain.""" def __init__(self, domain): self._domain = domain def on_get(self, request, response): """/domains//lists""" resource = self._make_collection(request) okay(response, etag(resource)) def _get_collection(self, request): """See `CollectionMixin`.""" return list(self._domain.mailing_lists) @public class ArchiverGetterSetter(GetterSetter): """Resource for updating archiver statuses.""" def __init__(self, mlist): super().__init__() self._archiver_set = IListArchiverSet(mlist) def put(self, mlist, attribute, value): # attribute will contain the (bytes) name of the archiver that is # getting a new status. value will be the representation of the new # boolean status. archiver = self._archiver_set.get(attribute) assert archiver is not None, attribute archiver.is_enabled = as_boolean(value) @public class ListArchivers: """The archivers for a list, with their enabled flags.""" def __init__(self, mlist): self._mlist = mlist def on_get(self, request, response): """Get all the archiver statuses.""" archiver_set = IListArchiverSet(self._mlist) resource = {archiver.name: archiver.is_enabled for archiver in archiver_set.archivers} okay(response, etag(resource)) def patch_put(self, request, response, is_optional): archiver_set = IListArchiverSet(self._mlist) kws = {archiver.name: ArchiverGetterSetter(self._mlist) for archiver in archiver_set.archivers} if is_optional: # For a PATCH, all attributes are optional. kws['_optional'] = kws.keys() try: Validator(**kws).update(self._mlist, request) except ValueError as error: bad_request(response, str(error)) else: no_content(response) def on_put(self, request, response): """Update all the archiver statuses.""" self.patch_put(request, response, is_optional=False) def on_patch(self, request, response): """Patch some archiver statueses.""" self.patch_put(request, response, is_optional=True) @public class ListDigest: """Simple resource representing actions on a list's digest.""" def __init__(self, mlist): self._mlist = mlist def on_get(self, request, response): resource = dict( next_digest_number=self._mlist.next_digest_number, volume=self._mlist.volume, ) okay(response, etag(resource)) def on_post(self, request, response): try: validator = Validator( send=as_boolean, bump=as_boolean, _optional=('send', 'bump')) values = validator(request) except ValueError as error: bad_request(response, str(error)) return if len(values) == 0: # There's nothing to do, but that's okay. okay(response) return if values.get('bump', False): bump_digest_number_and_volume(self._mlist) if values.get('send', False): maybe_send_digest_now(self._mlist, force=True) accepted(response) @public class Styles: """Simple resource representing all list styles.""" def __init__(self): manager = getUtility(IStyleManager) style_names = sorted(style.name for style in manager.styles) self._resource = dict( style_names=style_names, default=config.styles.default) def on_get(self, request, response): okay(response, etag(self._resource))