client.py 18.4 KB
Newer Older
Abhilash Raj's avatar
Abhilash Raj committed
1
# Copyright (C) 2010-2022 by the Free Software Foundation, Inc.
J08nY's avatar
J08nY committed
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
#
# This file is part of mailmanclient.
#
# mailmanclient is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# mailmanclient 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with mailmanclient.  If not, see <http://www.gnu.org/licenses/>.

"""Client code."""

import warnings
from operator import itemgetter

from mailmanclient.constants import (MISSING)
from mailmanclient.restobjects.address import Address
from mailmanclient.restobjects.ban import Bans, BannedAddress
from mailmanclient.restobjects.configuration import Configuration
from mailmanclient.restobjects.domain import Domain
27
from mailmanclient.restobjects.mailinglist import MailingList
J08nY's avatar
J08nY committed
28
29
30
from mailmanclient.restobjects.member import Member
from mailmanclient.restobjects.preferences import Preferences
from mailmanclient.restobjects.queue import Queue
31
from mailmanclient.restobjects.styles import Styles
J08nY's avatar
J08nY committed
32
from mailmanclient.restobjects.user import User
33
from mailmanclient.restobjects.templates import Template, TemplateList
J08nY's avatar
J08nY committed
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from mailmanclient.restbase.connection import Connection
from mailmanclient.restbase.page import Page

__metaclass__ = type
__all__ = [
    'Client'
]


#
# --- The following classes are part of the API
#

class Client:
48
49
50
    """Access the Mailman REST API root.

    :param baseurl: The base url to access the Mailman 3 REST API.
51
    :type baseurl: str
52
53
    :param name: The Basic Auth user name.  If given, the `password` must
        also be given.
54
    :type name: str
55
56
    :param password: The Basic Auth password.  If given the `name` must
        also be given.
57
58
59
60
    :type password: str
    :param request_hooks: Callable hooks to process request parameters before
        being sent to Core's API.
    :type request_hooks: List[callables]
61
    """
J08nY's avatar
J08nY committed
62

63
    def __init__(self, baseurl, name=None, password=None, request_hooks=None):
64
        """Initialize client access to the REST API."""
65
        self._connection = Connection(baseurl, name, password, request_hooks)
J08nY's avatar
J08nY committed
66
67
68
69
70

    def __repr__(self):
        return '<Client ({0.name}:{0.password}) {0.baseurl}>'.format(
            self._connection)

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
    def add_hooks(self, request_hooks):
        """Add a hook to process connections to Mailman's API.

        Hooks are callables that are passed in the parameters of HTTP call made
        to Mailman Core' RESt API and are expected to return the same number of
        parameters back. This can be useful to manipulate parameters and update
        them, or simply log each call to API.
        ::

            from mailmanclient.client import Client
            client = Client('http://localhost:8001/3.1', <user>, <password>)

            def sample_hook(params):
                print(f'Request params are {params}')
                return params

            client.add_hook([sample_hook])


        :param request_hooks: A list of callables that take in a dictionary of
           parameters. ``params`` is the list of request parameters before the
           request is made.
        """
Abhilash Raj's avatar
Abhilash Raj committed
94
        self._connection.add_hooks(request_hooks=request_hooks)
95

J08nY's avatar
J08nY committed
96
97
    @property
    def system(self):
98
99
100
101
        """Get the basic system information.

        :returns: System information about Mailman Core
        :rtype: Dict[str, str]
102
        """
J08nY's avatar
J08nY committed
103
104
105
106
        return self._connection.call('system/versions')[1]

    @property
    def preferences(self):
107
108
109
110
        """Get all default system Preferences.

        :returns: System preferences.
        :rtype: :class:`Preferences`
111
        """
J08nY's avatar
J08nY committed
112
113
114
115
        return Preferences(self._connection, 'system/preferences')

    @property
    def configuration(self):
116
117
118
119
        """Get the system configuration.

        :returns: All the system configuration.
        :rtype: Dict[str, :class:`Configuration`]
120
        """
J08nY's avatar
J08nY committed
121
122
123
124
125
126
        response, content = self._connection.call('system/configuration')
        return {section: Configuration(
            self._connection, section) for section in content['sections']}

    @property
    def pipelines(self):
127
128
129
130
        """Get a list of all Pipelines.

        :returns: A list of all the pipelines in Core.
        :rtype: List
131
        """
J08nY's avatar
J08nY committed
132
133
134
135
136
        response, content = self._connection.call('system/pipelines')
        return content

    @property
    def chains(self):
137
138
139
140
        """Get a list of all the Chains.

        :returns: A list of all the chains in Core.
        :rtype: List
141
        """
J08nY's avatar
J08nY committed
142
143
144
145
146
        response, content = self._connection.call('system/chains')
        return content

    @property
    def queues(self):
147
148
149
150
        """Get a list of all Queues.

        :returns: A list of all the queues in Core.
        :rtype: List
151
        """
J08nY's avatar
J08nY committed
152
153
154
155
156
157
158
        response, content = self._connection.call('queues')
        queues = {}
        for entry in content['entries']:
            queues[entry['name']] = Queue(
                self._connection, entry['self_link'], entry)
        return queues

159
160
    @property
    def styles(self):
161
162
163
164
165
        """All the default styles in Mailman Core.

        :returns: All the styles in Core.
        :rtype: :class:`Styles`
        """
166
167
        return Styles(self._connection, 'lists/styles')

J08nY's avatar
J08nY committed
168
169
    @property
    def lists(self):
170
171
172
173
        """Get a list of all MailingLists.

        :returns: All the mailing lists.
        :rtype: list(:class:`MailingList`)
174
        """
J08nY's avatar
J08nY committed
175
176
        return self.get_lists()

177
    def get_lists(self, advertised=False):
178
179
180
181
        """Get a list of all the MailingLists.

        :param advertised: If marked True, returns all MailingLists including
                           the ones that aren't advertised.
182
183
184
        :type advertised: bool
        :returns: A list of mailing lists.
        :rtype: List(:class:`MailingList`)
185
        """
J08nY's avatar
J08nY committed
186
187
188
189
190
191
192
193
194
        url = 'lists'
        if advertised:
            url += '?advertised=true'
        response, content = self._connection.call(url)
        if 'entries' not in content:
            return []
        return [MailingList(self._connection, entry['self_link'], entry)
                for entry in content['entries']]

195
    def get_list_page(self, count=50, page=1, advertised=None, mail_host=None):
196
        """Get a list of all MailingList with pagination.
197
198
199
200
201

        :param count: Number of entries per-page (defaults to 50).
        :param page: The page number to return (defaults to 1).
        :param advertised: If marked True, returns all MailingLists including
                           the ones that aren't advertised.
202
        :param mail_host: Domain to filter results by.
203
        """
204
205
206
207
        if mail_host:
            url = 'domains/{0}/lists'.format(mail_host)
        else:
            url = 'lists'
J08nY's avatar
J08nY committed
208
209
210
211
212
213
        if advertised:
            url += '?advertised=true'
        return Page(self._connection, url, MailingList, count, page)

    @property
    def domains(self):
214
215
216
217
        """Get a list of all Domains.

        :returns: All the domains on the system.
        :rtype: List[:class:`Domain`]
218
        """
J08nY's avatar
J08nY committed
219
220
221
222
223
224
225
226
227
        response, content = self._connection.call('domains')
        if 'entries' not in content:
            return []
        return [Domain(self._connection, entry['self_link'])
                for entry in sorted(content['entries'],
                                    key=itemgetter('mail_host'))]

    @property
    def members(self):
228
229
230
231
        """Get a list of all the Members.

        :returns: All the list memebrs.
        :rtype: List[:class:`Member`]
232
        """
J08nY's avatar
J08nY committed
233
234
235
236
237
238
239
        response, content = self._connection.call('members')
        if 'entries' not in content:
            return []
        return [Member(self._connection, entry['self_link'], entry)
                for entry in content['entries']]

    def get_member(self, fqdn_listname, subscriber_address):
240
        """Get the Member object for a given MailingList and Subsciber's Email
241
242
        Address.

243
244
245
246
        :param str fqdn_listname: Fully qualified address for the MailingList.
        :param str subscriber_address: Email Address for the subscriber.
        :returns: A member of a list.
        :rtype: :class:`Member`
247
        """
J08nY's avatar
J08nY committed
248
249
        return self.get_list(fqdn_listname).get_member(subscriber_address)

Abhilash Raj's avatar
Abhilash Raj committed
250
251
252
253
254
255
256
257
258
259
    def get_nonmember(self, fqdn_listname, nonmember_address):
        """Get the Member object for a given MailingList and Non-member's Email.

        :param str fqdn_listname: Fully qualified address for the MailingList.
        :param str subscriber_address: Email Address for the non-member.
        :returns: A member of a list.
        :rtype: :class:`Member`
        """
        return self.get_list(fqdn_listname).get_nonmember(nonmember_address)

J08nY's avatar
J08nY committed
260
    def get_member_page(self, count=50, page=1):
261
262
263
264
265
266
267
        """Return a paginated list of Members.

        :param int count: Number of items to return.
        :param int page: The page number.
        :returns: Paginated lists of members.
        :rtype: :class:`Page` of :class:`Member`.
        """
J08nY's avatar
J08nY committed
268
269
270
271
        return Page(self._connection, 'members', Member, count, page)

    @property
    def users(self):
272
273
274
275
        """Get all the users.

        :returns: All the users in Mailman Core.
        :rtype: List[:class:`User`]
276
        """
J08nY's avatar
J08nY committed
277
278
279
280
281
282
283
284
        response, content = self._connection.call('users')
        if 'entries' not in content:
            return []
        return [User(self._connection, entry['self_link'], entry)
                for entry in sorted(content['entries'],
                                    key=itemgetter('self_link'))]

    def get_user_page(self, count=50, page=1):
285
        """Get all the users with pagination.
286

287
288
289
290
        :param int count: Number of entries per-page (defaults to 50).
        :param int page: The page number to return (defaults to 1).
        :returns: Paginated list of users on Mailman.
        :rtype: :class:`Page` of :class:`User`
291
        """
J08nY's avatar
J08nY committed
292
293
294
        return Page(self._connection, 'users', User, count, page)

    def create_domain(self, mail_host, base_url=MISSING,
295
                      description=None, owner=None, alias_domain=None):
296
297
298
299
300
301
302
303
304
305
        """Create a new Domain.

        :param str mail_host: The Mail host for the new domain. If you want
            foo@bar.com" as the address for your MailingList, use "bar.com"
            here.
        :param str description: A brief description for this Domain.
        :param str owner: Email address for the owner of this list.
        :param str alias_domain: Alias domain.
        :returns: The created Domain.
        :rtype: :class:`Domain`
306
        """
J08nY's avatar
J08nY committed
307
308
309
310
311
312
313
314
315
316
        if base_url is not MISSING:
            warnings.warn(
                'The `base_url` parameter in the `create_domain()` method is '
                'deprecated. It is not used any more and will be removed in '
                'the future.', DeprecationWarning, stacklevel=2)
        data = dict(mail_host=mail_host)
        if description is not None:
            data['description'] = description
        if owner is not None:
            data['owner'] = owner
317
318
        if alias_domain is not None:
            data['alias_domain'] = alias_domain
J08nY's avatar
J08nY committed
319
        response, content = self._connection.call('domains', data)
320
        return Domain(self._connection, response.headers.get('location'))
J08nY's avatar
J08nY committed
321
322

    def delete_domain(self, mail_host):
323
        """Delete a Domain.
324

325
        :param str mail_host: The Mail host for the domain you want to delete.
326
        """
J08nY's avatar
J08nY committed
327
328
329
330
        response, content = self._connection.call(
            'domains/{0}'.format(mail_host), None, 'DELETE')

    def get_domain(self, mail_host, web_host=MISSING):
331
        """Get Domain by its mail_host."""
J08nY's avatar
J08nY committed
332
333
334
335
336
337
338
339
340
341
        if web_host is not MISSING:
            warnings.warn(
                'The `web_host` parameter in the `get_domain()` method is '
                'deprecated. It is not used any more and will be removed in '
                'the future.', DeprecationWarning, stacklevel=2)
        response, content = self._connection.call(
            'domains/{0}'.format(mail_host))
        return Domain(self._connection, content['self_link'])

    def create_user(self, email, password, display_name=''):
342
        """Create a new User.
343

344
345
346
347
348
        :param str email: Email address for the new user.
        :param str password: Password for the new user.
        :param str display_name: An optional name for the new user.
        :returns: The created user instance.
        :rtype: :class:`User`
349
        """
J08nY's avatar
J08nY committed
350
351
352
353
        response, content = self._connection.call(
            'users', dict(email=email,
                          password=password,
                          display_name=display_name))
354
        return User(self._connection, response.headers.get('location'))
J08nY's avatar
J08nY committed
355
356

    def get_user(self, address):
357
        """Given an Email Address, return the User it belongs to.
358

359
360
361
        :param str address: Email Address of the User.
        :returns: The user instance that owns the address.
        :rtype: :class:`User`
362
        """
J08nY's avatar
J08nY committed
363
364
365
366
367
        response, content = self._connection.call(
            'users/{0}'.format(address))
        return User(self._connection, content['self_link'], content)

    def get_address(self, address):
368
        """Given an Email Address, return the Address object.
369

370
371
372
        :param str address: Email address.
        :returns: The Address object for given email address.
        :rtype: :class:`Address`
373
        """
J08nY's avatar
J08nY committed
374
375
376
377
378
        response, content = self._connection.call(
            'addresses/{0}'.format(address))
        return Address(self._connection, content['self_link'], content)

    def get_list(self, fqdn_listname):
379
        """Get a MailingList object.
380

381
382
383
        :param str fqdn_listname: Fully qualified name of the MailingList.
        :returns: The mailing list object of the given fqdn_listname.
        :rtype: :class:`MailingList`
384
        """
J08nY's avatar
J08nY committed
385
386
387
388
389
        response, content = self._connection.call(
            'lists/{0}'.format(fqdn_listname))
        return MailingList(self._connection, content['self_link'], content)

    def delete_list(self, fqdn_listname):
390
        """Delete a MailingList.
391

392
        :param str fqdn_listname: Fully qualified name of the MailingList.
393
        """
J08nY's avatar
J08nY committed
394
395
396
397
398
        response, content = self._connection.call(
            'lists/{0}'.format(fqdn_listname), None, 'DELETE')

    @property
    def bans(self):
399
400
401
402
        """Get a list of all the bans.

        :returns: A list of all the bans.
        :rtype: :class:`Bans`
403
        """
J08nY's avatar
J08nY committed
404
405
406
        return Bans(self._connection, 'bans', mlist=None)

    def get_bans_page(self, count=50, page=1):
407
        """Get a list of all the bans with pagination.
408

409
410
411
412
        :param int count: Number of entries per-page (defaults to 50).
        :param int page: The page number to return (defaults to 1).
        :returns: Paginated list of banned addresses.
        :rtype: :class:`Page` of :class:`BannedAddress`
413
        """
J08nY's avatar
J08nY committed
414
        return Page(self._connection, 'bans', BannedAddress, count, page)
415
416
417
418

    @property
    def templates(self):
        """Get all site-context templates.
419
420
421

        :returns: List of templates for the site context.
        :rtype: :class:`TemplateList`
422
423
424
425
426
        """
        return TemplateList(self._connection, 'uris')

    def get_templates_page(self, count=25, page=1):
        """Get paginated site-context templates.
427
428
429

        :returns: Paginated list of templates of site context.
        :rtype: :class:`Page` of :class:`Template`
430
431
432
433
434
        """
        return Page(self._connection, 'uris', Template, count, page)

    def set_template(self, template_name, url, username=None, password=None):
        """Set template in site-context.
435
436
437
438
439

        :param str template_name: The template to set.
        :param str url: The URL to fetch the template from.
        :param str username: Username for access to the template.
        :param str password: Password for the ``username`` to access templates.
440
441
442
443
444
445
        """
        data = {template_name: url}
        if username is not None and password is not None:
            data['username'] = username
            data['password'] = password
        return self._connection.call('uris', data, 'PATCH')[1]
446

447
448
    def find_lists(self, subscriber, role=None, count=50, page=1,
                   mail_host=None):
449
        """Given a subscriber and a role, return all the list they are subscribed
450
451
452
453
454
455
        to with given role.

        If no role is specified all the related mailing lists are returned
        without duplicates, even though there can potentially be multiple
        memberships of a user in a single mailing list.

456
        :param str subscriber: The address of the subscriber.
457
458
459
460
        :param str role: owner, moderator or subscriber.
        :param int count: Number of entries per-page (defaults to 50).
        :param int page: The page number to return (defaults to 1).
        :param str mail_host: Domain to filter results by.
461
462
        :returns: A filtered list of mailing lists with given filters.
        :rtype: List[:class:`MailingList`]
463
464
465
466
467
468
469
470
471
        """
        url = 'lists/find'
        data = dict(subscriber=subscriber, count=count, page=page)
        if role is not None:
            data['role'] = role
        response, content = self._connection.call(url, data)
        if 'entries' not in content:
            return []
        return [MailingList(self._connection, entry['self_link'], entry)
472
473
                for entry in content['entries']
                if not mail_host or entry['mail_host'] == mail_host]
474

475
    def find_users(self, query):
476
477
478
479
480
481
482
483
484
485
486
487
488
        """Find users with query string matching display name and emails.

        :param str query: The string to search for. The search string is case
            insensitive as core doens't care about the case.
        :param int count: Number of entries per-page (defaults to 50).
        :param int page: The page number to return (defaults to 1).
        """
        url = 'users/find?q={}'.format(query)
        response, content = self._connection.call(url)
        if 'entries' not in content:
            return []
        return [User(self._connection, entry['self_link'], entry)
                for entry in content['entries']]
489
490
491
492
493
494
495
496
497
498
499

    def find_users_page(self, query, count, page):
        """Same as :py:meth:`find_users` but allows for pagination.

        :param str query: The string to search for. The search string is case
            insensitive as core doens't care about the case.
        :param int count: Number of entries per-page (defaults to 50).
        :param int page: The page number to return (defaults to 1).
        """
        url = 'users/find?q={}'.format(query)
        return Page(self._connection, url, User, count, page)