summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/membership.py12
-rw-r--r--src/mailman/commands/cli_lists.py4
-rw-r--r--src/mailman/core/errors.py41
-rw-r--r--src/mailman/docs/lifecycle.txt2
-rw-r--r--src/mailman/docs/listmanager.txt2
-rw-r--r--src/mailman/docs/registration.txt12
-rw-r--r--src/mailman/email/validate.py6
-rw-r--r--src/mailman/interfaces/address.py7
-rw-r--r--src/mailman/interfaces/listmanager.py13
-rw-r--r--src/mailman/interfaces/member.py26
-rw-r--r--src/mailman/interfaces/membership.py42
-rw-r--r--src/mailman/interfaces/registrar.py2
-rw-r--r--src/mailman/interfaces/user.py2
-rw-r--r--src/mailman/model/listmanager.py4
-rw-r--r--src/mailman/model/roster.py13
-rw-r--r--src/mailman/queue/docs/outgoing.txt6
-rw-r--r--src/mailman/rest/adapters.py36
-rw-r--r--src/mailman/rest/docs/membership.txt33
18 files changed, 208 insertions, 55 deletions
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py
index f8234f808..2adba814a 100644
--- a/src/mailman/app/membership.py
+++ b/src/mailman/app/membership.py
@@ -35,7 +35,8 @@ from mailman.core import errors
from mailman.core.i18n import _
from mailman.email.message import OwnerNotification
from mailman.email.validate import validate
-from mailman.interfaces.member import AlreadySubscribedError, MemberRole
+from mailman.interfaces.member import (
+ AlreadySubscribedError, MemberRole, MembershipIsBannedError)
from mailman.interfaces.usermanager import IUserManager
@@ -58,6 +59,12 @@ def add_member(mlist, address, realname, password, delivery_mode, language):
:type delivery_mode: DeliveryMode
:param language: The language that the subscriber is going to use.
:type language: string
+ :return: The just created member.
+ :rtype: `IMember`
+ :raises AlreadySubscribedError: if the user is already subscribed to
+ the mailing list.
+ :raises InvalidEmailAddressError: if the email address is not valid.
+ :raises MembershipIsBannedError: if the membership is not allowed.
"""
# Let's be extra cautious.
validate(address)
@@ -68,7 +75,7 @@ def add_member(mlist, address, realname, password, delivery_mode, language):
# confirmations.
pattern = Utils.get_pattern(address, mlist.ban_list)
if pattern:
- raise errors.MembershipIsBanned(pattern)
+ raise MembershipIsBannedError(mlist, address)
# Do the actual addition. First, see if there's already a user linked
# with the given address.
user_manager = getUtility(IUserManager)
@@ -110,6 +117,7 @@ def add_member(mlist, address, realname, password, delivery_mode, language):
member.preferences.delivery_mode = delivery_mode
## mlist.setMemberOption(email, config.Moderate,
## mlist.default_member_moderation)
+ return member
diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py
index 93749c45a..79341ca6c 100644
--- a/src/mailman/commands/cli_lists.py
+++ b/src/mailman/commands/cli_lists.py
@@ -34,9 +34,9 @@ from mailman.Utils import maketext
from mailman.app.lifecycle import create_list, remove_list
from mailman.config import config
from mailman.core.constants import system_preferences
-from mailman.core.errors import InvalidEmailAddress
from mailman.core.i18n import _
from mailman.email.message import UserNotification
+from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.domain import (
BadDomainSpecificationError, IDomainManager)
@@ -188,7 +188,7 @@ class Create:
domain_manager.add(domain)
try:
mlist = create_list(fqdn_listname, args.owners)
- except InvalidEmailAddress:
+ except InvalidEmailAddressError:
self.parser.error(_('Illegal list name: $fqdn_listname'))
return
except ListAlreadyExistsError:
diff --git a/src/mailman/core/errors.py b/src/mailman/core/errors.py
index 8037ea823..2d31863c3 100644
--- a/src/mailman/core/errors.py
+++ b/src/mailman/core/errors.py
@@ -15,7 +15,16 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Mailman errors."""
+"""Legacy Mailman exceptions.
+
+This module is largely obsolete, though not all exceptions in use have been
+migrated to their proper location. There are still a number of Mailman 2.1
+exceptions floating about in here too.
+
+The right place for exceptions is in the interface module for their related
+interfaces.
+"""
+
from __future__ import absolute_import, unicode_literals
@@ -29,18 +38,14 @@ __all__ = [
'EmailAddressError',
'HandlerError',
'HoldMessage',
- 'HostileSubscriptionError',
- 'InvalidEmailAddress',
'LostHeldMessage',
'MailmanError',
'MailmanException',
'MemberError',
- 'MembershipIsBanned',
'MustDigestError',
'NotAMemberError',
'PasswordError',
'RejectMessage',
- 'SubscriptionError',
]
@@ -58,7 +63,6 @@ class AlreadyReceivingDigests(MemberError): pass
class AlreadyReceivingRegularDeliveries(MemberError): pass
class CantDigestError(MemberError): pass
class MustDigestError(MemberError): pass
-class MembershipIsBanned(MemberError): pass
@@ -71,17 +75,6 @@ class MailmanError(MailmanException):
-# Exception hierarchy for bad email address errors that can be raised from
-# mailman.email.validate.validate()
-class EmailAddressError(MailmanError):
- """Base class for email address validation errors."""
-
-
-class InvalidEmailAddress(EmailAddressError):
- """Email address is invalid."""
-
-
-
# Exceptions for admin request database
class LostHeldMessage(MailmanError):
"""Held message was lost."""
@@ -134,20 +127,6 @@ class RejectMessage(HandlerError):
-# Subscription exceptions
-class SubscriptionError(MailmanError):
- """Subscription errors base class."""
-
-
-class HostileSubscriptionError(SubscriptionError):
- """A cross-subscription attempt was made.
-
- This exception gets raised when an invitee attempts to use the
- invitation to cross-subscribe to some other mailing list.
- """
-
-
-
class PasswordError(MailmanError):
"""A password related error."""
diff --git a/src/mailman/docs/lifecycle.txt b/src/mailman/docs/lifecycle.txt
index ee4657f1d..a1cd50825 100644
--- a/src/mailman/docs/lifecycle.txt
+++ b/src/mailman/docs/lifecycle.txt
@@ -29,7 +29,7 @@ bogus posting address, you get an exception.
>>> create_list('not a valid address')
Traceback (most recent call last):
...
- InvalidEmailAddress: u'not a valid address'
+ InvalidEmailAddressError: u'not a valid address'
If the posting address is valid, but the domain has not been registered with
Mailman yet, you get an exception.
diff --git a/src/mailman/docs/listmanager.txt b/src/mailman/docs/listmanager.txt
index e8af0d71d..e07659066 100644
--- a/src/mailman/docs/listmanager.txt
+++ b/src/mailman/docs/listmanager.txt
@@ -47,7 +47,7 @@ It is an error to create a mailing list that isn't a fully qualified list name
>>> list_manager.create('foo')
Traceback (most recent call last):
...
- InvalidEmailAddress: foo
+ InvalidEmailAddressError: foo
Deleting a mailing list
diff --git a/src/mailman/docs/registration.txt b/src/mailman/docs/registration.txt
index a3dfc7004..abc7f2c93 100644
--- a/src/mailman/docs/registration.txt
+++ b/src/mailman/docs/registration.txt
@@ -49,27 +49,27 @@ addresses are rejected outright.
>>> registrar.register(mlist, '')
Traceback (most recent call last):
...
- InvalidEmailAddress: u''
+ InvalidEmailAddressError: u''
>>> registrar.register(mlist, 'some name@example.com')
Traceback (most recent call last):
...
- InvalidEmailAddress: u'some name@example.com'
+ InvalidEmailAddressError: u'some name@example.com'
>>> registrar.register(mlist, '<script>@example.com')
Traceback (most recent call last):
...
- InvalidEmailAddress: u'<script>@example.com'
+ InvalidEmailAddressError: u'<script>@example.com'
>>> registrar.register(mlist, '\xa0@example.com')
Traceback (most recent call last):
...
- InvalidEmailAddress: u'\xa0@example.com'
+ InvalidEmailAddressError: u'\xa0@example.com'
>>> registrar.register(mlist, 'noatsign')
Traceback (most recent call last):
...
- InvalidEmailAddress: u'noatsign'
+ InvalidEmailAddressError: u'noatsign'
>>> registrar.register(mlist, 'nodom@ain')
Traceback (most recent call last):
...
- InvalidEmailAddress: u'nodom@ain'
+ InvalidEmailAddressError: u'nodom@ain'
Register an email address
diff --git a/src/mailman/email/validate.py b/src/mailman/email/validate.py
index fc33b1509..9e6e62b39 100644
--- a/src/mailman/email/validate.py
+++ b/src/mailman/email/validate.py
@@ -28,8 +28,8 @@ __all__ = [
import re
-from mailman.core.errors import InvalidEmailAddress
from mailman.email.utils import split_email
+from mailman.interfaces.address import InvalidEmailAddressError
# What other characters should be disallowed?
@@ -42,10 +42,10 @@ def validate(address):
:param address: An email address.
:type address: string
- :raise `InvalidEmailAddress`: when the address is deemed invalid.
+ :raise InvalidEmailAddressError: when the address is deemed invalid.
"""
if not is_valid(address):
- raise InvalidEmailAddress(repr(address))
+ raise InvalidEmailAddressError(repr(address))
diff --git a/src/mailman/interfaces/address.py b/src/mailman/interfaces/address.py
index 968ded3ee..4b82c13db 100644
--- a/src/mailman/interfaces/address.py
+++ b/src/mailman/interfaces/address.py
@@ -26,10 +26,13 @@ __all__ = [
'AddressNotLinkedError',
'ExistingAddressError',
'IAddress',
+ 'InvalidEmailAddressError',
]
+from lazr.restful.declarations import error_status
from zope.interface import Interface, Attribute
+
from mailman.interfaces.errors import MailmanError
@@ -50,6 +53,10 @@ class AddressNotLinkedError(AddressError):
"""The address is not linked to the user."""
+@error_status(400)
+class InvalidEmailAddressError(AddressError):
+ """Email address is invalid."""
+
class IAddress(Interface):
diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py
index 8a058eaa5..b707df7e7 100644
--- a/src/mailman/interfaces/listmanager.py
+++ b/src/mailman/interfaces/listmanager.py
@@ -23,6 +23,7 @@ __metaclass__ = type
__all__ = [
'IListManager',
'ListAlreadyExistsError',
+ 'NoSuchListError',
]
@@ -45,6 +46,18 @@ class ListAlreadyExistsError(MailmanError):
"""
+@error_status(400)
+class NoSuchListError(MailmanError):
+ """Attempt to access a mailing list that does not exist."""
+
+ def __init__(self, fqdn_listname):
+ self.fqdn_listname = fqdn_listname
+
+ def __str__(self):
+ return 'No such mailing list: {0.fqdn_listname}'.format(self)
+
+
+
class IListManager(Interface):
"""The interface of the global list manager.
diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py
index e5a82060a..87ad19c83 100644
--- a/src/mailman/interfaces/member.py
+++ b/src/mailman/interfaces/member.py
@@ -26,15 +26,17 @@ __all__ = [
'DeliveryStatus',
'IMember',
'MemberRole',
+ 'MembershipError',
+ 'MembershipIsBannedError',
]
from lazr.restful.declarations import (
- export_as_webservice_entry, exported)
+ error_status, export_as_webservice_entry, exported)
from munepy import Enum
from zope.interface import Interface, Attribute
-from mailman.core.errors import SubscriptionError
+from mailman.core.errors import MailmanError
@@ -71,7 +73,11 @@ class MemberRole(Enum):
-class AlreadySubscribedError(SubscriptionError):
+class MembershipError(MailmanError):
+ """Base exception for all membership errors."""
+
+
+class AlreadySubscribedError(MembershipError):
"""The member is already subscribed to the mailing list with this role."""
def __init__(self, fqdn_listname, address, role):
@@ -85,6 +91,20 @@ class AlreadySubscribedError(SubscriptionError):
self._address, self._role, self._fqdn_listname)
+@error_status(400)
+class MembershipIsBannedError(MembershipError):
+ """The address is not allowed to subscribe to the mailing list."""
+
+ def __init__(self, mlist, address):
+ super(MembershipIsBanned, self).__init__()
+ self._mlist = mlist
+ self._address = address
+
+ def __str__(self):
+ return '{0} is not allowed to subscribe to {1.fqdn_listname}'.format(
+ self._address, self._mlist)
+
+
class IMember(Interface):
"""A member of a mailing list."""
diff --git a/src/mailman/interfaces/membership.py b/src/mailman/interfaces/membership.py
index 0a04ff630..51e36c6e5 100644
--- a/src/mailman/interfaces/membership.py
+++ b/src/mailman/interfaces/membership.py
@@ -27,8 +27,9 @@ __all__ = [
from lazr.restful.declarations import (
collection_default_content, export_as_webservice_collection,
- export_factory_operation)
+ export_write_operation, operation_parameters)
from zope.interface import Interface
+from zope.schema import TextLine
from mailman.core.i18n import _
from mailman.interfaces.member import IMember
@@ -53,3 +54,42 @@ class ISubscriptionService(Interface):
:return: The list of all members.
:rtype: list of `IMember`
"""
+
+ @operation_parameters(
+ fqdn_listname=TextLine(),
+ address=TextLine(),
+ real_name=TextLine(),
+ delivery_mode=TextLine(),
+ )
+ @export_write_operation()
+ def join(fqdn_listname, address, real_name=None, delivery_mode=None):
+ """Subscribe to a mailing list.
+
+ A user for the address is created if it is not yet known to Mailman,
+ however newly registered addresses will not yet be validated. No
+ confirmation message will be sent to the address, and the approval of
+ the subscription request is still dependent on the policy of the
+ mailing list.
+
+ :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 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.
+ :type real_name: string
+ :param delivery_mode: The delivery mode for this subscription. This
+ can be one of the enum values of `DeliveryMode`. If not given,
+ regular delivery is assumed.
+ :type delivery_mode: string
+ :return: The just created member.
+ :rtype: `IMember`
+ :raises AlreadySubscribedError: if the user is already subscribed to
+ the mailing list.
+ :raises InvalidEmailAddressError: if the email address is not valid.
+ :raises MembershipIsBannedError: if the membership is not allowed.
+ :raises NoSuchListError: if the named mailing list does not exist.
+ :raises ValueError: when `delivery_mode` is invalid.
+ """
diff --git a/src/mailman/interfaces/registrar.py b/src/mailman/interfaces/registrar.py
index dae4d53a8..d7eac1072 100644
--- a/src/mailman/interfaces/registrar.py
+++ b/src/mailman/interfaces/registrar.py
@@ -61,7 +61,7 @@ class IRegistrar(Interface):
:type real_name: str
:return: The confirmation token string.
:rtype: str
- :raises InvalidEmailAddress: if the address is not allowed.
+ :raises InvalidEmailAddressError: if the address is not allowed.
"""
def confirm(token):
diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py
index 5c3ff58cd..2892b44d2 100644
--- a/src/mailman/interfaces/user.py
+++ b/src/mailman/interfaces/user.py
@@ -42,7 +42,7 @@ class IUser(Interface):
"""An iterator over all the IAddresses controlled by this user.""")
memberships = Attribute(
- """A roster of this user's membership.""")
+ """A roster of this user's memberships.""")
def register(address, real_name=None):
"""Register the given email address and link it to this user.
diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py
index d4fd49cd0..a89b771b0 100644
--- a/src/mailman/model/listmanager.py
+++ b/src/mailman/model/listmanager.py
@@ -30,7 +30,7 @@ import datetime
from zope.interface import implements
from mailman.config import config
-from mailman.core.errors import InvalidEmailAddress
+from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError
from mailman.interfaces.rest import IResolvePathNames
from mailman.model.mailinglist import MailingList
@@ -47,7 +47,7 @@ class ListManager:
"""See `IListManager`."""
listname, at, hostname = fqdn_listname.partition('@')
if len(hostname) == 0:
- raise InvalidEmailAddress(fqdn_listname)
+ raise InvalidEmailAddressError(fqdn_listname)
mlist = config.db.store.find(
MailingList,
MailingList.list_name == listname,
diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py
index 62e3ab5dc..094c2c21c 100644
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -67,6 +67,7 @@ class AbstractRoster:
@property
def members(self):
+ """See `IRoster`."""
for member in config.db.store.find(
Member,
mailing_list=self._mlist.fqdn_listname,
@@ -75,6 +76,7 @@ class AbstractRoster:
@property
def users(self):
+ """See `IRoster`."""
# Members are linked to addresses, which in turn are linked to users.
# So while the 'members' attribute does most of the work, we have to
# keep a set of unique users. It's possible for the same user to be
@@ -86,12 +88,14 @@ class AbstractRoster:
@property
def addresses(self):
+ """See `IRoster`."""
# Every Member is linked to exactly one address so the 'members'
# attribute does most of the work.
for member in self.members:
yield member.address
def get_member(self, address):
+ """See `IRoster`."""
results = config.db.store.find(
Member,
Member.mailing_list == self._mlist.fqdn_listname,
@@ -140,6 +144,7 @@ class AdministratorRoster(AbstractRoster):
@property
def members(self):
+ """See `IRoster`."""
# Administrators are defined as the union of the owners and the
# moderators.
members = config.db.store.find(
@@ -151,6 +156,7 @@ class AdministratorRoster(AbstractRoster):
yield member
def get_member(self, address):
+ """See `IRoster`."""
results = config.db.store.find(
Member,
Member.mailing_list == self._mlist.fqdn_listname,
@@ -195,6 +201,7 @@ class RegularMemberRoster(DeliveryMemberRoster):
@property
def members(self):
+ """See `IRoster`."""
for member in self._get_members(DeliveryMode.regular):
yield member
@@ -207,6 +214,7 @@ class DigestMemberRoster(DeliveryMemberRoster):
@property
def members(self):
+ """See `IRoster`."""
for member in self._get_members(DeliveryMode.plaintext_digests,
DeliveryMode.mime_digests,
DeliveryMode.summary_digests):
@@ -221,6 +229,7 @@ class Subscribers(AbstractRoster):
@property
def members(self):
+ """See `IRoster`."""
for member in config.db.store.find(
Member,
mailing_list=self._mlist.fqdn_listname):
@@ -240,6 +249,7 @@ class Memberships:
@property
def members(self):
+ """See `IRoster`."""
results = config.db.store.find(
Member,
Address.user_id == self._user.id,
@@ -249,14 +259,17 @@ class Memberships:
@property
def users(self):
+ """See `IRoster`."""
yield self._user
@property
def addresses(self):
+ """See `IRoster`."""
for address in self._user.addresses:
yield address
def get_member(self, address):
+ """See `IRoster`."""
results = config.db.store.find(
Member,
Member.address_id == Address.id,
diff --git a/src/mailman/queue/docs/outgoing.txt b/src/mailman/queue/docs/outgoing.txt
index 6188ab35c..cdabdb795 100644
--- a/src/mailman/queue/docs/outgoing.txt
+++ b/src/mailman/queue/docs/outgoing.txt
@@ -18,10 +18,16 @@ move messages to the 'retry queue' for handling delivery failures.
>>> from mailman.interfaces.member import DeliveryMode
>>> add_member(mlist, 'aperson@example.com', 'Anne Person',
... 'password', DeliveryMode.regular, 'en')
+ <Member: Anne Person <aperson@example.com>
+ on test@example.com as MemberRole.member>
>>> add_member(mlist, 'bperson@example.com', 'Bart Person',
... 'password', DeliveryMode.regular, 'en')
+ <Member: Bart Person <bperson@example.com>
+ on test@example.com as MemberRole.member>
>>> add_member(mlist, 'cperson@example.com', 'Cris Person',
... 'password', DeliveryMode.regular, 'en')
+ <Member: Cris Person <cperson@example.com>
+ on test@example.com as MemberRole.member>
Normally, messages would show up in the outgoing queue after the message has
been processed by the rule set and pipeline. But we can simulate that here by
diff --git a/src/mailman/rest/adapters.py b/src/mailman/rest/adapters.py
index 720bdd7cb..0cc5e2f95 100644
--- a/src/mailman/rest/adapters.py
+++ b/src/mailman/rest/adapters.py
@@ -31,8 +31,12 @@ from zope.component import getUtility
from zope.interface import implements
from zope.publisher.interfaces import NotFound
+from mailman.app.membership import add_member
+from mailman.core.constants import system_preferences
+from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.domain import IDomainCollection, IDomainManager
-from mailman.interfaces.listmanager import IListManager
+from mailman.interfaces.listmanager import IListManager, NoSuchListError
+from mailman.interfaces.member import DeliveryMode
from mailman.interfaces.membership import ISubscriptionService
from mailman.interfaces.rest import IResolvePathNames
@@ -91,3 +95,33 @@ class SubscriptionService:
sorted((member for member in mailing_list.members.members),
key=address_of_member))
return members
+
+ def join(self, fqdn_listname, address,
+ real_name= None, delivery_mode=None):
+ """See `ISubscriptionService`."""
+ mlist = getUtility(IListManager).get(fqdn_listname)
+ if mlist is None:
+ raise NoSuchListError(fqdn_listname)
+ # Convert from string to enum. Allow ValueError exceptions to be
+ # returned by the API. XXX 2009-12-30 Does lazr.restful intelligently
+ # handle ValueError?
+ mode = (DeliveryMode.regular
+ if delivery_mode is None
+ else DeliveryMode(delivery_mode))
+ if real_name is None:
+ real_name, at, domain = address.partition('@')
+ if len(at) == 0:
+ # It can't possibly be a valid email address.
+ raise InvalidEmailAddressError(address)
+ # Because we want to keep the REST API simple, there is no password or
+ # language given to us. We'll use the system's default language for
+ # the user's default language. We'll set the password to None. XXX
+ # Is that a good idea? Maybe we should set it to something else,
+ # except that once we encode the password (as we must do to avoid
+ # cleartext passwords in the database) we'll never be able to retrieve
+ # it.
+ #
+ # Note that none of these are used unless the address is completely
+ # new to us.
+ return add_member(mlist, address, real_name, None, mode,
+ system_preferences.preferred_language)
diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.txt
index e225184b5..0e333f3d1 100644
--- a/src/mailman/rest/docs/membership.txt
+++ b/src/mailman/rest/docs/membership.txt
@@ -169,3 +169,36 @@ test-one mailing list.
resource_type_link: http://localhost:8001/3.0/#members
start: 0
total_size: 7
+
+
+Joining a mailing list
+======================
+
+A user can be subscribed to a mailing list via the REST API. Actually,
+addresses not users are subscribed to mailing lists, but addresses are always
+tied to users. A subscribed user is called a member.
+
+Elly subscribes to the alpha mailing list. By default, get gets a regular
+delivery. Since Elly's email address is not yet known to Mailman, a user is
+created for her.
+
+ >>> dump_json('http://localhost:8001/3.0/members', {
+ ... 'ws.op': 'join',
+ ... 'fqdn_listname': 'alpha@example.com',
+ ... 'address': 'eperson@example.com',
+ ... 'real_name': 'Elly Person',
+ ... })
+ http_etag: "a0213c9ff485ef3d24a6e2cc8ee68ed147f05398"
+ resource_type_link: http://localhost:8001/3.0/#member
+ self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/eperson@example.com
+
+Elly is now a member of the mailing list.
+
+ >>> elly = user_manager.get_user('eperson@example.com')
+ >>> elly
+ <User "Elly Person" at ...>
+
+ >>> member_of_lists = set(member.mailing_list
+ ... for member in elly.memberships.members)
+ >>> member_of_lists
+ set([u'alpha@example.com'])