# Copyright (C) 2011-2014 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 .
"""REST for addresses."""
from __future__ import absolute_import, print_function,unicode_literals
__metaclass__ = type
__all__ = [
'AllAddresses',
'AnAddress',
'UserAddresses',
]
from operator import attrgetter
from restish import http, resource
from zope.component import getUtility
from mailman.interfaces.address import (
ExistingAddressError, InvalidEmailAddressError)
from mailman.interfaces.usermanager import IUserManager
from mailman.rest.helpers import CollectionMixin, etag, no_content, path_to
from mailman.rest.members import MemberCollection
from mailman.rest.preferences import Preferences
from mailman.rest.validator import Validator
from mailman.utilities.datetime import now
class _AddressBase(resource.Resource, CollectionMixin):
"""Shared base class for address representations."""
def _resource_as_dict(self, address):
"""See `CollectionMixin`."""
# The canonical url for an address is its lower-cased version,
# although it can be looked up with either its original or lower-cased
# email address.
representation = dict(
email=address.email,
original_email=address.original_email,
registered_on=address.registered_on,
self_link=path_to('addresses/{0}'.format(address.email)),
)
# Add optional attributes. These can be None or the empty string.
if address.display_name:
representation['display_name'] = address.display_name
if address.verified_on:
representation['verified_on'] = address.verified_on
return representation
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(IUserManager).addresses)
class AllAddresses(_AddressBase):
"""The addresses."""
@resource.GET()
def collection(self, request):
"""/addresses"""
resource = self._make_collection(request)
return http.ok([], etag(resource))
class _VerifyResource(resource.Resource):
"""A helper resource for verify/unverify POSTS."""
def __init__(self, address, action):
self._address = address
self._action = action
assert action in ('verify', 'unverify')
@resource.POST()
def verify(self, request):
# We don't care about the POST data, just do the action.
if self._action == 'verify' and self._address.verified_on is None:
self._address.verified_on = now()
elif self._action == 'unverify':
self._address.verified_on = None
return no_content()
class AnAddress(_AddressBase):
"""An address."""
def __init__(self, email):
"""Get an address by either its original or lower-cased email.
:param email: The email address of the `IAddress`.
:type email: string
"""
self._address = getUtility(IUserManager).get_address(email)
@resource.GET()
def address(self, request):
"""Return a single address."""
if self._address is None:
return http.not_found()
return http.ok([], self._resource_as_json(self._address))
@resource.child()
def memberships(self, request, segments):
"""/addresses//memberships"""
if len(segments) != 0:
return http.bad_request()
if self._address is None:
return http.not_found()
return AddressMemberships(self._address)
@resource.child()
def preferences(self, request, segments):
"""/addresses//preferences"""
if len(segments) != 0:
return http.bad_request()
if self._address is None:
return http.not_found()
child = Preferences(
self._address.preferences,
'addresses/{0}'.format(self._address.email))
return child, []
@resource.child()
def verify(self, request, segments):
"""/addresses//verify"""
if len(segments) != 0:
return http.bad_request()
if self._address is None:
return http.not_found()
child = _VerifyResource(self._address, 'verify')
return child, []
@resource.child()
def unverify(self, request, segments):
"""/addresses//verify"""
if len(segments) != 0:
return http.bad_request()
if self._address is None:
return http.not_found()
child = _VerifyResource(self._address, 'unverify')
return child, []
class UserAddresses(_AddressBase):
"""The addresses of a user."""
def __init__(self, user):
self._user = user
super(UserAddresses, self).__init__()
def _get_collection(self, request):
"""See `CollectionMixin`."""
return sorted(self._user.addresses,
key=attrgetter('original_email'))
@resource.GET()
def collection(self, request):
"""/addresses"""
resource = self._make_collection(request)
return http.ok([], etag(resource))
@resource.POST()
def create(self, request):
"""POST to /addresses
Add a new address to the user record.
"""
if self._user is None:
return http.not_found()
user_manager = getUtility(IUserManager)
validator = Validator(email=unicode,
display_name=unicode,
_optional=('display_name',))
try:
address = user_manager.create_address(**validator(request))
except ValueError as error:
return http.bad_request([], str(error))
except InvalidEmailAddressError:
return http.bad_request([], b'Invalid email address')
except ExistingAddressError:
return http.bad_request([], b'Address already exists')
else:
# Link the address to the current user and return it.
address.user = self._user
location = path_to('addresses/{0}'.format(address.email))
return http.created(location, [], None)
def membership_key(member):
# Sort first by mailing list, then by address, then by role.
return member.list_id, member.address.email, member.role.value
class AddressMemberships(MemberCollection):
"""All the memberships of a particular email address."""
def __init__(self, address):
super(AddressMemberships, self).__init__()
self._address = address
def _get_collection(self, request):
"""See `CollectionMixin`."""
# XXX Improve this by implementing a .memberships attribute on
# IAddress, similar to the way IUser does it.
#
# Start by getting the IUser that controls this address. For now, if
# the address is not controlled by a user, return the empty set.
# Later when we address the XXX comment, it will return some
# memberships. But really, it should not be legal to subscribe an
# address to a mailing list that isn't controlled by a user -- maybe!
user = getUtility(IUserManager).get_user(self._address.email)
if user is None:
return []
return sorted((member for member in user.memberships.members
if member.address == self._address),
key=membership_key)