summaryrefslogtreecommitdiff
path: root/Mailman/docs
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/docs')
-rw-r--r--Mailman/docs/Makefile.in82
-rw-r--r--Mailman/docs/__init__.py0
-rw-r--r--Mailman/docs/addresses.txt144
-rw-r--r--Mailman/docs/mlist-addresses.txt85
-rw-r--r--Mailman/docs/mlist-rosters.txt118
-rw-r--r--Mailman/docs/use-listmanager.txt124
-rw-r--r--Mailman/docs/use-usermanager.txt100
-rw-r--r--Mailman/docs/users.txt180
8 files changed, 833 insertions, 0 deletions
diff --git a/Mailman/docs/Makefile.in b/Mailman/docs/Makefile.in
new file mode 100644
index 000000000..0662d8a3e
--- /dev/null
+++ b/Mailman/docs/Makefile.in
@@ -0,0 +1,82 @@
+# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
+#
+# This program 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 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+DESTDIR=
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+PACKAGEDIR= $(prefix)/Mailman/docs
+SHELL= /bin/sh
+
+OTHERFILES= *.txt
+MODULES= *.py
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+# Directories make should decend into
+SUBDIRS=
+
+# Rules
+
+all:
+
+install:
+ for f in $(MODULES) $(OTHERFILES); \
+ do \
+ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
+ done
+ for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) DESTDIR=$(DESTDIR) install); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm *.pyc
+ -rm Makefile
+ @for d in $(SUBDIRS); \
+ do \
+ (cd $$d; $(MAKE) distclean); \
+ done
diff --git a/Mailman/docs/__init__.py b/Mailman/docs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/Mailman/docs/__init__.py
diff --git a/Mailman/docs/addresses.txt b/Mailman/docs/addresses.txt
new file mode 100644
index 000000000..a8cf9f655
--- /dev/null
+++ b/Mailman/docs/addresses.txt
@@ -0,0 +1,144 @@
+Email addresses and rosters
+===========================
+
+Addresses represent email address, and nothing more. Some addresses are tied
+to users that Mailman knows about. For example, a list member is a user that
+the system knows about, but a non-member posting from a brand new email
+address is a counter-example.
+
+
+Creating a roster
+-----------------
+
+Email address objects are tied to rosters, and rosters are tied to the user
+manager. To get things started, access the global user manager and create a
+new roster.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> mgr = config.user_manager
+ >>> roster_1 = mgr.create_roster('roster-1')
+ >>> sorted(roster_1.addresses)
+ []
+
+
+Creating addresses
+------------------
+
+Creating a simple email address object is straight forward.
+
+ >>> addr_1 = roster_1.create('aperson@example.com')
+ >>> flush()
+ >>> addr_1.address
+ 'aperson@example.com'
+ >>> addr_1.real_name is None
+ True
+
+You can also create an email address object with a real name.
+
+ >>> addr_2 = roster_1.create('bperson@example.com', 'Barney Person')
+ >>> addr_2.address
+ 'bperson@example.com'
+ >>> addr_2.real_name
+ 'Barney Person'
+
+You can also iterate through all the addresses on a roster.
+
+ >>> sorted(addr.address for addr in roster_1.addresses)
+ ['aperson@example.com', 'bperson@example.com']
+
+You can create another roster and add a bunch of existing addresses to the
+second roster.
+
+ >>> roster_2 = mgr.create_roster('roster-2')
+ >>> flush()
+ >>> sorted(roster_2.addresses)
+ []
+ >>> for address in roster_1.addresses:
+ ... roster_2.addresses.append(address)
+ >>> roster_2.create('cperson@example.com', 'Charlie Person')
+ <Address: Charlie Person <cperson@example.com> [not verified]>
+ >>> sorted(addr.address for addr in roster_2.addresses)
+ ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
+
+The first roster hasn't been affected.
+
+ >>> sorted(addr.address for addr in roster_1.addresses)
+ ['aperson@example.com', 'bperson@example.com']
+
+
+Removing addresses
+------------------
+
+You can remove an address from a roster just by deleting it.
+
+ >>> for addr in roster_1.addresses:
+ ... if addr.address == 'aperson@example.com':
+ ... break
+ >>> addr.address
+ 'aperson@example.com'
+ >>> roster_1.addresses.remove(addr)
+ >>> sorted(addr.address for addr in roster_1.addresses)
+ ['bperson@example.com']
+
+Again, this doesn't affect the other rosters.
+
+ >>> sorted(addr.address for addr in roster_2.addresses)
+ ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
+
+
+Registration and validation
+---------------------------
+
+Addresses have two dates, the date the address was registered on and the date
+the address was validated on. Neither date isset by default.
+
+ >>> addr = roster_1.create('dperson@example.com', 'David Person')
+ >>> addr.registered_on is None
+ True
+ >>> addr.validated_on is None
+ True
+
+The registered date takes a Python datetime object.
+
+ >>> from datetime import datetime
+ >>> addr.registered_on = datetime(2007, 5, 8, 22, 54, 1)
+ >>> print addr.registered_on
+ 2007-05-08 22:54:01
+ >>> addr.validated_on is None
+ True
+
+And of course, you can also set the validation date.
+
+ >>> addr.validated_on = datetime(2007, 5, 13, 22, 54, 1)
+ >>> print addr.registered_on
+ 2007-05-08 22:54:01
+ >>> print addr.validated_on
+ 2007-05-13 22:54:01
+
+
+The null roster
+---------------
+
+All address objects that have been created are members of the null roster.
+
+ >>> all = mgr.get_roster('')
+ >>> sorted(addr.address for addr in all.addresses)
+ ['aperson@example.com', 'bperson@example.com',
+ 'cperson@example.com', 'dperson@example.com']
+
+And conversely, all addresses should have the null roster on their list of
+rosters.
+
+ >>> for addr in all.addresses:
+ ... assert all in addr.rosters, 'Address is missing null roster'
+
+
+Clean up
+--------
+
+ >>> for roster in mgr.rosters:
+ ... mgr.delete_roster(roster)
+ >>> flush()
+ >>> sorted(roster.name for roster in mgr.rosters)
+ []
diff --git a/Mailman/docs/mlist-addresses.txt b/Mailman/docs/mlist-addresses.txt
new file mode 100644
index 000000000..257cf95c7
--- /dev/null
+++ b/Mailman/docs/mlist-addresses.txt
@@ -0,0 +1,85 @@
+Mailing list addresses
+======================
+
+Every mailing list has a number of addresses which are publicly available.
+These are defined in the IMailingListAddresses interface.
+
+ >>> from Mailman.configuration import config
+ >>> from Mailman.interfaces import IMailingListAddresses
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> IMailingListAddresses.providedBy(mlist)
+ True
+
+The posting address is where people send messages to be posted to the mailing
+list. This is exactly the same as the fully qualified list name.
+
+ >>> mlist.fqdn_listname
+ '_xtest@example.com'
+ >>> mlist.posting_address
+ '_xtest@example.com'
+
+Messages to the mailing list's 'no reply' address always get discarded without
+prejudice.
+
+ >>> mlist.noreply_address
+ 'noreply@example.com'
+
+The mailing list's owner address reaches the human moderators.
+
+ >>> mlist.owner_address
+ '_xtest-owner@example.com'
+
+The request address goes to the list's email command robot.
+
+ >>> mlist.request_address
+ '_xtest-request@example.com'
+
+The bounces address accepts and processes all potential bounces.
+
+ >>> mlist.bounces_address
+ '_xtest-bounces@example.com'
+
+The join (a.k.a. subscribe) address is where someone can email to get added to
+the mailing list. The subscribe alias is a synonym for join, but it's
+deprecated.
+
+ >>> mlist.join_address
+ '_xtest-join@example.com'
+ >>> mlist.subscribe_address
+ '_xtest-subscribe@example.com'
+
+The leave (a.k.a. unsubscribe) address is where someone can email to get added
+to the mailing list. The unsubscribe alias is a synonym for leave, but it's
+deprecated.
+
+ >>> mlist.leave_address
+ '_xtest-leave@example.com'
+ >>> mlist.unsubscribe_address
+ '_xtest-unsubscribe@example.com'
+
+
+Email confirmations
+-------------------
+
+Email confirmation messages are sent when actions such as subscriptions need
+to be confirmed. It requires that a cookie be provided, which will be
+included in the local part of the email address. The exact format of this is
+dependent on the VERP_CONFIRM_FORMAT configuration variable.
+
+ >>> mlist.confirm_address('cookie')
+ '_xtest-confirm+cookie@example.com'
+ >>> mlist.confirm_address('wookie')
+ '_xtest-confirm+wookie@example.com'
+
+ >>> old_format = config.VERP_CONFIRM_FORMAT
+ >>> config.VERP_CONFIRM_FORMAT = '$address---$cookie'
+ >>> mlist.confirm_address('cookie')
+ '_xtest-confirm---cookie@example.com'
+ >>> config.VERP_CONFIRM_FORMAT = old_format
+
+
+Clean up
+--------
+
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
diff --git a/Mailman/docs/mlist-rosters.txt b/Mailman/docs/mlist-rosters.txt
new file mode 100644
index 000000000..490a07e0c
--- /dev/null
+++ b/Mailman/docs/mlist-rosters.txt
@@ -0,0 +1,118 @@
+Mailing list rosters
+====================
+
+Mailing lists use rosters to manage and organize users for various purposes.
+In order to allow for separate storage of mailing list data and user data, the
+connection between mailing list objects and rosters is indirect. Mailing
+lists manage roster names, and these roster names are used to find the rosters
+that contain the actual users.
+
+
+Privileged rosters
+------------------
+
+Mailing lists have two types of privileged users, owners and moderators.
+Owners get to change the configuration of mailing lists and moderators get to
+approve or deny held messages and subscription requests.
+
+When a mailing list is created, it automatically contains a roster for the
+list owners and a roster for the list moderators.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> flush()
+ >>> sorted(roster.name for roster in mlist.owner_rosters)
+ ['_xtest@example.com owners']
+ >>> sorted(roster.name for roster in mlist.moderator_rosters)
+ ['_xtest@example.com moderators']
+
+These rosters are initially empty.
+
+ >>> owner_roster = list(mlist.owner_rosters)[0]
+ >>> sorted(address for address in owner_roster.addresses)
+ []
+ >>> moderator_roster = list(mlist.moderator_rosters)[0]
+ >>> sorted(address for address in moderator_roster.addresses)
+ []
+
+You can create new rosters and add them to the list of owner or moderator
+rosters.
+
+ >>> roster_1 = config.user_manager.create_roster('roster-1')
+ >>> roster_2 = config.user_manager.create_roster('roster-2')
+ >>> roster_3 = config.user_manager.create_roster('roster-3')
+ >>> flush()
+
+Make roster-1 an owner roster, roster-2 a moderator roster, and roster-3 both
+an owner and a moderator roster.
+
+ >>> mlist.add_owner_roster(roster_1)
+ >>> mlist.add_moderator_roster(roster_2)
+ >>> mlist.add_owner_roster(roster_3)
+ >>> mlist.add_moderator_roster(roster_3)
+ >>> flush()
+
+ >>> sorted(roster.name for roster in mlist.owner_rosters)
+ ['_xtest@example.com owners', 'roster-1', 'roster-3']
+ >>> sorted(roster.name for roster in mlist.moderator_rosters)
+ ['_xtest@example.com moderators', 'roster-2', 'roster-3']
+
+
+Privileged users
+----------------
+
+Rosters are the lower level way of managing owners and moderators, but usually
+you just want to know which users have owner and moderator privileges. You
+can get the list of such users by using different attributes.
+
+Because the rosters are all empty to start with, we can create a bunch of
+users that will end up being our owners and moderators.
+
+ >>> aperson = config.user_manager.create_user()
+ >>> bperson = config.user_manager.create_user()
+ >>> cperson = config.user_manager.create_user()
+
+These users need addresses, because rosters manage addresses.
+
+ >>> address_1 = roster_1.create('aperson@example.com', 'Anne Person')
+ >>> aperson.link(address_1)
+ >>> address_2 = roster_2.create('bperson@example.com', 'Ben Person')
+ >>> bperson.link(address_2)
+ >>> address_3 = roster_1.create('cperson@example.com', 'Claire Person')
+ >>> cperson.link(address_3)
+ >>> roster_3.addresses.append(address_3)
+ >>> flush()
+
+Now that everything is set up, we can iterate through the various collections
+of privileged users. Here are the owners of the list.
+
+ >>> from Mailman.interfaces import IUser
+ >>> addresses = []
+ >>> for user in mlist.owners:
+ ... assert IUser.providedBy(user), 'Non-IUser owner found'
+ ... for address in user.addresses:
+ ... addresses.append(address.address)
+ >>> sorted(addresses)
+ ['aperson@example.com', 'cperson@example.com']
+
+Here are the moderators of the list.
+
+ >>> addresses = []
+ >>> for user in mlist.moderators:
+ ... assert IUser.providedBy(user), 'Non-IUser moderator found'
+ ... for address in user.addresses:
+ ... addresses.append(address.address)
+ >>> sorted(addresses)
+ ['bperson@example.com', 'cperson@example.com']
+
+The administrators of a mailing list are the union of the owners and
+moderators.
+
+ >>> addresses = []
+ >>> for user in mlist.administrators:
+ ... assert IUser.providedBy(user), 'Non-IUser administrator found'
+ ... for address in user.addresses:
+ ... addresses.append(address.address)
+ >>> sorted(addresses)
+ ['aperson@example.com', 'bperson@example.com', 'cperson@example.com']
diff --git a/Mailman/docs/use-listmanager.txt b/Mailman/docs/use-listmanager.txt
new file mode 100644
index 000000000..9e237f02f
--- /dev/null
+++ b/Mailman/docs/use-listmanager.txt
@@ -0,0 +1,124 @@
+Using the IListManager interface
+================================
+
+The IListManager is how you create, delete, and retrieve mailing list
+objects. The Mailman system instantiates an IListManager for you based on the
+configuration variable MANAGERS_INIT_FUNCTION. The instance is accessible
+on the global config object.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> from Mailman.interfaces import IListManager
+ >>> IListManager.providedBy(config.list_manager)
+ True
+ >>> mgr = config.list_manager
+
+
+Creating a mailing list
+-----------------------
+
+Creating the list returns the newly created IMailList object.
+
+ >>> from Mailman.interfaces import IMailingList
+ >>> mlist = mgr.create('_xtest@example.com')
+ >>> flush()
+ >>> IMailingList.providedBy(mlist)
+ True
+
+This object has an identity.
+
+ >>> from Mailman.interfaces import IMailingListIdentity
+ >>> IMailingListIdentity.providedBy(mlist)
+ True
+
+All lists with identities have a short name, a host name, and a fully
+qualified listname. This latter is what uniquely distinguishes the mailing
+list to the system.
+
+ >>> mlist.list_name
+ '_xtest'
+ >>> mlist.host_name
+ 'example.com'
+ >>> mlist.fqdn_listname
+ '_xtest@example.com'
+
+If you try to create a mailing list with the same name as an existing list,
+you will get an exception.
+
+ >>> mlist_dup = mgr.create('_xtest@example.com')
+ Traceback (most recent call last):
+ ...
+ MMListAlreadyExistsError: _xtest@example.com
+
+
+Deleting a mailing list
+-----------------------
+
+Deleting an existing mailing list also deletes its rosters and roster sets.
+
+ >>> sorted(r.name for r in config.user_manager.rosters)
+ ['', '_xtest@example.com moderators', '_xtest@example.com owners']
+
+ >>> mgr.delete(mlist)
+ >>> flush()
+ >>> sorted(mgr.names)
+ []
+ >>> sorted(r.name for r in config.user_manager.rosters)
+ ['']
+
+Attempting to access attributes of the deleted mailing list raises an
+exception:
+
+ >>> mlist.fqdn_listname
+ Traceback (most recent call last):
+ ...
+ AttributeError: fqdn_listname
+
+After deleting the list, you can create it again.
+
+ >>> mlist = mgr.create('_xtest@example.com')
+ >>> flush()
+ >>> mlist.fqdn_listname
+ '_xtest@example.com'
+
+
+Retrieving a mailing list
+-------------------------
+
+When a mailing list exists, you can ask the list manager for it and you will
+always get the same object back.
+
+ >>> mlist_2 = mgr.get('_xtest@example.com')
+ >>> mlist_2 is mlist
+ True
+
+Don't try to get a list that doesn't exist yet though, or you will get an
+exception.
+
+ >>> mgr.get('_xtest_2@example.com')
+ Traceback (most recent call last):
+ ...
+ MMUnknownListError: _xtest_2@example.com
+
+
+Iterating over all mailing lists
+--------------------------------
+
+Once you've created a bunch of mailing lists, you can use the list manager to
+iterate over either the list objects, or the list names.
+
+ >>> mlist_3 = mgr.create('_xtest_3@example.com')
+ >>> mlist_4 = mgr.create('_xtest_4@example.com')
+ >>> flush()
+ >>> sorted(mgr.names)
+ ['_xtest@example.com', '_xtest_3@example.com', '_xtest_4@example.com']
+ >>> sorted(m.fqdn_listname for m in mgr.mailing_lists)
+ ['_xtest@example.com', '_xtest_3@example.com', '_xtest_4@example.com']
+
+
+Cleaning up
+-----------
+
+ >>> for mlist in mgr.mailing_lists:
+ ... mgr.delete(mlist)
+ >>> flush()
diff --git a/Mailman/docs/use-usermanager.txt b/Mailman/docs/use-usermanager.txt
new file mode 100644
index 000000000..f79bff8c6
--- /dev/null
+++ b/Mailman/docs/use-usermanager.txt
@@ -0,0 +1,100 @@
+The user manager and rosters
+============================
+
+The IUserManager is how you create, delete, and roster objects. Rosters
+manage collections of users. The Mailman system instantiates an IUserManager
+for you based on the configuration variable MANAGERS_INIT_FUNCTION. The
+instance is accessible on the global config object.
+
+ >>> from Mailman.configuration import config
+ >>> from Mailman.interfaces import IUserManager
+ >>> mgr = config.user_manager
+ >>> IUserManager.providedBy(mgr)
+ True
+
+
+The default roster
+------------------
+
+The user manager always contains at least one roster, the 'null' roster or
+'all inclusive roster'.
+
+ >>> sorted(roster.name for roster in mgr.rosters)
+ ['']
+
+
+Adding rosters
+--------------
+
+You create a roster to hold users. The only thing a roster needs is a name,
+basically just an identifying string.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.interfaces import IRoster
+ >>> roster = mgr.create_roster('roster-1')
+ >>> IRoster.providedBy(roster)
+ True
+ >>> roster.name
+ 'roster-1'
+ >>> flush()
+
+If you try to create a roster with the same name as an existing roster, you
+will get an exception.
+
+ >>> roster_dup = mgr.create_roster('roster-1')
+ Traceback (most recent call last):
+ ...
+ RosterExistsError: roster-1
+
+
+Deleting a roster
+-----------------
+
+Delete the roster, and you can then create it again.
+
+ >>> mgr.delete_roster(roster)
+ >>> flush()
+ >>> roster = mgr.create_roster('roster-1')
+ >>> flush()
+ >>> roster.name
+ 'roster-1'
+
+
+Retrieving a roster
+-------------------
+
+When a roster exists, you can ask the user manager for it and you will always
+get the same object back.
+
+ >>> roster_2 = mgr.get_roster('roster-1')
+ >>> roster_2.name
+ 'roster-1'
+ >>> roster is roster_2
+ True
+
+Trying to get a roster that does not yet exist returns None.
+
+ >>> print mgr.get_roster('no roster')
+ None
+
+
+Iterating over all the rosters
+------------------------------
+
+Once you've created a bunch of rosters, you can use the user manager to
+iterate over all the rosters.
+
+ >>> roster_2 = mgr.create_roster('roster-2')
+ >>> roster_3 = mgr.create_roster('roster-3')
+ >>> roster_4 = mgr.create_roster('roster-4')
+ >>> flush()
+ >>> sorted(roster.name for roster in mgr.rosters)
+ ['', 'roster-1', 'roster-2', 'roster-3', 'roster-4']
+
+
+Cleaning up
+-----------
+
+ >>> for roster in mgr.rosters:
+ ... mgr.delete_roster(roster)
+ >>> flush()
diff --git a/Mailman/docs/users.txt b/Mailman/docs/users.txt
new file mode 100644
index 000000000..caad6b216
--- /dev/null
+++ b/Mailman/docs/users.txt
@@ -0,0 +1,180 @@
+Users
+=====
+
+Users are entities that combine addresses, preferences, and a password
+scheme. Password schemes can be anything from a traditional
+challenge/response type password string to an OpenID url.
+
+
+Create, deleting, and managing users
+------------------------------------
+
+Users are managed by the IUserManager. Users don't have any unique
+identifying information, and no such id is needed to create them.
+
+ >>> from Mailman.database import flush
+ >>> from Mailman.configuration import config
+ >>> mgr = config.user_manager
+ >>> user = mgr.create_user()
+
+Users have a real name, a password scheme, a default profile, and a set of
+addresses that they control. All of these data are None or empty for a newly
+created user.
+
+ >>> user.real_name is None
+ True
+ >>> user.password is None
+ True
+ >>> user.addresses
+ []
+
+You can iterate over all the users in a user manager.
+
+ >>> another_user = mgr.create_user()
+ >>> flush()
+ >>> all_users = list(mgr.users)
+ >>> len(list(all_users))
+ 2
+ >>> user is not another_user
+ True
+ >>> user in all_users
+ True
+ >>> another_user in all_users
+ True
+
+You can also delete users from the user manager.
+
+ >>> mgr.delete_user(user)
+ >>> mgr.delete_user(another_user)
+ >>> flush()
+ >>> len(list(mgr.users))
+ 0
+
+
+Simple user information
+-----------------------
+
+Users may have a real name and a password scheme.
+
+ >>> user = mgr.create_user()
+ >>> user.password = 'my password'
+ >>> user.real_name = 'Zoe Person'
+ >>> flush()
+ >>> only_person = list(mgr.users)[0]
+ >>> only_person.password
+ 'my password'
+ >>> only_person.real_name
+ 'Zoe Person'
+
+The password and real name can be changed at any time.
+
+ >>> user.real_name = 'Zoe X. Person'
+ >>> user.password = 'another password'
+ >>> only_person.real_name
+ 'Zoe X. Person'
+ >>> only_person.password
+ 'another password'
+
+
+Users and addresses
+-------------------
+
+One of the pieces of information that a user links to is a set of email
+addresses, in the form of IAddress objects. A user can control many
+addresses, but addresses may be control by only one user.
+
+Given a user and an address, you can link the two together.
+
+ >>> roster = mgr.get_roster('')
+ >>> address = roster.create('aperson@example.com', 'Anne Person')
+ >>> user.link(address)
+ >>> flush()
+ >>> sorted(address.address for address in user.addresses)
+ ['aperson@example.com']
+
+But don't try to link an address to more than one user.
+
+ >>> another_user = mgr.create_user()
+ >>> another_user.link(address)
+ Traceback (most recent call last):
+ ...
+ AddressAlreadyLinkedError: Anne Person <aperson@example.com>
+
+You can also ask whether a given user controls a given address.
+
+ >>> user.controls(address)
+ True
+ >>> not_my_address = roster.create('bperson@example.com', 'Ben Person')
+ >>> user.controls(not_my_address)
+ False
+
+Given a text email address, the user manager can find the user that controls
+that address.
+
+ >>> mgr.get_user('aperson@example.com') is user
+ True
+ >>> mgr.get_user('bperson@example.com') is None
+ True
+
+Addresses can also be unlinked from a user.
+
+ >>> user.unlink(address)
+ >>> user.controls(address)
+ False
+ >>> mgr.get_user('aperson@example.com') is None
+ True
+
+But don't try to unlink the address from a user it's not linked to.
+
+ >>> user.unlink(address)
+ Traceback (most recent call last):
+ ...
+ AddressNotLinkedError: Anne Person <aperson@example.com>
+ >>> another_user.unlink(address)
+ Traceback (most recent call last):
+ ...
+ AddressNotLinkedError: Anne Person <aperson@example.com>
+ >>> mgr.delete_user(another_user)
+
+
+Users and profiles
+------------------
+
+Users always have a default profile.
+
+ >>> from Mailman.interfaces import IProfile
+ >>> IProfile.providedBy(user.profile)
+ True
+
+A profile is a set of preferences such as whether the user wants to receive an
+acknowledgment of all of their posts to a mailing list...
+
+ >>> user.profile.acknowledge_posts
+ False
+
+...whether the user wants to hide their email addresses on web pages and in
+postings to the list...
+
+ >>> user.profile.hide_address
+ True
+
+...the language code for the user's preferred language...
+
+ >>> user.profile.preferred_language
+ 'en'
+
+...whether the user wants to receive the list's copy of a message if they are
+explicitly named in one of the recipient headers...
+
+ >>> user.profile.receive_list_copy
+ True
+
+...whether the user wants to receive a copy of their own postings...
+
+ >>> user.profile.receive_own_postings
+ True
+
+...and the preferred delivery method.
+
+ >>> print user.profile.delivery_mode
+ DeliveryMode.regular