Checkpointing.
[mailman.git] / src / mailman / rest / lists.py
blob0607102cba7e3cc151a5294e796972f06d86027c
1 # Copyright (C) 2010-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """REST for mailing lists."""
20 __all__ = [
21 'AList',
22 'AllLists',
23 'ListArchivers',
24 'ListConfiguration',
25 'ListsForDomain',
26 'Styles',
30 from lazr.config import as_boolean
31 from mailman.app.lifecycle import create_list, remove_list
32 from mailman.config import config
33 from mailman.interfaces.domain import BadDomainSpecificationError
34 from mailman.interfaces.listmanager import (
35 IListManager, ListAlreadyExistsError)
36 from mailman.interfaces.mailinglist import IListArchiverSet
37 from mailman.interfaces.member import MemberRole
38 from mailman.interfaces.styles import IStyleManager
39 from mailman.interfaces.subscriptions import ISubscriptionService
40 from mailman.rest.listconf import ListConfiguration
41 from mailman.rest.helpers import (
42 CollectionMixin, GetterSetter, NotFound, bad_request, child, created,
43 etag, no_content, not_found, okay, paginate, path_to)
44 from mailman.rest.members import AMember, MemberCollection
45 from mailman.rest.post_moderation import HeldMessages
46 from mailman.rest.sub_moderation import SubscriptionRequests
47 from mailman.rest.validator import Validator
48 from operator import attrgetter
49 from zope.component import getUtility
53 def member_matcher(request, segments):
54 """A matcher of member URLs inside mailing lists.
56 e.g. /<role>/aperson@example.org
57 """
58 if len(segments) != 2:
59 return None
60 try:
61 role = MemberRole[segments[0]]
62 except KeyError:
63 # Not a valid role.
64 return None
65 return (), dict(role=role, email=segments[1]), ()
68 def roster_matcher(request, segments):
69 """A matcher of all members URLs inside mailing lists.
71 e.g. /roster/<role>
72 """
73 if len(segments) != 2 or segments[0] != 'roster':
74 return None
75 try:
76 return (), dict(role=MemberRole[segments[1]]), ()
77 except KeyError:
78 # Not a valid role.
79 return None
82 def config_matcher(request, segments):
83 """A matcher for a mailing list's configuration resource.
85 e.g. /config
86 e.g. /config/description
87 """
88 if len(segments) < 1 or segments[0] != 'config':
89 return None
90 if len(segments) == 1:
91 return (), {}, ()
92 if len(segments) == 2:
93 return (), dict(attribute=segments[1]), ()
94 # More segments are not allowed.
95 return None
99 class _ListBase(CollectionMixin):
100 """Shared base class for mailing list representations."""
102 def _resource_as_dict(self, mlist):
103 """See `CollectionMixin`."""
104 return dict(
105 display_name=mlist.display_name,
106 fqdn_listname=mlist.fqdn_listname,
107 list_id=mlist.list_id,
108 list_name=mlist.list_name,
109 mail_host=mlist.mail_host,
110 member_count=mlist.members.member_count,
111 volume=mlist.volume,
112 self_link=path_to('lists/{0}'.format(mlist.list_id)),
115 @paginate
116 def _get_collection(self, request):
117 """See `CollectionMixin`."""
118 return list(getUtility(IListManager))
121 class AList(_ListBase):
122 """A mailing list."""
124 def __init__(self, list_identifier):
125 # list-id is preferred, but for backward compatibility, fqdn_listname
126 # is also accepted. If the string contains '@', treat it as the
127 # latter.
128 manager = getUtility(IListManager)
129 if '@' in list_identifier:
130 self._mlist = manager.get(list_identifier)
131 else:
132 self._mlist = manager.get_by_list_id(list_identifier)
134 def on_get(self, request, response):
135 """Return a single mailing list end-point."""
136 if self._mlist is None:
137 not_found(response)
138 else:
139 okay(response, self._resource_as_json(self._mlist))
141 def on_delete(self, request, response):
142 """Delete the named mailing list."""
143 if self._mlist is None:
144 not_found(response)
145 else:
146 remove_list(self._mlist)
147 no_content(response)
149 @child(member_matcher)
150 def member(self, request, segments, role, email):
151 """Return a single member representation."""
152 if self._mlist is None:
153 return NotFound(), []
154 members = getUtility(ISubscriptionService).find_members(
155 email, self._mlist.list_id, role)
156 if len(members) == 0:
157 return NotFound(), []
158 assert len(members) == 1, 'Too many matches'
159 return AMember(members[0].member_id)
161 @child(roster_matcher)
162 def roster(self, request, segments, role):
163 """Return the collection of all a mailing list's members."""
164 if self._mlist is None:
165 return NotFound(), []
166 return MembersOfList(self._mlist, role)
168 @child(config_matcher)
169 def config(self, request, segments, attribute=None):
170 """Return a mailing list configuration object."""
171 if self._mlist is None:
172 return NotFound(), []
173 return ListConfiguration(self._mlist, attribute)
175 @child()
176 def held(self, request, segments):
177 """Return a list of held messages for the mailing list."""
178 if self._mlist is None:
179 return NotFound(), []
180 return HeldMessages(self._mlist)
182 @child()
183 def requests(self, request, segments):
184 """Return a list of subscription/unsubscription requests."""
185 if self._mlist is None:
186 return NotFound(), []
187 return SubscriptionRequests(self._mlist)
189 @child()
190 def archivers(self, request, segments):
191 """Return a representation of mailing list archivers."""
192 if self._mlist is None:
193 return NotFound(), []
194 return ListArchivers(self._mlist)
198 class AllLists(_ListBase):
199 """The mailing lists."""
201 def on_post(self, request, response):
202 """Create a new mailing list."""
203 try:
204 validator = Validator(fqdn_listname=str,
205 style_name=str,
206 _optional=('style_name',))
207 mlist = create_list(**validator(request))
208 except ListAlreadyExistsError:
209 bad_request(response, b'Mailing list exists')
210 except BadDomainSpecificationError as error:
211 reason = 'Domain does not exist: {}'.format(error.domain)
212 bad_request(response, reason.encode('utf-8'))
213 except ValueError as error:
214 bad_request(response, str(error))
215 else:
216 created(response, path_to('lists/{0}'.format(mlist.list_id)))
218 def on_get(self, request, response):
219 """/lists"""
220 resource = self._make_collection(request)
221 okay(response, etag(resource))
225 class MembersOfList(MemberCollection):
226 """The members of a mailing list."""
228 def __init__(self, mailing_list, role):
229 super(MembersOfList, self).__init__()
230 self._mlist = mailing_list
231 self._role = role
233 @paginate
234 def _get_collection(self, request):
235 """See `CollectionMixin`."""
236 # Overrides _MemberBase._get_collection() because we only want to
237 # return the members from the requested roster.
238 roster = self._mlist.get_roster(self._role)
239 address_of_member = attrgetter('address.email')
240 return list(sorted(roster.members, key=address_of_member))
243 class ListsForDomain(_ListBase):
244 """The mailing lists for a particular domain."""
246 def __init__(self, domain):
247 self._domain = domain
249 def on_get(self, request, response):
250 """/domains/<domain>/lists"""
251 resource = self._make_collection(request)
252 okay(response, etag(resource))
254 @paginate
255 def _get_collection(self, request):
256 """See `CollectionMixin`."""
257 return list(self._domain.mailing_lists)
261 class ArchiverGetterSetter(GetterSetter):
262 """Resource for updating archiver statuses."""
264 def __init__(self, mlist):
265 super(ArchiverGetterSetter, self).__init__()
266 self._archiver_set = IListArchiverSet(mlist)
268 def put(self, mlist, attribute, value):
269 # attribute will contain the (bytes) name of the archiver that is
270 # getting a new status. value will be the representation of the new
271 # boolean status.
272 archiver = self._archiver_set.get(attribute)
273 if archiver is None:
274 raise ValueError('No such archiver: {}'.format(attribute))
275 archiver.is_enabled = as_boolean(value)
278 class ListArchivers:
279 """The archivers for a list, with their enabled flags."""
281 def __init__(self, mlist):
282 self._mlist = mlist
284 def on_get(self, request, response):
285 """Get all the archiver statuses."""
286 archiver_set = IListArchiverSet(self._mlist)
287 resource = {archiver.name: archiver.is_enabled
288 for archiver in archiver_set.archivers}
289 okay(response, etag(resource))
291 def patch_put(self, request, response, is_optional):
292 archiver_set = IListArchiverSet(self._mlist)
293 kws = {archiver.name: ArchiverGetterSetter(self._mlist)
294 for archiver in archiver_set.archivers}
295 if is_optional:
296 # For a PUT, all attributes are optional.
297 kws['_optional'] = kws.keys()
298 try:
299 Validator(**kws).update(self._mlist, request)
300 except ValueError as error:
301 bad_request(response, str(error))
302 else:
303 no_content(response)
305 def on_put(self, request, response):
306 """Update all the archiver statuses."""
307 self.patch_put(request, response, is_optional=False)
309 def on_patch(self, request, response):
310 """Patch some archiver statueses."""
311 self.patch_put(request, response, is_optional=True)
315 class Styles:
316 """Simple resource representing all list styles."""
318 def __init__(self):
319 manager = getUtility(IStyleManager)
320 style_names = sorted(style.name for style in manager.styles)
321 self._resource = dict(
322 style_names=style_names,
323 default=config.styles.default)
325 def on_get(self, request, response):
326 okay(response, etag(self._resource))