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)
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
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."""
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
58 if len(segments
) != 2:
61 role
= MemberRole
[segments
[0]]
65 return (), dict(role
=role
, email
=segments
[1]), ()
68 def roster_matcher(request
, segments
):
69 """A matcher of all members URLs inside mailing lists.
73 if len(segments
) != 2 or segments
[0] != 'roster':
76 return (), dict(role
=MemberRole
[segments
[1]]), ()
82 def config_matcher(request
, segments
):
83 """A matcher for a mailing list's configuration resource.
86 e.g. /config/description
88 if len(segments
) < 1 or segments
[0] != 'config':
90 if len(segments
) == 1:
92 if len(segments
) == 2:
93 return (), dict(attribute
=segments
[1]), ()
94 # More segments are not allowed.
99 class _ListBase(CollectionMixin
):
100 """Shared base class for mailing list representations."""
102 def _resource_as_dict(self
, mlist
):
103 """See `CollectionMixin`."""
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
,
112 self_link
=path_to('lists/{0}'.format(mlist
.list_id
)),
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
128 manager
= getUtility(IListManager
)
129 if '@' in list_identifier
:
130 self
._mlist
= manager
.get(list_identifier
)
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:
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:
146 remove_list(self
._mlist
)
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
)
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
)
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
)
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."""
204 validator
= Validator(fqdn_listname
=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
))
216 created(response
, path_to('lists/{0}'.format(mlist
.list_id
)))
218 def on_get(self
, request
, response
):
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
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
))
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
272 archiver
= self
._archiver
_set
.get(attribute
)
274 raise ValueError('No such archiver: {}'.format(attribute
))
275 archiver
.is_enabled
= as_boolean(value
)
279 """The archivers for a list, with their enabled flags."""
281 def __init__(self
, 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
}
296 # For a PUT, all attributes are optional.
297 kws
['_optional'] = kws
.keys()
299 Validator(**kws
).update(self
._mlist
, request
)
300 except ValueError as error
:
301 bad_request(response
, str(error
))
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)
316 """Simple resource representing all list styles."""
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
))