# Copyright (C) 2012-2017 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 API for held message moderation.""" from contextlib import suppress from email.errors import MessageError from email.header import decode_header, make_header from mailman.app.moderator import handle_message from mailman.interfaces.action import Action from mailman.interfaces.messages import IMessageStore from mailman.interfaces.requests import IListRequests, RequestType from mailman.rest.helpers import ( CollectionMixin, bad_request, child, etag, no_content, not_found, okay) from mailman.rest.validator import Validator, enum_validator from public import public from zope.component import getUtility class _ModerationBase: """Common base class.""" def _make_resource(self, request_id): requests = IListRequests(self._mlist) results = requests.get_request(request_id) if results is None: return None key, data = results resource = dict(key=key, request_id=request_id) # Flatten the IRequest payload into the JSON representation. if data is not None: resource.update(data) # Check for a matching request type, and insert the type name into the # resource. try: request_type = RequestType[resource.pop('_request_type', None)] except KeyError: request_type = None if request_type is not RequestType.held_message: return None resource['type'] = RequestType.held_message.name # This key isn't what you think it is. Usually, it's the Pendable # record's row id, which isn't helpful at all. If it's not there, # that's fine too. resource.pop('id', None) # Add a self_link. resource['self_link'] = self.api.path_to( 'lists/{}/held/{}'.format(self._mlist.list_id, request_id)) return resource class _HeldMessageBase(_ModerationBase): """Held messages are a little different.""" def _make_resource(self, request_id): resource = super()._make_resource(request_id) if resource is None: return None # Grab the message and insert its text representation into the # resource. XXX See LP: #967954 key = resource.pop('key') msg = getUtility(IMessageStore).get_message_by_id(key) try: resource['msg'] = msg.as_string() except KeyError: # If the message can't be parsed, return a generic message instead # of raising an error. # # See http://bugs.python.org/issue27321 and GL#256 resource['msg'] = 'This message is defective' # Some of the _mod_* keys we want to rename and place into the JSON # resource. Others we can drop. Since we're mutating the dictionary, # we need to make a copy of the keys. When you port this to Python 3, # you'll need to list()-ify the .keys() dictionary view. for key in list(resource): if key in ('_mod_subject', '_mod_hold_date', '_mod_reason', '_mod_sender', '_mod_message_id'): resource[key[5:]] = resource.pop(key) elif key.startswith('_mod_'): del resource[key] # Store the original header and then try decoding it. resource['original_subject'] = resource['subject'] # If we can't decode the header, leave the subject unchanged. with suppress(LookupError, MessageError): resource['subject'] = str( make_header(decode_header(resource['subject']))) # Also, held message resources will always be this type, so ignore # this key value. del resource['type'] return resource @public class HeldMessage(_HeldMessageBase): """Resource for moderating a held message.""" def __init__(self, mlist, request_id): self._mlist = mlist self._request_id = request_id def on_get(self, request, response): try: request_id = int(self._request_id) except ValueError: not_found(response) return resource = self._make_resource(request_id) if resource is None: not_found(response) else: okay(response, etag(resource)) def on_post(self, request, response): try: validator = Validator(action=enum_validator(Action)) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return requests = IListRequests(self._mlist) try: request_id = int(self._request_id) except ValueError: not_found(response) return results = requests.get_request(request_id, RequestType.held_message) if results is None: not_found(response) else: handle_message(self._mlist, request_id, **arguments) no_content(response) @public class HeldMessages(_HeldMessageBase, CollectionMixin): """Resource for messages held for moderation.""" def __init__(self, mlist): self._mlist = mlist def _resource_as_dict(self, request): """See `CollectionMixin`.""" resource = self._make_resource(request.id) assert resource is not None, resource return resource def _get_collection(self, request): requests = IListRequests(self._mlist) return requests.of_type(RequestType.held_message) def on_get(self, request, response): """/lists/listname/held""" resource = self._make_collection(request) okay(response, etag(resource)) @child(r'^(?P[^/]+)') def message(self, context, segments, **kw): return HeldMessage(self._mlist, kw['id'])