summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2011-04-19 14:52:16 -0400
committerBarry Warsaw2011-04-19 14:52:16 -0400
commit6b4a3ebc37e5e11d74161fed12b3cf75eca6c339 (patch)
treea9267a6748c5bedeacc88f383f88c435b2f21a91 /src
parent23bed0392f25df18478c01aa991c289b83069307 (diff)
downloadmailman-6b4a3ebc37e5e11d74161fed12b3cf75eca6c339.tar.gz
mailman-6b4a3ebc37e5e11d74161fed12b3cf75eca6c339.tar.zst
mailman-6b4a3ebc37e5e11d74161fed12b3cf75eca6c339.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/model/address.py2
-rw-r--r--src/mailman/model/docs/addresses.txt25
-rw-r--r--src/mailman/rest/addresses.py113
-rw-r--r--src/mailman/rest/docs/addresses.txt134
-rw-r--r--src/mailman/rest/docs/users.txt81
-rw-r--r--src/mailman/rest/root.py13
-rw-r--r--src/mailman/rest/users.py52
7 files changed, 333 insertions, 87 deletions
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py
index 92f7f8986..0ba0f47d5 100644
--- a/src/mailman/model/address.py
+++ b/src/mailman/model/address.py
@@ -31,6 +31,7 @@ from zope.interface import implements
from mailman.database.model import Model
from mailman.interfaces.address import IAddress
+from mailman.utilities.datetime import now
@@ -55,6 +56,7 @@ class Address(Model):
self.email = lower_case
self.real_name = real_name
self._original = (None if lower_case == email else email)
+ self.registered_on = now()
def __str__(self):
addr = (self.email if self._original is None else self._original)
diff --git a/src/mailman/model/docs/addresses.txt b/src/mailman/model/docs/addresses.txt
index ffbb897ab..01e68c954 100644
--- a/src/mailman/model/docs/addresses.txt
+++ b/src/mailman/model/docs/addresses.txt
@@ -126,31 +126,28 @@ Registration and validation
===========================
Addresses have two dates, the date the address was registered on and the date
-the address was validated on. Neither date is set by default.
+the address was validated on. The former is set when the address is created,
+but the latter must be set explicitly.
>>> address_4 = user_manager.create_address(
... 'dperson@example.com', 'Dan Person')
>>> print address_4.registered_on
- None
+ 2005-08-01 07:49:23
>>> print address_4.verified_on
None
-The registered date takes a Python datetime object.
+The verification date records when the user has completed a mail-back
+verification procedure. It takes a datetime object.
- >>> from datetime import datetime
- >>> address_4.registered_on = datetime(2007, 5, 8, 22, 54, 1)
- >>> print address_4.registered_on
- 2007-05-08 22:54:01
+ >>> from mailman.utilities.datetime import now
+ >>> address_4.verified_on = now()
>>> print address_4.verified_on
- None
+ 2005-08-01 07:49:23
-And of course, you can also set the validation date.
+The address shows the verified status in its repr.
- >>> address_4.verified_on = datetime(2007, 5, 13, 22, 54, 1)
- >>> print address_4.registered_on
- 2007-05-08 22:54:01
- >>> print address_4.verified_on
- 2007-05-13 22:54:01
+ >>> address_4
+ <Address: Dan Person <dperson@example.com> [verified] at ...>
Case-preserved addresses
diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py
new file mode 100644
index 000000000..d7fae3b9b
--- /dev/null
+++ b/src/mailman/rest/addresses.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2011 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 <http://www.gnu.org/licenses/>.
+
+"""REST for addresses."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'AllAddresses',
+ 'AnAddress',
+ 'UserAddresses',
+ ]
+
+
+from operator import attrgetter
+from restish import http, resource
+from zope.component import getUtility
+
+from mailman.rest.helpers import CollectionMixin, etag, path_to
+from mailman.interfaces.usermanager import IUserManager
+
+
+
+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.real_name:
+ representation['real_name'] = address.real_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 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))
+
+
+
+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))
diff --git a/src/mailman/rest/docs/addresses.txt b/src/mailman/rest/docs/addresses.txt
new file mode 100644
index 000000000..dbd1ecf86
--- /dev/null
+++ b/src/mailman/rest/docs/addresses.txt
@@ -0,0 +1,134 @@
+=========
+Addresses
+=========
+
+The REST API can be used to manage addresses.
+
+There are no addresses yet.
+
+ >>> dump_json('http://localhost:9001/3.0/addresses')
+ http_etag: "..."
+ start: 0
+ total_size: 0
+
+When an address is created via the internal API, it is available in the REST
+API.
+::
+
+ >>> from zope.component import getUtility
+ >>> from mailman.interfaces.usermanager import IUserManager
+ >>> user_manager = getUtility(IUserManager)
+ >>> anne = user_manager.create_address('anne@example.com')
+ >>> transaction.commit()
+
+ >>> dump_json('http://localhost:9001/3.0/addresses')
+ entry 0:
+ email: anne@example.com
+ http_etag: "..."
+ original_email: anne@example.com
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/anne@example.com
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+Anne's address can also be accessed directly.
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com')
+ email: anne@example.com
+ http_etag: "..."
+ original_email: anne@example.com
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/anne@example.com
+
+Bart registers with a mixed-case address. The canonical URL always includes
+the lower-case version.
+
+ >>> bart = user_manager.create_address('Bart.Person@example.com')
+ >>> transaction.commit()
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/addresses/bart.person@example.com')
+ email: bart.person@example.com
+ http_etag: "..."
+ original_email: Bart.Person@example.com
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/bart.person@example.com
+
+But his address record can be accessed with the case-preserved version too.
+
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/addresses/Bart.Person@example.com')
+ email: bart.person@example.com
+ http_etag: "..."
+ original_email: Bart.Person@example.com
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/bart.person@example.com
+
+A non-existent email address can't be retrieved.
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/nobody@example.com')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 404: 404 Not Found
+
+When an address has a real name associated with it, this is also available in
+the REST API.
+
+ >>> cris = user_manager.create_address('cris@example.com', 'Cris Person')
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/addresses/cris@example.com')
+ email: cris@example.com
+ http_etag: "..."
+ original_email: cris@example.com
+ real_name: Cris Person
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/cris@example.com
+
+
+Verifying
+=========
+
+When the address gets verified, this attribute is available in the REST
+representation.
+::
+
+ >>> from mailman.utilities.datetime import now
+ >>> anne.verified_on = now()
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com')
+ email: anne@example.com
+ http_etag: "..."
+ original_email: anne@example.com
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/anne@example.com
+ verified_on: 2005-08-01T07:49:23
+
+
+User addresses
+==============
+
+Users control addresses. The canonical URLs for these user-controlled
+addresses live in the /addresses namespace.
+::
+
+ >>> dave = user_manager.create_user('dave@example.com', 'Dave Person')
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/users/dave@example.com/addresses')
+ entry 0:
+ email: dave@example.com
+ http_etag: "..."
+ original_email: dave@example.com
+ real_name: Dave Person
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/dave@example.com
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+ >>> dump_json('http://localhost:9001/3.0/addresses/dave@example.com')
+ email: dave@example.com
+ http_etag: "..."
+ original_email: dave@example.com
+ real_name: Dave Person
+ registered_on: 2005-08-01T07:49:23
+ self_link: http://localhost:9001/3.0/addresses/dave@example.com
diff --git a/src/mailman/rest/docs/users.txt b/src/mailman/rest/docs/users.txt
index 4a73ab8d8..114ca4e49 100644
--- a/src/mailman/rest/docs/users.txt
+++ b/src/mailman/rest/docs/users.txt
@@ -3,7 +3,7 @@ Users
=====
The REST API can be used to add and remove users, add and remove user
-addresses, and change their preferred address, passord, or name. Users are
+addresses, and change their preferred address, password, or name. Users are
different than members; the latter represents an email address subscribed to a
specific mailing list. Users are just people that Mailman knows about.
@@ -27,7 +27,6 @@ When there are users in the database, they can be retrieved as a collection.
entry 0:
created_on: 2005-08-01T07:49:23
http_etag: "..."
- password: None
real_name: Anne Person
self_link: http://localhost:9001/3.0/users/1
user_id: 1
@@ -41,6 +40,27 @@ The user ids match.
>>> json['entries'][0]['user_id'] == anne.user_id
True
+A user might not have a real name, in which case, the attribute will not be
+returned in the REST API.
+
+ >>> dave = user_manager.create_user('dave@example.com')
+ >>> transaction.commit()
+ >>> dump_json('http://localhost:9001/3.0/users')
+ entry 0:
+ created_on: 2005-08-01T07:49:23
+ http_etag: "..."
+ real_name: Anne Person
+ self_link: http://localhost:9001/3.0/users/1
+ user_id: 1
+ entry 1:
+ created_on: 2005-08-01T07:49:23
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/users/2
+ user_id: 2
+ http_etag: "..."
+ start: 0
+ total_size: 2
+
Creating users via the API
==========================
@@ -57,7 +77,7 @@ email address for the user, and optionally the user's full name and password.
... })
content-length: 0
date: ...
- location: http://localhost:9001/3.0/users/2
+ location: http://localhost:9001/3.0/users/3
server: ...
status: 201
@@ -66,17 +86,17 @@ The user exists in the database.
>>> bart = user_manager.get_user('bart@example.com')
>>> bart
- <User "Bart Person" (2) at ...>
+ <User "Bart Person" (3) at ...>
It is also available via the location given in the response.
- >>> dump_json('http://localhost:9001/3.0/users/2')
+ >>> dump_json('http://localhost:9001/3.0/users/3')
created_on: 2005-08-01T07:49:23
http_etag: "..."
password: {CLEARTEXT}bbb
real_name: Bart Person
- self_link: http://localhost:9001/3.0/users/2
- user_id: 2
+ self_link: http://localhost:9001/3.0/users/3
+ user_id: 3
Because email addresses just have an ``@`` sign in then, there's no confusing
them with user ids. Thus, a user can be retrieved via its email address.
@@ -86,8 +106,8 @@ them with user ids. Thus, a user can be retrieved via its email address.
http_etag: "..."
password: {CLEARTEXT}bbb
real_name: Bart Person
- self_link: http://localhost:9001/3.0/users/2
- user_id: 2
+ self_link: http://localhost:9001/3.0/users/3
+ user_id: 3
Users can be created without a password. A *user friendly* password will be
assigned to them automatically, but this password will be encrypted and
@@ -101,17 +121,17 @@ therefore cannot be retrieved. It can be reset though.
... })
content-length: 0
date: ...
- location: http://localhost:9001/3.0/users/3
+ location: http://localhost:9001/3.0/users/4
server: ...
status: 201
- >>> dump_json('http://localhost:9001/3.0/users/3')
+ >>> dump_json('http://localhost:9001/3.0/users/4')
created_on: 2005-08-01T07:49:23
http_etag: "..."
password: {CLEARTEXT}...
real_name: Cris Person
- self_link: http://localhost:9001/3.0/users/3
- user_id: 3
+ self_link: http://localhost:9001/3.0/users/4
+ user_id: 4
Missing users
@@ -152,40 +172,33 @@ sorted in lexical order by original (i.e. case-preserved) email address.
key: bart.q.person@example.com at ...>
>>> transaction.commit()
- >>> dump_json('http://localhost:9001/3.0/users/2/addresses')
+ >>> dump_json('http://localhost:9001/3.0/users/3/addresses')
entry 0:
email: bart.q.person@example.com
http_etag: "..."
original_email: Bart.Q.Person@example.com
- real_name:
- registered_on: None
+ registered_on: 2005-08-01T07:49:23
self_link:
- http://localhost:9001/3.0/addresses/Bart.Q.Person@example.com
- verified_on: None
+ http://localhost:9001/3.0/addresses/bart.q.person@example.com
entry 1:
email: bart.person@example.com
http_etag: "..."
original_email: bart.person@example.com
- real_name:
- registered_on: None
+ registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/bart.person@example.com
- verified_on: None
entry 2:
email: bart@example.com
http_etag: "..."
original_email: bart@example.com
real_name: Bart Person
- registered_on: None
+ registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/bart@example.com
- verified_on: None
entry 3:
email: bperson@example.com
http_etag: "..."
original_email: bperson@example.com
- real_name:
- registered_on: None
+ registered_on: 2005-08-01T07:49:23
self_link: http://localhost:9001/3.0/addresses/bperson@example.com
- verified_on: None
http_etag: "..."
start: 0
total_size: 4
@@ -198,29 +211,29 @@ In fact, any of these addresses can be used to look up Bart's user record.
http_etag: "..."
password: {CLEARTEXT}bbb
real_name: Bart Person
- self_link: http://localhost:9001/3.0/users/2
- user_id: 2
+ self_link: http://localhost:9001/3.0/users/3
+ user_id: 3
>>> dump_json('http://localhost:9001/3.0/users/bart.person@example.com')
created_on: 2005-08-01T07:49:23
http_etag: "..."
password: {CLEARTEXT}bbb
real_name: Bart Person
- self_link: http://localhost:9001/3.0/users/2
- user_id: 2
+ self_link: http://localhost:9001/3.0/users/3
+ user_id: 3
>>> dump_json('http://localhost:9001/3.0/users/bperson@example.com')
created_on: 2005-08-01T07:49:23
http_etag: "..."
password: {CLEARTEXT}bbb
real_name: Bart Person
- self_link: http://localhost:9001/3.0/users/2
- user_id: 2
+ self_link: http://localhost:9001/3.0/users/3
+ user_id: 3
>>> dump_json('http://localhost:9001/3.0/users/Bart.Q.Person@example.com')
created_on: 2005-08-01T07:49:23
http_etag: "..."
password: {CLEARTEXT}bbb
real_name: Bart Person
- self_link: http://localhost:9001/3.0/users/2
- user_id: 2
+ self_link: http://localhost:9001/3.0/users/3
+ user_id: 3
diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py
index 3287a6be2..99f28cbb2 100644
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -30,6 +30,7 @@ from restish import guard, http, resource
from mailman.config import config
from mailman.core.system import system
+from mailman.rest.addresses import AllAddresses, AnAddress
from mailman.rest.domains import ADomain, AllDomains
from mailman.rest.helpers import etag, path_to
from mailman.rest.lists import AList, AllLists
@@ -80,6 +81,18 @@ class TopLevel(resource.Resource):
return http.ok([], etag(resource))
@resource.child()
+ def addresses(self, request, segments):
+ """/<api>/addresses
+ /<api>/addresses/<email>
+ """
+ if len(segments) == 0:
+ return AllAddresses()
+ elif len(segments) == 1:
+ return AnAddress(segments[0]), []
+ else:
+ return http.bad_request()
+
+ @resource.child()
def domains(self, request, segments):
"""/<api>/domains
/<api>/domains/<domain>
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index 14ed04316..a75b448e7 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -26,12 +26,12 @@ __all__ = [
]
-from operator import attrgetter
from restish import http, resource
from zope.component import getUtility
from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
+from mailman.rest.addresses import UserAddresses
from mailman.rest.helpers import CollectionMixin, etag, path_to
from mailman.rest.validator import Validator
from mailman.utilities.passwords import (
@@ -44,16 +44,21 @@ class _UserBase(resource.Resource, CollectionMixin):
def _resource_as_dict(self, user):
"""See `CollectionMixin`."""
- # The canonical URL for a user is their preferred email address,
- # although we can always look up a user based on any registered and
- # validated email address associated with their account.
- return dict(
- real_name=user.real_name,
- password=user.password,
+ # The canonical URL for a user is their unique user id, although we
+ # can always look up a user based on any registered and validated
+ # email address associated with their account.
+ resource = dict(
user_id=user.user_id,
created_on=user.created_on,
self_link=path_to('users/{0}'.format(user.user_id)),
)
+ # Add the password attribute, only if the user has a password. Same
+ # with the real name. These could be None or the empty string.
+ if user.password:
+ resource['password'] = user.password
+ if user.real_name:
+ resource['real_name'] = user.real_name
+ return resource
def _get_collection(self, request):
"""See `CollectionMixin`."""
@@ -128,35 +133,4 @@ class AUser(_UserBase):
@resource.child()
def addresses(self, request, segments):
"""/users/<uid>/addresses"""
- return _AllUserAddresses(self._user)
-
-
-
-class _AllUserAddresses(resource.Resource, CollectionMixin):
- """All addresses that a user controls."""
-
- def __init__(self, user):
- self._user = user
- super(_AllUserAddresses, self).__init__()
-
- def _resource_as_dict(self, address):
- """See `CollectionMixin`."""
- return dict(
- email=address.email,
- original_email=address.original_email,
- real_name=address.real_name,
- registered_on=address.registered_on,
- self_link=path_to('addresses/{0}'.format(address.original_email)),
- verified_on=address.verified_on,
- )
-
- 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))
+ return UserAddresses(self._user)