diff --git a/mailmanclient/__init__.py b/mailmanclient/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/mailmanclient/__init__.py diff --git a/mailmanclient/rest.py b/mailmanclient/rest.py new file mode 100644 index 0000000..dd82ec3 --- /dev/null +++ b/mailmanclient/rest.py @@ -0,0 +1,393 @@ +# Copyright (C) 2010 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see . + +"""A client library for the Mailman REST API.""" + + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'MailmanRESTClient', + 'MailmanRESTClientError', + ] + + +import re +import json +import os.path + +from base64 import b64encode +from httplib2 import Http +from operator import itemgetter +from urllib import urlencode +from urllib2 import HTTPError + + +class MailmanRESTClientError(Exception): + """An exception thrown by the Mailman REST API client.""" + + +class MailmanRESTClient(): + """A wrapper for the Mailman REST API.""" + + def __init__(self, host, username, password): + """Check and modify the host name. Also, try to parse a config file to + get the API credentials. + + :param host: the host name of the REST API + :type host: string + :param username: auth user name for the rest api + :type: username: string + :param password: pwd for the rest api + :type: password: string + :return: a MailmanRESTClient object + :rtype: objectFirst line should + """ + self.host = host + self.username = username + self.password = password + # If there is a trailing slash remove it + if self.host[-1] == '/': + self.host = self.host[:-1] + # If there is no protocol, fall back to http:// + if self.host[0:4] != 'http': + self.host = 'http://' + self.host + + def __repr__(self): + return '' % self.host + + def _http_request(self, path, data=None, method=None): + """Send an HTTP request. + + :param path: the path to send the request to + :type path: string + :param data: POST oder PUT data to send + :type data: dict + :param method: the HTTP method; defaults to GET or POST (if + data is not None) + :type method: string + :return: the request content or a status code, depending on the + method and if the request was successful + :rtype: int, list or dict + """ + url = self.host + path + # Include general header information + headers = { + 'User-Agent': 'MailmanRESTClient', + 'Accept': 'text/plain', + } + if data is not None: + data = urlencode(data, doseq=True) + headers['Content-type'] = "application/x-www-form-urlencoded" + if method is None: + if data is None: + method = 'GET' + else: + method = 'POST' + method = method.upper() + basic_auth = '{0}:{1}'.format(self.username, self.password) + headers['Authorization'] = 'Basic ' + b64encode(basic_auth) + response, content = Http().request(url, method, data, headers) + + if method == 'GET': + if response.status // 100 != 2: + raise HTTPError(url, response.status, content, response, None) + else: + return json.loads(content) + else: + return response.status + + def create_domain(self, email_host): + """Create a domain and return a domain object. + + :param email_host: The host domain to create + :type email_host: string + :return: A domain object or a status code (if the create + request failed) + :rtype int or object + """ + data = { + 'email_host': email_host, + } + response = self._http_request('/3.0/domains', data, 'POST') + if response == 201: + return _Domain(self.host, self.username, self.password, email_host) + else: + return response + + def get_domain(self, email_host): + """Return a domain object. + + :param email_host: host domain + :type email_host: string + :rtype object + """ + return _Domain(self.host, self.username, self.password, email_host) + + def get_lists(self): + """Get a list of all mailing list. + + :return: a list of dicts with all mailing lists + :rtype: list + """ + response = self._http_request('/3.0/lists') + if 'entries' not in response: + return [] + else: + # Return a dict with entries sorted by fqdn_listname + return sorted(response['entries'], + key=itemgetter('fqdn_listname')) + + def get_list(self, fqdn_listname): + """Find and return a list object. + + :param fqdn_listname: the mailing list address + :type fqdn_listname: string + :rtype: object + """ + return _List(self.host, self.username, self.password, fqdn_listname) + + def get_members(self): + """Get a list of all list members. + + :return: a list of dicts with the members of all lists + :rtype: list + """ + response = self._http_request('/3.0/members') + if 'entries' not in response: + return [] + else: + return sorted(response['entries'], + key=itemgetter('self_link')) + + def get_member(self, email_address, fqdn_listname): + """Return a member object. + + :param email_adresses: the email address used + :type email_address: string + :param fqdn_listname: the mailing list + :type fqdn_listname: string + :return: a member object + :rtype: _Member + """ + return _Member(self.host, self.username, self.password, email_address, fqdn_listname) + + + def get_user(self, email_address): + """Find and return a user object. + + :param email_address: one of the user's email addresses + :type email: string: + :returns: a user object + :rtype: _User + """ + return _User(self.host, self.username, self.password, email_address) + + +class _Domain(MailmanRESTClient): + """A domain wrapper for the MailmanRESTClient.""" + + def __init__(self, host, username, password, email_host): + """Connect to host and get list information. + + :param host: the host name of the REST API + :type host: string + :param username: auth user name for the rest api + :type: username: string + :param password: pwd for the rest api + :type: password: string + :param email_host: host domain + :type email_host: string + :rtype: object + """ + super(_Domain, self).__init__(host, username, password) + self.info = self._http_request('/3.0/domains/' + email_host) + + def create_list(self, list_name): + """Create a mailing list and return a list object. + + :param list_name: the name of the list to be created + :type list_name: string + :rtype: object + """ + fqdn_listname = list_name + '@' + self.info['email_host'] + data = { + 'fqdn_listname': fqdn_listname + } + response = self._http_request('/3.0/lists', data, 'POST') + return _List(self.host, self.username, self.password, fqdn_listname) + + def delete_list(self, list_name): + fqdn_listname = list_name + '@' + self.info['email_host'] + return self._http_request('/3.0/lists/' + fqdn_listname, None, 'DELETE') + + +class _List(MailmanRESTClient): + """A mailing list wrapper for the MailmanRESTClient.""" + + def __init__(self, host, username, password, fqdn_listname): + """Connect to host and get list information. + + :param host: the host name of the REST API + :type host: string + :param username: auth user name for the rest api + :type: username: string + :param password: pwd for the rest api + :type: password: string + :param fqdn_listname: the mailing list address + :type fqdn_listname: string + :rtype: object + """ + super(_List, self).__init__(host, username, password) + self.info = self._http_request('/3.0/lists/' + fqdn_listname) + self.config = self._http_request('/3.0/lists/%s/config' % fqdn_listname) + + def subscribe(self, address, real_name=None): + """Add an address to a list. + + :param address: email address to add to the list. + :type address: string + :param real_name: the real name of the new member + :type real_name: string + """ + data = { + 'fqdn_listname': self.info['fqdn_listname'], + 'address': address, + 'real_name': real_name + } + return self._http_request('/3.0/members', data, 'POST') + + def unsubscribe(self, address): + """Unsubscribe an address to a list. + + :param address: email address to add to the list. + :type address: string + :param real_name: the real name of the new member + :type real_name: string + """ + return self._http_request('/3.0/lists/' + + self.info['fqdn_listname'] + + '/member/' + + address, + None, + 'DELETE') + + def get_members(self): + """Get a list of all list members. + + :return: a list of dicts with all members + :rtype: list + """ + response = self._http_request('/3.0/lists/' + + self.info['fqdn_listname'] + + '/roster/members') + if 'entries' not in response: + return [] + else: + return sorted(response['entries'], + key=itemgetter('self_link')) + + def get_member(self, email_address): + """Return a member object. + + :param email_adresses: the email address used + :type email_address: string + :param fqdn_listname: the mailing list + :type fqdn_listname: string + :return: a member object + :rtype: _Member + """ + return _Member(self.host, self.username, self.password, email_address, self.info['fqdn_listname']) + + def update_list(self, data): + """Update the settings for a list. + """ + return self._http_request('/3.0/lists/' + self.info['fqdn_listname'], + data, method='PATCH') + + def update_config(self, data): + """Update the list configuration. + + :param data: A dict with the config attributes to be updated. + :type data: dict + :return: the return code of the http request + :rtype: integer + """ + url = '/3.0/lists/%s/config' % self.info['fqdn_listname'] + status = self._http_request(url, data, 'PATCH') + if status == 200: + for key in data: + self.config[key] = data[key] + return status + + def __str__(self): + """A string representation of a list. + """ + return "A list object for the list '%s'." % self.info['fqdn_listname'] + + +class _Member(MailmanRESTClient): + """A user wrapper for the MailmanRESTClient.""" + + def __init__(self, host, username, password, email_address, fqdn_listname): + """Connect to host and get membership information. + + :param host: the host name of the REST API + :type host: string + :param username: auth user name for the rest api + :type: username: string + :param password: pwd for the rest api + :type: password: string + :param email_address: email address + :type email_address: string + :param fqdn_listname: the mailing list + :type fqdn_listname: string + :return: a member object + :rtype: _Member + """ + super(_Member, self).__init__(host, username, password) + # Create an info attribute to receive member info via the API. + # To be filled with actual data as soon as the API supports it. + self.info = {} + # Keep these values out of the info dict so info can be send + # back to the server without additional keys + self.email_address = email_address + self.fqdn_listname = fqdn_listname + self.info = self._http_request('/3.0/lists/%s/member/%s' % (fqdn_listname, email_address)) + + def unsubscribe(self, fqdn_listname): + url = '/3.0/lists/%s/member/%s' % (fqdn_listname, 'jack@example.com') + return self._http_request(url, None, 'DELETE') + + def update(self, data=None): + """Update member settings.""" + + # If data is None, use the info dict + if data is None: + data = self.info + + path = '/3.0/lists/%s/member/%s' % (self.fqdn_listname, + self.email_address) + return self._http_request(path, data, method='PATCH') + + def __str__(self): + """A string representation of a member.""" + + return "A member object for '%s', subscribed to '%s'." \ + % (self.email_address, self.fqdn_listname) + diff --git a/static/mailman_django/css/style.css b/static/mailman_django/css/style.css index 61cf659..437dc53 100755 --- a/static/mailman_django/css/style.css +++ b/static/mailman_django/css/style.css @@ -3,18 +3,53 @@ /************************* * Reset *************************/ -html { - height: 101%; +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; } -img { - border: none; +/* remember to define focus styles! */ +:focus { + outline: 0; } -ul, -li { - list-style: none; - margin: 0; - padding: 0; +body { + line-height: 1; + color: black; + background: white; } +ol, ul { + list-style: none; +} +/* tables still need 'cellspacing="0"' in the markup */ +table { + border-collapse: separate; + border-spacing: 0; +} +caption, th, td { + text-align: left; + font-weight: normal; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ""; +} +blockquote, q { + quotes: "" ""; +} + /************************* * Fonts @@ -176,3 +211,6 @@ } } + +@media screen and (max-width: 600px) { +}