summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/bounces.py2
-rw-r--r--src/mailman/app/docs/subscriptions.rst146
-rw-r--r--src/mailman/app/subscriptions.py (renamed from src/mailman/rest/adapters.py)60
-rw-r--r--src/mailman/config/configure.zcml4
-rw-r--r--src/mailman/docs/NEWS.rst4
-rw-r--r--src/mailman/interfaces/subscriptions.py (renamed from src/mailman/interfaces/membership.py)29
-rw-r--r--src/mailman/rest/docs/membership.rst (renamed from src/mailman/rest/docs/membership.txt)95
-rw-r--r--src/mailman/rest/members.py60
-rw-r--r--src/mailman/rest/root.py10
-rw-r--r--src/mailman/runners/outgoing.py2
10 files changed, 385 insertions, 27 deletions
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py
index e227dfc32..611ab48b9 100644
--- a/src/mailman/app/bounces.py
+++ b/src/mailman/app/bounces.py
@@ -44,8 +44,8 @@ from mailman.core.i18n import _
from mailman.email.message import OwnerNotification, UserNotification
from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.listmanager import IListManager
-from mailman.interfaces.membership import ISubscriptionService
from mailman.interfaces.pending import IPendable, IPendings
+from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.utilities.email import split_email
from mailman.utilities.i18n import make
from mailman.utilities.string import oneline
diff --git a/src/mailman/app/docs/subscriptions.rst b/src/mailman/app/docs/subscriptions.rst
new file mode 100644
index 000000000..378dd0a40
--- /dev/null
+++ b/src/mailman/app/docs/subscriptions.rst
@@ -0,0 +1,146 @@
+=====================
+Subscription services
+=====================
+
+The `ISubscriptionService` utility provides higher level convenience methods
+useful for searching, retrieving, iterating, adding, and removing
+memberships.
+
+ >>> from mailman.interfaces.subscriptions import ISubscriptionService
+ >>> from zope.component import getUtility
+ >>> service = getUtility(ISubscriptionService)
+
+You can use the service to get all members of all mailing lists, for any
+membership role. At first, there are no memberships.
+
+ >>> service.get_members()
+ []
+ >>> sum(1 for member in service)
+ 0
+ >>> print service.get_member(801)
+ None
+
+The service can be used to subscribe new members, but only with the `member`
+role. At a minimum, a mailing list and an address for the new user is
+required.
+
+ >>> mlist = create_list('test@example.com')
+ >>> anne = service.join('test@example.com', 'anne@example.com')
+ >>> anne
+ <Member: anne <anne@example.com> on test@example.com as MemberRole.member>
+
+The real name of the new member can be given.
+
+ >>> bart = service.join('test@example.com', 'bart@example.com',
+ ... 'Bart Person')
+ >>> bart
+ <Member: Bart Person <bart@example.com>
+ on test@example.com as MemberRole.member>
+
+Other roles can be subscribed using the more traditional interfaces.
+
+ >>> from mailman.interfaces.member import MemberRole
+ >>> from mailman.utilities.datetime import now
+ >>> address = list(anne.user.addresses)[0]
+ >>> address.verified_on = now()
+ >>> anne.user.preferred_address = address
+ >>> anne_owner = mlist.subscribe(anne.user, MemberRole.owner)
+
+And all the subscribed members can now be displayed.
+
+ >>> service.get_members()
+ [<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.member>,
+ <Member: Bart Person <bart@example.com> on test@example.com
+ as MemberRole.member>]
+ >>> sum(1 for member in service)
+ 3
+ >>> print service.get_member(3)
+ <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>
+
+Regular members can also be removed.
+
+ >>> service.leave('test@example.com', 'anne@example.com')
+ >>> service.get_members()
+ [<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>,
+ <Member: Bart Person <bart@example.com> on test@example.com
+ as MemberRole.member>]
+ >>> sum(1 for member in service)
+ 2
+
+
+Finding members
+===============
+
+If you know the member id for a specific member, you can get that member.
+
+ >>> service.get_member(3)
+ <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>
+
+If you know the member's address, you can find all their memberships, based on
+specific search criteria. At a minimum, you need the member's email address.
+::
+
+ >>> mlist2 = create_list('foo@example.com')
+ >>> mlist3 = create_list('bar@example.com')
+ >>> mlist.subscribe(anne.user, MemberRole.member)
+ <Member: anne <anne@example.com> on test@example.com as MemberRole.member>
+ >>> mlist.subscribe(anne.user, MemberRole.moderator)
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.moderator>
+ >>> mlist2.subscribe(anne.user, MemberRole.member)
+ <Member: anne <anne@example.com> on foo@example.com as MemberRole.member>
+ >>> mlist3.subscribe(anne.user, MemberRole.owner)
+ <Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>
+
+ >>> service.find_members('anne@example.com')
+ [<Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>,
+ <Member: anne <anne@example.com> on foo@example.com as MemberRole.member>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.member>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.owner>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.moderator>]
+
+There may be no matching memberships.
+
+ >>> service.find_members('cris@example.com')
+ []
+
+Memberships can also be searched for by user id.
+
+ >>> service.find_members(1)
+ [<Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>,
+ <Member: anne <anne@example.com> on foo@example.com as MemberRole.member>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.member>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.owner>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.moderator>]
+
+You can find all the memberships for an address on a specific mailing list.
+
+ >>> service.find_members('anne@example.com', 'test@example.com')
+ [<Member: anne <anne@example.com> on test@example.com
+ as MemberRole.member>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.owner>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.moderator>]
+
+You can find all the memberships for an address with a specific role.
+
+ >>> service.find_members('anne@example.com', role=MemberRole.owner)
+ [<Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.owner>]
+
+You can also find a specific membership by all three criteria.
+
+ >>> service.find_members('anne@example.com', 'test@example.com',
+ ... MemberRole.owner)
+ [<Member: anne <anne@example.com> on test@example.com
+ as MemberRole.owner>]
diff --git a/src/mailman/rest/adapters.py b/src/mailman/app/subscriptions.py
index 5be128d3d..ab8c3d53c 100644
--- a/src/mailman/rest/adapters.py
+++ b/src/mailman/app/subscriptions.py
@@ -26,7 +26,7 @@ __all__ = [
from operator import attrgetter
-
+from storm.expr import And, Or
from zope.component import getUtility
from zope.interface import implements
@@ -36,7 +36,7 @@ from mailman.core.constants import system_preferences
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import IListManager, NoSuchListError
from mailman.interfaces.member import DeliveryMode
-from mailman.interfaces.membership import (
+from mailman.interfaces.subscriptions import (
ISubscriptionService, MissingUserError)
from mailman.interfaces.usermanager import IUserManager
from mailman.model.member import Member
@@ -44,6 +44,20 @@ from mailman.utilities.passwords import make_user_friendly_password
+def _membership_sort_key(member):
+ """Sort function for get_memberships().
+
+ The members are sorted first by fully-qualified mailing list name,
+ then by subscribed email address, then by role.
+ """
+ # member.mailing_list is already the fqdn_listname, not the IMailingList
+ # object.
+ return (member.mailing_list,
+ member.address.email,
+ int(member.role))
+
+
+
class SubscriptionService:
"""Subscription services for the REST API."""
@@ -72,13 +86,52 @@ class SubscriptionService:
"""See `ISubscriptionService`."""
members = config.db.store.find(
Member,
- Member._member_id == member_id)
+ Member._member_id == unicode(member_id))
if members.count() == 0:
return None
else:
assert members.count() == 1, 'Too many matching members'
return members[0]
+ def find_members(self, subscriber, fqdn_listname=None, role=None):
+ """See `ISubscriptionService`."""
+ # If `subscriber` is a user id, then we'll search for all addresses
+ # which are controlled by the user, otherwise we'll just search for
+ # the given address.
+ user_manager = getUtility(IUserManager)
+ query = None
+ if isinstance(subscriber, basestring):
+ # subscriber is an email address.
+ address = user_manager.get_address(subscriber)
+ user = user_manager.get_user(subscriber)
+ # This probably could be made more efficient.
+ if address is None or user is None:
+ return []
+ or_clause = Or(Member.address_id == address.id,
+ Member.user_id == user.id)
+ else:
+ # subscriber is a user id.
+ user = user_manager.get_user_by_id(unicode(subscriber))
+ address_ids = list(address.id for address in user.addresses
+ if address.id is not None)
+ if len(address_ids) == 0 or user is None:
+ return []
+ or_clause = Or(Member.user_id == user.id,
+ Member.address_id.is_in(address_ids))
+ # The rest is the same, based on the given criteria.
+ if fqdn_listname is None and role is None:
+ query = or_clause
+ elif fqdn_listname is None:
+ query = And(Member.role == role, or_clause)
+ elif role is None:
+ query = And(Member.mailing_list == fqdn_listname, or_clause)
+ else:
+ query = And(Member.mailing_list == fqdn_listname,
+ Member.role == role,
+ or_clause)
+ results = config.db.store.find(Member, query)
+ return sorted(results, key=_membership_sort_key)
+
def __iter__(self):
for member in self.get_members():
yield member
@@ -124,4 +177,3 @@ class SubscriptionService:
raise NoSuchListError(fqdn_listname)
# XXX for now, no notification or user acknowledgement.
delete_member(mlist, address, False, False)
- return ''
diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml
index f34076b78..b5317aa69 100644
--- a/src/mailman/config/configure.zcml
+++ b/src/mailman/config/configure.zcml
@@ -67,8 +67,8 @@
/>
<utility
- factory="mailman.rest.adapters.SubscriptionService"
- provides="mailman.interfaces.membership.ISubscriptionService"
+ factory="mailman.app.subscriptions.SubscriptionService"
+ provides="mailman.interfaces.subscriptions.ISubscriptionService"
/>
<utility
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 83e740f56..57e3ad43d 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -25,6 +25,10 @@ REST
* The IMailingList attribute ``host_name`` has been renamed to ``mail_host``
for consistency. This changes the REST API for mailing list
resources. (LP: #787599)
+ * New REST resource http://.../members/find can be POSTed to in order to find
+ member records. Arguments are `subscriber` (email address to search for -
+ required), `fqdn_listname` (optional), and `role` (i.e. MemberRole -
+ optional). (LP: #799612)
Commands
--------
diff --git a/src/mailman/interfaces/membership.py b/src/mailman/interfaces/subscriptions.py
index 233d248fd..c6bc47d5d 100644
--- a/src/mailman/interfaces/membership.py
+++ b/src/mailman/interfaces/subscriptions.py
@@ -44,7 +44,7 @@ class MissingUserError(MailmanError):
class ISubscriptionService(Interface):
- """Subscription services for the REST API."""
+ """General Subscription services."""
def get_members():
"""Return a sequence of all members of all mailing lists.
@@ -63,11 +63,31 @@ class ISubscriptionService(Interface):
"""Return a member record matching the member id.
:param member_id: A member id.
- :type member_id: unicode
+ :type member_id: int
:return: The matching member, or None if no matching member is found.
:rtype: `IMember`
"""
+ def find_members(subscriber, fqdn_listname=None, role=None):
+ """Search for and return a specific member.
+
+ The members are sorted first by fully-qualified mailing list name,
+ then by subscribed email address, then by role. Because the user may
+ be a member of the list under multiple roles (e.g. as an owner and as
+ a digest member), the member can appear multiple times in this list.
+
+ :param subscriber: The email address or user id of the user getting
+ subscribed.
+ :type subscriber: string or int
+ :param fqdn_listname: The posting address of the mailing list to
+ search for the subscriber's memberships on.
+ :type fqdn_listname: string
+ :param role: The member role.
+ :type role: `MemberRole`
+ :return: The list of all memberships, which may be empty.
+ :rtype: list of `IMember`
+ """
+
def __iter__():
"""See `get_members()`."""
@@ -83,8 +103,9 @@ class ISubscriptionService(Interface):
:param fqdn_listname: The posting address of the mailing list to
subscribe the user to.
:type fqdn_listname: string
- :param address: The address of the user getting subscribed.
- :type address: string
+ :param subscriber: The email address or user id of the user getting
+ subscribed.
+ :type subscriber: string
:param real_name: The name of the user. This is only used if a new
user is created, and it defaults to the local part of the email
address if not given.
diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.rst
index 80b5a563d..179ac99fe 100644
--- a/src/mailman/rest/docs/membership.txt
+++ b/src/mailman/rest/docs/membership.rst
@@ -255,6 +255,101 @@ mailing list.
total_size: 7
+Finding members
+===============
+
+You can find a specific member based on several different criteria. For
+example, we can search for all the memberships of a particular address.
+
+ >>> dump_json('http://localhost:9001/3.0/members/find', {
+ ... 'subscriber': 'aperson@example.com',
+ ... })
+ entry 0:
+ address: aperson@example.com
+ fqdn_listname: ant@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/4
+ user: http://localhost:9001/3.0/users/3
+ entry 1:
+ address: aperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/3
+ user: http://localhost:9001/3.0/users/3
+ http_etag: ...
+ start: 0
+ total_size: 2
+
+Or, we can find all the memberships for an address on a particular mailing
+list.
+
+ >>> dump_json('http://localhost:9001/3.0/members/find', {
+ ... 'subscriber': 'cperson@example.com',
+ ... 'fqdn_listname': 'bee@example.com',
+ ... })
+ entry 0:
+ address: cperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/2
+ user: http://localhost:9001/3.0/users/2
+ entry 1:
+ address: cperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: owner
+ self_link: http://localhost:9001/3.0/members/7
+ user: http://localhost:9001/3.0/users/2
+ http_etag: ...
+ start: 0
+ total_size: 2
+
+Or, we can find all the memberships for an address with a specific role.
+
+ >>> dump_json('http://localhost:9001/3.0/members/find', {
+ ... 'subscriber': 'cperson@example.com',
+ ... 'role': 'member',
+ ... })
+ entry 0:
+ address: cperson@example.com
+ fqdn_listname: ant@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/5
+ user: http://localhost:9001/3.0/users/2
+ entry 1:
+ address: cperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/2
+ user: http://localhost:9001/3.0/users/2
+ http_etag: ...
+ start: 0
+ total_size: 2
+
+Finally, we can search for a specific member given all three criteria.
+
+ >>> dump_json('http://localhost:9001/3.0/members/find', {
+ ... 'subscriber': 'cperson@example.com',
+ ... 'fqdn_listname': 'bee@example.com',
+ ... 'role': 'member',
+ ... })
+ entry 0:
+ address: cperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/2
+ user: http://localhost:9001/3.0/users/2
+ http_etag: ...
+ start: 0
+ total_size: 1
+
+
Joining a mailing list
======================
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index 67495874b..aea5df843 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -23,10 +23,12 @@ __metaclass__ = type
__all__ = [
'AMember',
'AllMembers',
+ 'FindMembers',
'MemberCollection',
]
+from operator import attrgetter
from restish import http, resource
from zope.component import getUtility
@@ -36,7 +38,7 @@ from mailman.interfaces.listmanager import IListManager, NoSuchListError
from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, MemberRole, MembershipError,
NotAMemberError)
-from mailman.interfaces.membership import ISubscriptionService
+from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.interfaces.user import UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.rest.helpers import (
@@ -65,6 +67,25 @@ class _MemberBase(resource.Resource, CollectionMixin):
+class MemberCollection(_MemberBase):
+ """Abstract class for supporting submemberships.
+
+ This is used for example to return a resource representing all the
+ memberships of a mailing list, or all memberships for a specific email
+ address.
+ """
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ raise NotImplementedError
+
+ @resource.GET()
+ def container(self, request):
+ """roster/[members|owners|moderators]"""
+ resource = self._make_collection(request)
+ return http.ok([], etag(resource))
+
+
+
class AMember(_MemberBase):
"""A member."""
@@ -150,19 +171,34 @@ class AllMembers(_MemberBase):
-class MemberCollection(_MemberBase):
- """Abstract class for supporting submemberships.
+class _FoundMembers(MemberCollection):
+ """The found members collection."""
+
+ def __init__(self, members):
+ super(_FoundMembers, self).__init__()
+ self._members = members
- This is used for example to return a resource representing all the
- memberships of a mailing list, or all memberships for a specific email
- address.
- """
def _get_collection(self, request):
"""See `CollectionMixin`."""
- raise NotImplementedError
+ address_of_member = attrgetter('address.email')
+ return list(sorted(self._members, key=address_of_member))
- @resource.GET()
- def container(self, request):
- """roster/[members|owners|moderators]"""
- resource = self._make_collection(request)
+
+class FindMembers(_MemberBase):
+ """/members/find"""
+
+ @resource.POST()
+ def find(self, request):
+ """Find a member"""
+ service = getUtility(ISubscriptionService)
+ validator = Validator(fqdn_listname=unicode,
+ subscriber=unicode,
+ role=enum_validator(MemberRole),
+ _optional=('fqdn_listname', 'role'))
+ members = service.find_members(**validator(request))
+ # We can't just return the _FoundMembers instance, because
+ # CollectionMixins have only a GET method, which is incompatible with
+ # this POSTed resource. IOW, without doing this here, restish would
+ # throw a 405 Method Not Allowed.
+ resource = _FoundMembers(members)._make_collection(request)
return http.ok([], etag(resource))
diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py
index caa44f872..cd4c8ceef 100644
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -34,7 +34,7 @@ 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
-from mailman.rest.members import AMember, AllMembers
+from mailman.rest.members import AMember, AllMembers, FindMembers
from mailman.rest.users import AUser, AllUsers
@@ -120,9 +120,13 @@ class TopLevel(resource.Resource):
"""/<api>/members"""
if len(segments) == 0:
return AllMembers()
+ # Either the next segment is the string "find" or a member id. They
+ # cannot collide.
+ segment = segments.pop(0)
+ if segment == 'find':
+ return FindMembers(), segments
else:
- member_id = segments.pop(0)
- return AMember(member_id), segments
+ return AMember(segment), segments
@resource.child()
def users(self, request, segments):
diff --git a/src/mailman/runners/outgoing.py b/src/mailman/runners/outgoing.py
index f611ccf92..65d8928a6 100644
--- a/src/mailman/runners/outgoing.py
+++ b/src/mailman/runners/outgoing.py
@@ -28,9 +28,9 @@ from mailman.config import config
from mailman.core.runner import Runner
from mailman.interfaces.bounce import BounceContext, IBounceProcessor
from mailman.interfaces.mailinglist import Personalization
-from mailman.interfaces.membership import ISubscriptionService
from mailman.interfaces.mta import SomeRecipientsFailed
from mailman.interfaces.pending import IPendings
+from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.utilities.datetime import now
from mailman.utilities.modules import find_name