~raj-abhilash1/mailman.client/domainowner

« back to all changes in this revision

Viewing changes to src/mailmanclient/_client.py

  • Committer: Florian Fuchs
  • Date: 2015-01-20 20:48:26 UTC
  • mfrom: (59.1.10 bilingual)
  • Revision ID: flo.fuchs@gmail.com-20150120204826-6nigz383gx9d006z
Merged ~barry/mailman.client/bilingual:
  * Added support for Python3.4
  * Use tox and vcrpy for testing

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2010-2014 by the Free Software Foundation, Inc.
 
1
# Copyright (C) 2010-2015 by the Free Software Foundation, Inc.
2
2
#
3
3
# This file is part of mailman.client.
4
4
#
25
25
]
26
26
 
27
27
 
28
 
import re
 
28
import six
29
29
import json
30
30
 
31
31
from base64 import b64encode
32
32
from httplib2 import Http
 
33
from mailmanclient import __version__
33
34
from operator import itemgetter
34
 
from urllib import urlencode
35
 
from urllib2 import HTTPError
36
 
from urlparse import urljoin
37
 
 
38
 
 
39
 
from mailmanclient import __version__
 
35
from six.moves.urllib_error import HTTPError
 
36
from six.moves.urllib_parse import urlencode, urljoin
40
37
 
41
38
 
42
39
DEFAULT_PAGE_ITEM_COUNT = 50
43
40
 
44
41
 
45
42
class MailmanConnectionError(Exception):
46
 
 
47
43
    """Custom Exception to catch connection errors."""
48
 
    pass
49
44
 
50
45
 
51
46
class _Connection:
52
 
 
53
47
    """A connection to the REST client."""
54
48
 
55
49
    def __init__(self, baseurl, name=None, password=None):
74
68
            self.basic_auth = None
75
69
        else:
76
70
            auth = '{0}:{1}'.format(name, password)
77
 
            self.basic_auth = b64encode(auth)
 
71
            self.basic_auth = b64encode(auth.encode('utf-8')).decode('utf-8')
78
72
 
79
73
    def call(self, path, data=None, method=None):
80
74
        """Make a call to the Mailman REST API.
93
87
        """
94
88
        headers = {
95
89
            'User-Agent': 'GNU Mailman REST client v{0}'.format(__version__),
96
 
        }
 
90
            }
97
91
        if data is not None:
98
92
            data = urlencode(data, doseq=True)
99
93
            headers['Content-Type'] = 'application/x-www-form-urlencoded'
115
109
            if len(content) == 0:
116
110
                return response, None
117
111
            # XXX Work around for http://bugs.python.org/issue10038
118
 
            content = unicode(content)
 
112
            if isinstance(content, six.binary_type):
 
113
                content = content.decode('utf-8')
119
114
            return response, json.loads(content)
120
115
        except HTTPError:
121
116
            raise
124
119
 
125
120
 
126
121
class Client:
127
 
 
128
122
    """Access the Mailman REST API root."""
129
123
 
130
124
    def __init__(self, baseurl, name=None, password=None):
144
138
 
145
139
    @property
146
140
    def system(self):
147
 
        return self._connection.call('system')[1]
 
141
        return self._connection.call('system/versions')[1]
148
142
 
149
143
    @property
150
144
    def preferences(self):
151
145
        return _Preferences(self._connection, 'system/preferences')
152
146
 
153
147
    @property
 
148
    def queues(self):
 
149
        response, content = self._connection.call('queues')
 
150
        queues = {}
 
151
        for entry in content['entries']:
 
152
            queues[entry['name']] = _Queue(self._connection, entry)
 
153
        return queues
 
154
 
 
155
    @property
154
156
    def lists(self):
155
157
        response, content = self._connection.call('lists')
156
158
        if 'entries' not in content:
209
211
        return _Domain(self._connection, response['location'])
210
212
 
211
213
    def delete_domain(self, mail_host):
212
 
        response, content = self._connection.call('domains/{0}'
213
 
                                                  .format(mail_host),
214
 
                                                  None, 'DELETE')
 
214
        response, content = self._connection.call(
 
215
            'domains/{0}'.format(mail_host), None, 'DELETE')
215
216
 
216
217
    def get_domain(self, mail_host=None, web_host=None):
217
218
        """Get domain by its mail_host or its web_host."""
225
226
                # in Mailman3Alpha8
226
227
                if domain.base_url == web_host:
227
228
                    return domain
228
 
                    break
229
229
            else:
230
230
                return None
231
231
 
232
232
    def create_user(self, email, password, display_name=''):
233
233
        response, content = self._connection.call(
234
 
            'users', dict(email=email, password=password,
 
234
            'users', dict(email=email,
 
235
                          password=password,
235
236
                          display_name=display_name))
236
237
        return _User(self._connection, response['location'])
237
238
 
319
320
    def __init__(self, connection, url, data=None):
320
321
        self._connection = connection
321
322
        self._url = url
322
 
        self._info = None
323
 
        if data is not None:
324
 
            self._info = data
 
323
        self._info = data
325
324
 
326
325
    def __repr__(self):
327
326
        return '<List "{0}">'.format(self.fqdn_listname)
409
408
 
410
409
    @property
411
410
    def held(self):
412
 
        """Return a list of dicts with held message information.
413
 
        """
 
411
        """Return a list of dicts with held message information."""
414
412
        response, content = self._connection.call(
415
413
            'lists/{0}/held'.format(self.fqdn_listname), None, 'GET')
416
414
        if 'entries' not in content:
429
427
 
430
428
    @property
431
429
    def requests(self):
432
 
        """Return a list of dicts with subscription requests.
433
 
        """
 
430
        """Return a list of dicts with subscription requests."""
434
431
        response, content = self._connection.call(
435
432
            'lists/{0}/requests'.format(self.fqdn_listname), None, 'GET')
436
433
        if 'entries' not in content:
479
476
        :param action: Action to perform on held message.
480
477
        :type action: String.
481
478
        """
482
 
        path = 'lists/{0}/held/{1}'.format(self.fqdn_listname,
483
 
                                           str(request_id))
484
 
        response, content = self._connection.call(path, dict(action=action),
485
 
                                                  'POST')
 
479
        path = 'lists/{0}/held/{1}'.format(
 
480
            self.fqdn_listname, str(request_id))
 
481
        response, content = self._connection.call(
 
482
            path, dict(action=action), 'POST')
486
483
        return response
487
484
 
488
485
    def discard_message(self, request_id):
489
 
        """Shortcut for moderate_message.
490
 
        """
 
486
        """Shortcut for moderate_message."""
491
487
        return self.moderate_message(request_id, 'discard')
492
488
 
493
489
    def reject_message(self, request_id):
494
 
        """Shortcut for moderate_message.
495
 
        """
 
490
        """Shortcut for moderate_message."""
496
491
        return self.moderate_message(request_id, 'reject')
497
492
 
498
493
    def defer_message(self, request_id):
499
 
        """Shortcut for moderate_message.
500
 
        """
 
494
        """Shortcut for moderate_message."""
501
495
        return self.moderate_message(request_id, 'defer')
502
496
 
503
497
    def accept_message(self, request_id):
504
 
        """Shortcut for moderate_message.
505
 
        """
 
498
        """Shortcut for moderate_message."""
506
499
        return self.moderate_message(request_id, 'accept')
507
500
 
508
501
    def get_member(self, email):
516
509
        for member in self.members:
517
510
            if member.email == email:
518
511
                return member
519
 
                break
520
512
        else:
521
513
            raise ValueError('%s is not a member address of %s' %
522
514
                             (email, self.fqdn_listname))
534
526
            list_id=self.list_id,
535
527
            subscriber=address,
536
528
            display_name=display_name,
537
 
        )
 
529
            )
538
530
        response, content = self._connection.call('members', data)
539
531
        return _Member(self._connection, response['location'])
540
532
 
568
560
        self._preferences = None
569
561
 
570
562
    def __repr__(self):
571
 
        return '<Member "{0}" on "{1}">'.format(
572
 
            self.email, self.list_id)
 
563
        return '<Member "{0}" on "{1}">'.format(self.email, self.list_id)
573
564
 
574
565
    def _get_info(self):
575
566
        if self._info is None:
634
625
        self._cleartext_password = None
635
626
 
636
627
    def __repr__(self):
637
 
        return '<User "{0}" ({1})>'.format(
638
 
            self.display_name, self.user_id)
 
628
        return '<User "{0}" ({1})>'.format(self.display_name, self.user_id)
639
629
 
640
630
    def _get_info(self):
641
631
        if self._info is None:
685
675
        if self._subscriptions is None:
686
676
            subscriptions = []
687
677
            for address in self.addresses:
688
 
                response, content = self._connection.call('members/find',
689
 
                                                          data={'subscriber': address})
 
678
                response, content = self._connection.call(
 
679
                    'members/find', data={'subscriber': address})
690
680
                try:
691
681
                    for entry in content['entries']:
692
682
                        subscriptions.append(_Member(self._connection,
713
703
        return self._preferences
714
704
 
715
705
    def add_address(self, email):
716
 
        # Adds another email adress to the user record and returns an 
 
706
        # Adds another email adress to the user record and returns an
717
707
        # _Address object.
718
708
        url = '{0}/addresses'.format(self._url)
719
709
        self._connection.call(url, {'email': email})
723
713
        if self._cleartext_password is not None:
724
714
            data['cleartext_password'] = self._cleartext_password
725
715
        self.cleartext_password = None
726
 
        response, content = self._connection.call(self._url,
727
 
                                                  data, method='PATCH')
 
716
        response, content = self._connection.call(
 
717
            self._url, data, method='PATCH')
728
718
        self._info = None
729
719
 
730
720
    def delete(self):
741
731
 
742
732
    def _get_addresses(self):
743
733
        if self._addresses is None:
744
 
            response, content = self._connection.call('users/{0}/addresses'
745
 
                                                      .format(self._user_id))
 
734
            response, content = self._connection.call(
 
735
                'users/{0}/addresses'.format(self._user_id))
746
736
            if 'entries' not in content:
747
737
                self._addresses = []
748
738
            self._addresses = content['entries']
795
785
        return self._preferences
796
786
 
797
787
    def verify(self):
798
 
        self._connection.call('addresses/{0}/verify'
799
 
                              .format(self._address['email']), method='POST')
 
788
        self._connection.call('addresses/{0}/verify'.format(
 
789
            self._address['email']), method='POST')
800
790
        self._info = None
801
791
 
802
792
    def unverify(self):
803
 
        self._connection.call('addresses/{0}/unverify'
804
 
                              .format(self._address['email']), method='POST')
 
793
        self._connection.call('addresses/{0}/unverify'.format(
 
794
            self._address['email']), method='POST')
805
795
        self._info = None
806
796
 
807
797
 
812
802
    'hide_address',
813
803
    'preferred_language',
814
804
    'receive_list_copy',
815
 
    'receive_own_postings', )
 
805
    'receive_own_postings',
 
806
    )
816
807
 
817
 
PREF_READ_ONLY_ATTRS = ('http_etag', 'self_link')
 
808
PREF_READ_ONLY_ATTRS = (
 
809
    'http_etag',
 
810
    'self_link',
 
811
    )
818
812
 
819
813
 
820
814
class _Preferences:
843
837
        return self._preferences[key]
844
838
 
845
839
    def __iter__(self):
846
 
        for key in self._preferences.keys():
 
840
        for key in self._preferences:
847
841
            yield self._preferences[key]
848
842
 
849
843
    def __len__(self):
861
855
    def save(self):
862
856
        data = {}
863
857
        for key in self._preferences:
864
 
            if key not in PREF_READ_ONLY_ATTRS and self._preferences[key] is not None:
 
858
            if (key not in PREF_READ_ONLY_ATTRS
 
859
                    and self._preferences[key] is not None):
865
860
                data[key] = self._preferences[key]
866
861
        response, content = self._connection.call(self._url, data, 'PATCH')
867
862
 
868
863
 
869
 
LIST_READ_ONLY_ATTRS = ('bounces_address', 'created_at', 'digest_last_sent_at',
870
 
                        'fqdn_listname', 'http_etag', 'mail_host',
871
 
                        'join_address', 'last_post_at', 'leave_address',
872
 
                        'list_id', 'list_name', 'next_digest_number',
873
 
                        'no_reply_address', 'owner_address', 'post_id',
874
 
                        'posting_address', 'request_address', 'scheme',
875
 
                        'volume', 'web_host',)
 
864
LIST_READ_ONLY_ATTRS = (
 
865
    'bounces_address',
 
866
    'created_at',
 
867
    'digest_last_sent_at',
 
868
    'fqdn_listname',
 
869
    'http_etag',
 
870
    'join_address',
 
871
    'last_post_at',
 
872
    'leave_address',
 
873
    'list_id',
 
874
    'list_name',
 
875
    'mail_host',
 
876
    'next_digest_number',
 
877
    'no_reply_address',
 
878
    'owner_address',
 
879
    'post_id',
 
880
    'posting_address',
 
881
    'request_address',
 
882
    'scheme',
 
883
    'volume',
 
884
    'web_host',
 
885
    )
876
886
 
877
887
 
878
888
class _Settings:
892
902
            self._info = content
893
903
 
894
904
    def __iter__(self):
895
 
        for key in self._info.keys():
 
905
        for key in self._info:
896
906
            yield key
897
907
 
898
908
    def __getitem__(self, key):
949
959
    def _create_page(self):
950
960
        self._entries = []
951
961
        # create url
952
 
        path = '{0}?count={1}&page={2}'.format(self._path, self._count,
953
 
                                               self._page)
 
962
        path = '{0}?count={1}&page={2}'.format(
 
963
            self._path, self._count, self._page)
954
964
        response, content = self._connection.call(path)
955
965
        if 'entries' in content:
956
966
            for entry in content['entries']:
973
983
            self._page -= 1
974
984
            self._create_page()
975
985
            return self
 
986
 
 
987
 
 
988
class _Queue:
 
989
    def __init__(self, connection, entry):
 
990
        self._connection = connection
 
991
        self.name = entry['name']
 
992
        self.url = entry['self_link']
 
993
        self.directory = entry['directory']
 
994
 
 
995
    def __repr__(self):
 
996
        return '<Queue: {}>'.format(self.name)
 
997
 
 
998
    def inject(self, list_id, text):
 
999
        self._connection.call(self.url, dict(list_id=list_id, text=text))
 
1000
 
 
1001
    @property
 
1002
    def files(self):
 
1003
        response, content = self._connection.call(self.url)
 
1004
        return content['files']