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
39
from mailmanclient import __version__
35
from six.moves.urllib_error import HTTPError
36
from six.moves.urllib_parse import urlencode, urljoin
42
39
DEFAULT_PAGE_ITEM_COUNT = 50
45
42
class MailmanConnectionError(Exception):
47
43
"""Custom Exception to catch connection errors."""
53
47
"""A connection to the REST client."""
55
49
def __init__(self, baseurl, name=None, password=None):
74
68
self.basic_auth = None
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')
79
73
def call(self, path, data=None, method=None):
80
74
"""Make a call to the Mailman REST API.
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:
146
140
def system(self):
147
return self._connection.call('system')[1]
141
return self._connection.call('system/versions')[1]
150
144
def preferences(self):
151
145
return _Preferences(self._connection, 'system/preferences')
149
response, content = self._connection.call('queues')
151
for entry in content['entries']:
152
queues[entry['name']] = _Queue(self._connection, entry)
155
157
response, content = self._connection.call('lists')
156
158
if 'entries' not in content:
209
211
return _Domain(self._connection, response['location'])
211
213
def delete_domain(self, mail_host):
212
response, content = self._connection.call('domains/{0}'
214
response, content = self._connection.call(
215
'domains/{0}'.format(mail_host), None, 'DELETE')
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:
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
236
display_name=display_name))
236
237
return _User(self._connection, response['location'])
319
320
def __init__(self, connection, url, data=None):
320
321
self._connection = connection
326
325
def __repr__(self):
327
326
return '<List "{0}">'.format(self.fqdn_listname)
412
"""Return a list of dicts with held message information.
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:
431
429
def requests(self):
432
"""Return a list of dicts with subscription requests.
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.
482
path = 'lists/{0}/held/{1}'.format(self.fqdn_listname,
484
response, content = self._connection.call(path, dict(action=action),
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')
488
485
def discard_message(self, request_id):
489
"""Shortcut for moderate_message.
486
"""Shortcut for moderate_message."""
491
487
return self.moderate_message(request_id, 'discard')
493
489
def reject_message(self, request_id):
494
"""Shortcut for moderate_message.
490
"""Shortcut for moderate_message."""
496
491
return self.moderate_message(request_id, 'reject')
498
493
def defer_message(self, request_id):
499
"""Shortcut for moderate_message.
494
"""Shortcut for moderate_message."""
501
495
return self.moderate_message(request_id, 'defer')
503
497
def accept_message(self, request_id):
504
"""Shortcut for moderate_message.
498
"""Shortcut for moderate_message."""
506
499
return self.moderate_message(request_id, 'accept')
508
501
def get_member(self, email):
516
509
for member in self.members:
517
510
if member.email == email:
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,
538
530
response, content = self._connection.call('members', data)
539
531
return _Member(self._connection, response['location'])
634
625
self._cleartext_password = None
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)
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})
691
681
for entry in content['entries']:
692
682
subscriptions.append(_Member(self._connection,
713
703
return self._preferences
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
730
720
def delete(self):
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
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
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
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')
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 = (
867
'digest_last_sent_at',
876
'next_digest_number',
949
959
def _create_page(self):
950
960
self._entries = []
952
path = '{0}?count={1}&page={2}'.format(self._path, self._count,
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']:
974
984
self._create_page()
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']
996
return '<Queue: {}>'.format(self.name)
998
def inject(self, list_id, text):
999
self._connection.call(self.url, dict(list_id=list_id, text=text))
1003
response, content = self._connection.call(self.url)
1004
return content['files']