1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
# 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 <http://www.gnu.org/licenses/>.
"""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<id>[^/]+)')
def message(self, context, segments, **kw):
return HeldMessage(self._mlist, kw['id'])
|