summaryrefslogtreecommitdiff
path: root/src/mailman/app/docs
diff options
context:
space:
mode:
authorBarry Warsaw2010-01-12 08:27:38 -0500
committerBarry Warsaw2010-01-12 08:27:38 -0500
commit41faffef13f11c793c140d7f18d3b0698685b7a2 (patch)
treebce0b307279a9682afeb57e50d16aa646440e22e /src/mailman/app/docs
parentf137d934b0d5b9e37bd24989e7fb613540ca675d (diff)
downloadmailman-41faffef13f11c793c140d7f18d3b0698685b7a2.tar.gz
mailman-41faffef13f11c793c140d7f18d3b0698685b7a2.tar.zst
mailman-41faffef13f11c793c140d7f18d3b0698685b7a2.zip
Documentation reorganization.
Diffstat (limited to 'src/mailman/app/docs')
-rw-r--r--src/mailman/app/docs/__init__.py0
-rw-r--r--src/mailman/app/docs/bounces.txt103
-rw-r--r--src/mailman/app/docs/chains.txt354
-rw-r--r--src/mailman/app/docs/hooks.txt108
-rw-r--r--src/mailman/app/docs/lifecycle.txt143
-rw-r--r--src/mailman/app/docs/message.txt48
-rw-r--r--src/mailman/app/docs/pipelines.txt193
-rw-r--r--src/mailman/app/docs/styles.txt160
-rw-r--r--src/mailman/app/docs/system.txt27
9 files changed, 1136 insertions, 0 deletions
diff --git a/src/mailman/app/docs/__init__.py b/src/mailman/app/docs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/app/docs/__init__.py
diff --git a/src/mailman/app/docs/bounces.txt b/src/mailman/app/docs/bounces.txt
new file mode 100644
index 000000000..a12305154
--- /dev/null
+++ b/src/mailman/app/docs/bounces.txt
@@ -0,0 +1,103 @@
+=======
+Bounces
+=======
+
+An important feature of Mailman is automatic bounce process.
+
+XXX Many more converted tests go here.
+
+
+Bounces, or message rejection
+=============================
+
+Mailman can also bounce messages back to the original sender. This is
+essentially equivalent to rejecting the message with notification. Mailing
+lists can bounce a message with an optional error message.
+
+ >>> mlist = create_list('_xtest@example.com')
+
+Any message can be bounced.
+
+ >>> msg = message_from_string("""\
+ ... To: _xtest@example.com
+ ... From: aperson@example.com
+ ... Subject: Something important
+ ...
+ ... I sometimes say something important.
+ ... """)
+
+Bounce a message by passing in the original message, and an optional error
+message. The bounced message ends up in the virgin queue, awaiting sending
+to the original messageauthor.
+
+ >>> from mailman.app.bounces import bounce_message
+ >>> bounce_message(mlist, msg)
+ >>> from mailman.testing.helpers import get_queue_messages
+ >>> items = get_queue_messages('virgin')
+ >>> len(items)
+ 1
+ >>> print items[0].msg.as_string()
+ Subject: Something important
+ From: _xtest-owner@example.com
+ To: aperson@example.com
+ MIME-Version: 1.0
+ Content-Type: multipart/mixed; boundary="..."
+ Message-ID: ...
+ Date: ...
+ Precedence: bulk
+ <BLANKLINE>
+ --...
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ [No bounce details are available]
+ --...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ To: _xtest@example.com
+ From: aperson@example.com
+ Subject: Something important
+ <BLANKLINE>
+ I sometimes say something important.
+ <BLANKLINE>
+ --...--
+
+An error message can be given when the message is bounced, and this will be
+included in the payload of the text/plain part. The error message must be
+passed in as an instance of a RejectMessage exception.
+
+ >>> from mailman.core.errors import RejectMessage
+ >>> error = RejectMessage("This wasn't very important after all.")
+ >>> bounce_message(mlist, msg, error)
+ >>> items = get_queue_messages('virgin')
+ >>> len(items)
+ 1
+ >>> print items[0].msg.as_string()
+ Subject: Something important
+ From: _xtest-owner@example.com
+ To: aperson@example.com
+ MIME-Version: 1.0
+ Content-Type: multipart/mixed; boundary="..."
+ Message-ID: ...
+ Date: ...
+ Precedence: bulk
+ <BLANKLINE>
+ --...
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ This wasn't very important after all.
+ --...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ To: _xtest@example.com
+ From: aperson@example.com
+ Subject: Something important
+ <BLANKLINE>
+ I sometimes say something important.
+ <BLANKLINE>
+ --...--
diff --git a/src/mailman/app/docs/chains.txt b/src/mailman/app/docs/chains.txt
new file mode 100644
index 000000000..f9ed156b1
--- /dev/null
+++ b/src/mailman/app/docs/chains.txt
@@ -0,0 +1,354 @@
+======
+Chains
+======
+
+When a new message comes into the system, Mailman uses a set of rule chains to
+decide whether the message gets posted to the list, rejected, discarded, or
+held for moderator approval.
+
+There are a number of built-in chains available that act as end-points in the
+processing of messages.
+
+
+The Discard chain
+=================
+
+The Discard chain simply throws the message away.
+
+ >>> from zope.interface.verify import verifyObject
+ >>> from mailman.interfaces.chain import IChain
+ >>> chain = config.chains['discard']
+ >>> verifyObject(IChain, chain)
+ True
+ >>> print chain.name
+ discard
+ >>> print chain.description
+ Discard a message and stop processing.
+
+ >>> mlist = create_list('_xtest@example.com')
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: _xtest@example.com
+ ... Subject: My first post
+ ... Message-ID: <first>
+ ...
+ ... An important message.
+ ... """)
+
+ >>> from mailman.core.chains import process
+
+ # XXX This checks the vette log file because there is no other evidence
+ # that this chain has done anything.
+ >>> import os
+ >>> fp = open(os.path.join(config.LOG_DIR, 'vette'))
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'discard')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... DISCARD: <first>
+ <BLANKLINE>
+
+
+The Reject chain
+================
+
+The Reject chain bounces the message back to the original sender, and logs
+this action.
+
+ >>> chain = config.chains['reject']
+ >>> verifyObject(IChain, chain)
+ True
+ >>> print chain.name
+ reject
+ >>> print chain.description
+ Reject/bounce a message and stop processing.
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'reject')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... REJECT: <first>
+
+The bounce message is now sitting in the Virgin queue.
+
+ >>> virginq = config.switchboards['virgin']
+ >>> len(virginq.files)
+ 1
+ >>> qmsg, qdata = virginq.dequeue(virginq.files[0])
+ >>> print qmsg.as_string()
+ Subject: My first post
+ From: _xtest-owner@example.com
+ To: aperson@example.com
+ ...
+ [No bounce details are available]
+ ...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ From: aperson@example.com
+ To: _xtest@example.com
+ Subject: My first post
+ Message-ID: <first>
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+ ...
+
+
+The Hold Chain
+==============
+
+The Hold chain places the message into the admin request database and
+depending on the list's settings, sends a notification to both the original
+sender and the list moderators.
+
+ >>> chain = config.chains['hold']
+ >>> verifyObject(IChain, chain)
+ True
+ >>> print chain.name
+ hold
+ >>> print chain.description
+ Hold a message and stop processing.
+
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'hold')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... HOLD: _xtest@example.com post from aperson@example.com held,
+ message-id=<first>: n/a
+ <BLANKLINE>
+
+There are now two messages in the Virgin queue, one to the list moderators and
+one to the original author.
+
+ >>> len(virginq.files)
+ 2
+ >>> qfiles = []
+ >>> for filebase in virginq.files:
+ ... qmsg, qdata = virginq.dequeue(filebase)
+ ... virginq.finish(filebase)
+ ... qfiles.append(qmsg)
+ >>> from operator import itemgetter
+ >>> qfiles.sort(key=itemgetter('to'))
+
+This message is addressed to the mailing list moderators.
+
+ >>> print qfiles[0].as_string()
+ Subject: _xtest@example.com post from aperson@example.com requires approval
+ From: _xtest-owner@example.com
+ To: _xtest-owner@example.com
+ MIME-Version: 1.0
+ ...
+ As list administrator, your authorization is requested for the
+ following mailing list posting:
+ <BLANKLINE>
+ List: _xtest@example.com
+ From: aperson@example.com
+ Subject: My first post
+ Reason: XXX
+ <BLANKLINE>
+ At your convenience, visit:
+ <BLANKLINE>
+ http://lists.example.com/admindb/_xtest@example.com
+ <BLANKLINE>
+ to approve or deny the request.
+ <BLANKLINE>
+ ...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ From: aperson@example.com
+ To: _xtest@example.com
+ Subject: My first post
+ Message-ID: <first>
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+ ...
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Subject: confirm ...
+ Sender: _xtest-request@example.com
+ From: _xtest-request@example.com
+ ...
+ <BLANKLINE>
+ If you reply to this message, keeping the Subject: header intact,
+ Mailman will discard the held message. Do this if the message is
+ spam. If you reply to this message and include an Approved: header
+ with the list password in it, the message will be approved for posting
+ to the list. The Approved: header can also appear in the first line
+ of the body of the reply.
+ ...
+
+This message is addressed to the sender of the message.
+
+ >>> print qfiles[1].as_string()
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Your message to _xtest@example.com awaits moderator approval
+ From: _xtest-bounces@example.com
+ To: aperson@example.com
+ ...
+ Your mail to '_xtest@example.com' with the subject
+ <BLANKLINE>
+ My first post
+ <BLANKLINE>
+ Is being held until the list moderator can review it for approval.
+ <BLANKLINE>
+ The reason it is being held:
+ <BLANKLINE>
+ XXX
+ <BLANKLINE>
+ Either the message will get posted to the list, or you will receive
+ notification of the moderator's decision. If you would like to cancel
+ this posting, please visit the following URL:
+ <BLANKLINE>
+ http://lists.example.com/confirm/_xtest@example.com/...
+ <BLANKLINE>
+ <BLANKLINE>
+
+In addition, the pending database is holding the original messages, waiting
+for them to be disposed of by the original author or the list moderators. The
+database is essentially a dictionary, with the keys being the randomly
+selected tokens included in the urls and the values being a 2-tuple where the
+first item is a type code and the second item is a message id.
+
+ >>> import re
+ >>> cookie = None
+ >>> for line in qfiles[1].get_payload().splitlines():
+ ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line)
+ ... if mo:
+ ... cookie = mo.group('cookie')
+ ... break
+ >>> assert cookie is not None, 'No confirmation token found'
+
+ >>> from mailman.interfaces.pending import IPendings
+ >>> from zope.component import getUtility
+
+ >>> data = getUtility(IPendings).confirm(cookie)
+ >>> sorted(data.items())
+ [(u'id', ...), (u'type', u'held message')]
+
+The message itself is held in the message store.
+
+ >>> from mailman.interfaces.requests import IRequests
+ >>> list_requests = getUtility(IRequests).get_list_requests(mlist)
+ >>> rkey, rdata = list_requests.get_request(data['id'])
+
+ >>> from mailman.interfaces.messages import IMessageStore
+ >>> from zope.component import getUtility
+ >>> msg = getUtility(IMessageStore).get_message_by_id(
+ ... rdata['_mod_message_id'])
+
+ >>> print msg.as_string()
+ From: aperson@example.com
+ To: _xtest@example.com
+ Subject: My first post
+ Message-ID: <first>
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+
+
+The Accept chain
+================
+
+The Accept chain sends the message on the 'prep' queue, where it will be
+processed and sent on to the list membership.
+
+ >>> chain = config.chains['accept']
+ >>> verifyObject(IChain, chain)
+ True
+ >>> print chain.name
+ accept
+ >>> print chain.description
+ Accept a message.
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {}, 'accept')
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... ACCEPT: <first>
+
+ >>> pipelineq = config.switchboards['pipeline']
+ >>> len(pipelineq.files)
+ 1
+ >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
+ >>> print qmsg.as_string()
+ From: aperson@example.com
+ To: _xtest@example.com
+ Subject: My first post
+ Message-ID: <first>
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+
+
+Run-time chains
+===============
+
+We can also define chains at run time, and these chains can be mutated.
+Run-time chains are made up of links where each link associates both a rule
+and a 'jump'. The rule is really a rule name, which is looked up when
+needed. The jump names a chain which is jumped to if the rule matches.
+
+There is one built-in run-time chain, called appropriately 'built-in'. This
+is the default chain to use when no other input chain is defined for a mailing
+list. It runs through the default rules, providing functionality similar to
+the Hold handler from previous versions of Mailman.
+
+ >>> chain = config.chains['built-in']
+ >>> verifyObject(IChain, chain)
+ True
+ >>> print chain.name
+ built-in
+ >>> print chain.description
+ The built-in moderation chain.
+
+The previously created message is innocuous enough that it should pass through
+all default rules. This message will end up in the pipeline queue.
+
+ >>> file_pos = fp.tell()
+ >>> process(mlist, msg, {})
+ >>> fp.seek(file_pos)
+ >>> print 'LOG:', fp.read()
+ LOG: ... ACCEPT: <first>
+
+ >>> qmsg, qdata = pipelineq.dequeue(pipelineq.files[0])
+ >>> print qmsg.as_string()
+ From: aperson@example.com
+ To: _xtest@example.com
+ Subject: My first post
+ Message-ID: <first>
+ X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
+ X-Mailman-Rule-Misses: approved; emergency; loop; administrivia;
+ implicit-dest;
+ max-recipients; max-size; news-moderation; no-subject;
+ suspicious-header
+ <BLANKLINE>
+ An important message.
+ <BLANKLINE>
+
+In addition, the message metadata now contains lists of all rules that have
+hit and all rules that have missed.
+
+ >>> sorted(qdata['rule_hits'])
+ []
+ >>> for rule_name in sorted(qdata['rule_misses']):
+ ... print rule_name
+ administrivia
+ approved
+ emergency
+ implicit-dest
+ loop
+ max-recipients
+ max-size
+ news-moderation
+ no-subject
+ suspicious-header
diff --git a/src/mailman/app/docs/hooks.txt b/src/mailman/app/docs/hooks.txt
new file mode 100644
index 000000000..14dc76667
--- /dev/null
+++ b/src/mailman/app/docs/hooks.txt
@@ -0,0 +1,108 @@
+=====
+Hooks
+=====
+
+Mailman defines two initialization hooks, one which is run early in the
+initialization process and the other run late in the initialization process.
+Hooks name an importable callable so it must be accessible on sys.path.
+
+ >>> import os, sys
+ >>> from mailman.config import config
+ >>> config_directory = os.path.dirname(config.filename)
+ >>> sys.path.insert(0, config_directory)
+
+ >>> hook_path = os.path.join(config_directory, 'hooks.py')
+ >>> with open(hook_path, 'w') as fp:
+ ... print >> fp, """\
+ ... counter = 1
+ ... def pre_hook():
+ ... global counter
+ ... print 'pre-hook:', counter
+ ... counter += 1
+ ...
+ ... def post_hook():
+ ... global counter
+ ... print 'post-hook:', counter
+ ... counter += 1
+ ... """
+ >>> fp.close()
+
+
+Pre-hook
+========
+
+We can set the pre-hook in the configuration file.
+
+ >>> config_path = os.path.join(config_directory, 'hooks.cfg')
+ >>> with open(config_path, 'w') as fp:
+ ... print >> fp, """\
+ ... [meta]
+ ... extends: test.cfg
+ ...
+ ... [mailman]
+ ... pre_hook: hooks.pre_hook
+ ... """
+
+The hooks are run in the second and third steps of initialization. However,
+we can't run those initialization steps in process, so call a command line
+script that will produce no output to force the hooks to run.
+
+ >>> import subprocess
+ >>> def call():
+ ... proc = subprocess.Popen(
+ ... 'bin/mailman lists --domain ignore -q'.split(),
+ ... cwd='../..', # testrunner runs from ./parts/test
+ ... env=dict(MAILMAN_CONFIG_FILE=config_path,
+ ... PYTHONPATH=config_directory),
+ ... stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ ... stdout, stderr = proc.communicate()
+ ... assert proc.returncode == 0, stderr
+ ... print stdout
+
+ >>> call()
+ pre-hook: 1
+ <BLANKLINE>
+
+ >>> os.remove(config_path)
+
+
+Post-hook
+=========
+
+We can set the post-hook in the configuration file.
+
+ >>> with open(config_path, 'w') as fp:
+ ... print >> fp, """\
+ ... [meta]
+ ... extends: test.cfg
+ ...
+ ... [mailman]
+ ... post_hook: hooks.post_hook
+ ... """
+
+ >>> call()
+ post-hook: 1
+ <BLANKLINE>
+
+ >>> os.remove(config_path)
+
+
+Running both hooks
+==================
+
+We can set the pre- and post-hooks in the configuration file.
+
+ >>> with open(config_path, 'w') as fp:
+ ... print >> fp, """\
+ ... [meta]
+ ... extends: test.cfg
+ ...
+ ... [mailman]
+ ... pre_hook: hooks.pre_hook
+ ... post_hook: hooks.post_hook
+ ... """
+
+ >>> call()
+ pre-hook: 1
+ post-hook: 2
+ <BLANKLINE>
diff --git a/src/mailman/app/docs/lifecycle.txt b/src/mailman/app/docs/lifecycle.txt
new file mode 100644
index 000000000..a1cd50825
--- /dev/null
+++ b/src/mailman/app/docs/lifecycle.txt
@@ -0,0 +1,143 @@
+=================================
+Application level list life cycle
+=================================
+
+The low-level way to create and delete a mailing list is to use the
+IListManager interface. This interface simply adds or removes the appropriate
+database entries to record the list's creation.
+
+There is a higher level interface for creating and deleting mailing lists
+which performs additional tasks such as:
+
+ * validating the list's posting address (which also serves as the list's
+ fully qualified name);
+ * ensuring that the list's domain is registered;
+ * applying all matching styles to the new list;
+ * creating and assigning list owners;
+ * notifying watchers of list creation;
+ * creating ancillary artifacts (such as the list's on-disk directory)
+
+ >>> from mailman.app.lifecycle import create_list
+
+
+Posting address validation
+==========================
+
+If you try to use the higher-level interface to create a mailing list with a
+bogus posting address, you get an exception.
+
+ >>> create_list('not a valid address')
+ Traceback (most recent call last):
+ ...
+ 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.
+
+ >>> create_list('test@example.org')
+ Traceback (most recent call last):
+ ...
+ BadDomainSpecificationError: example.org
+
+
+Creating a list applies its styles
+==================================
+
+Start by registering a test style.
+
+ >>> from zope.interface import implements
+ >>> from mailman.interfaces.styles import IStyle
+ >>> class TestStyle(object):
+ ... implements(IStyle)
+ ... name = 'test'
+ ... priority = 10
+ ... def apply(self, mailing_list):
+ ... # Just does something very simple.
+ ... mailing_list.msg_footer = 'test footer'
+ ... def match(self, mailing_list, styles):
+ ... # Applies to any test list
+ ... if 'test' in mailing_list.fqdn_listname:
+ ... styles.append(self)
+
+ >>> config.style_manager.register(TestStyle())
+
+Using the higher level interface for creating a list, applies all matching
+list styles.
+
+ >>> mlist_1 = create_list('test_1@example.com')
+ >>> print mlist_1.fqdn_listname
+ test_1@example.com
+ >>> print mlist_1.msg_footer
+ test footer
+
+
+Creating a list with owners
+===========================
+
+You can also specify a list of owner email addresses. If these addresses are
+not yet known, they will be registered, and new users will be linked to them.
+However the addresses are not verified.
+
+ >>> owners = ['aperson@example.com', 'bperson@example.com',
+ ... 'cperson@example.com', 'dperson@example.com']
+ >>> mlist_2 = create_list('test_2@example.com', owners)
+ >>> print mlist_2.fqdn_listname
+ test_2@example.com
+ >>> print mlist_2.msg_footer
+ test footer
+ >>> sorted(addr.address for addr in mlist_2.owners.addresses)
+ [u'aperson@example.com', u'bperson@example.com',
+ u'cperson@example.com', u'dperson@example.com']
+
+None of the owner addresses are verified.
+
+ >>> any(addr.verified_on is not None for addr in mlist_2.owners.addresses)
+ False
+
+However, all addresses are linked to users.
+
+ >>> # The owners have no names yet
+ >>> len(list(mlist_2.owners.users))
+ 4
+
+If you create a mailing list with owner addresses that are already known to
+the system, they won't be created again.
+
+ >>> from mailman.interfaces.usermanager import IUserManager
+ >>> from zope.component import getUtility
+ >>> user_manager = getUtility(IUserManager)
+
+ >>> user_a = user_manager.get_user('aperson@example.com')
+ >>> user_b = user_manager.get_user('bperson@example.com')
+ >>> user_c = user_manager.get_user('cperson@example.com')
+ >>> user_d = user_manager.get_user('dperson@example.com')
+ >>> user_a.real_name = 'Anne Person'
+ >>> user_b.real_name = 'Bart Person'
+ >>> user_c.real_name = 'Caty Person'
+ >>> user_d.real_name = 'Dirk Person'
+
+ >>> mlist_3 = create_list('test_3@example.com', owners)
+ >>> sorted(user.real_name for user in mlist_3.owners.users)
+ [u'Anne Person', u'Bart Person', u'Caty Person', u'Dirk Person']
+
+
+Removing a list
+===============
+
+Removing a mailing list deletes the list, all its subscribers, and any related
+artifacts.
+
+ >>> from mailman.app.lifecycle import remove_list
+ >>> remove_list(mlist_2.fqdn_listname, mlist_2, True)
+
+ >>> from mailman.interfaces.listmanager import IListManager
+ >>> from zope.component import getUtility
+ >>> print getUtility(IListManager).get('test_2@example.com')
+ None
+
+We should now be able to completely recreate the mailing list.
+
+ >>> mlist_2a = create_list('test_2@example.com', owners)
+ >>> sorted(addr.address for addr in mlist_2a.owners.addresses)
+ [u'aperson@example.com', u'bperson@example.com',
+ u'cperson@example.com', u'dperson@example.com']
diff --git a/src/mailman/app/docs/message.txt b/src/mailman/app/docs/message.txt
new file mode 100644
index 000000000..41607ff44
--- /dev/null
+++ b/src/mailman/app/docs/message.txt
@@ -0,0 +1,48 @@
+========
+Messages
+========
+
+Mailman has its own Message classes, derived from the standard
+email.message.Message class, but providing additional useful methods.
+
+
+User notifications
+==================
+
+When Mailman needs to send a message to a user, it creates a UserNotification
+instance, and then calls the .send() method on this object. This method
+requires a mailing list instance.
+
+ >>> mlist = create_list('_xtest@example.com')
+
+The UserNotification constructor takes the recipient address, the sender
+address, an optional subject, optional body text, and optional language.
+
+ >>> from mailman.email.message import UserNotification
+ >>> msg = UserNotification(
+ ... 'aperson@example.com',
+ ... '_xtest@example.com',
+ ... 'Something you need to know',
+ ... 'I needed to tell you this.')
+ >>> msg.send(mlist)
+
+The message will end up in the virgin queue.
+
+ >>> switchboard = config.switchboards['virgin']
+ >>> len(switchboard.files)
+ 1
+ >>> filebase = switchboard.files[0]
+ >>> qmsg, qmsgdata = switchboard.dequeue(filebase)
+ >>> switchboard.finish(filebase)
+ >>> print qmsg.as_string()
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Subject: Something you need to know
+ From: _xtest@example.com
+ To: aperson@example.com
+ Message-ID: ...
+ Date: ...
+ Precedence: bulk
+ <BLANKLINE>
+ I needed to tell you this.
diff --git a/src/mailman/app/docs/pipelines.txt b/src/mailman/app/docs/pipelines.txt
new file mode 100644
index 000000000..cf848f1d9
--- /dev/null
+++ b/src/mailman/app/docs/pipelines.txt
@@ -0,0 +1,193 @@
+=========
+Pipelines
+=========
+
+This runner's purpose in life is to process messages that have been accepted
+for posting, applying any modifications and also sending copies of the message
+to the archives, digests, nntp, and outgoing queues. Pipelines are named and
+consist of a sequence of handlers, each of which is applied in turn. Unlike
+rules and chains, there is no way to stop a pipeline from processing the
+message once it's started.
+
+ >>> mlist = create_list('xtest@example.com')
+ >>> print mlist.pipeline
+ built-in
+ >>> from mailman.core.pipelines import process
+
+
+Processing a message
+====================
+
+Messages hit the pipeline after they've been accepted for posting.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... To: xtest@example.com
+ ... Subject: My first post
+ ... Message-ID: <first>
+ ...
+ ... First post!
+ ... """)
+ >>> msgdata = {}
+ >>> process(mlist, msg, msgdata, mlist.pipeline)
+
+The message has been modified with additional headers, footer decorations,
+etc.
+
+ >>> print msg.as_string()
+ From: aperson@example.com
+ To: xtest@example.com
+ Message-ID: <first>
+ Subject: [Xtest] My first post
+ X-BeenThere: xtest@example.com
+ X-Mailman-Version: ...
+ Precedence: list
+ List-Id: <xtest.example.com>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Post: <mailto:xtest@example.com>
+ List-Subscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-join@example.com>
+ Archived-At:
+ http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Unsubscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-leave@example.com>
+ List-Archive: <http://lists.example.com/archives/xtest@example.com>
+ List-Help: <mailto:xtest-request@example.com?subject=help>
+ <BLANKLINE>
+ First post!
+ <BLANKLINE>
+
+And the message metadata has information about recipients and other stuff.
+However there are currently no recipients for this message.
+
+ >>> dump_msgdata(msgdata)
+ original_sender : aperson@example.com
+ origsubj : My first post
+ recipients : set([])
+ stripped_subject: My first post
+
+And the message is now sitting in various other processing queues.
+
+ >>> from mailman.testing.helpers import get_queue_messages
+ >>> messages = get_queue_messages('archive')
+ >>> len(messages)
+ 1
+ >>> print messages[0].msg.as_string()
+ From: aperson@example.com
+ To: xtest@example.com
+ Message-ID: <first>
+ Subject: [Xtest] My first post
+ X-BeenThere: xtest@example.com
+ X-Mailman-Version: ...
+ Precedence: list
+ List-Id: <xtest.example.com>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Post: <mailto:xtest@example.com>
+ List-Subscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-join@example.com>
+ Archived-At:
+ http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Unsubscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-leave@example.com>
+ List-Archive: <http://lists.example.com/archives/xtest@example.com>
+ List-Help: <mailto:xtest-request@example.com?subject=help>
+ <BLANKLINE>
+ First post!
+ <BLANKLINE>
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ original_sender : aperson@example.com
+ origsubj : My first post
+ recipients : set([])
+ stripped_subject: My first post
+ version : 3
+
+This mailing list is not linked to an NNTP newsgroup, so there's nothing in
+the outgoing nntp queue.
+
+ >>> messages = get_queue_messages('news')
+ >>> len(messages)
+ 0
+
+This is the message that will actually get delivered to end recipients.
+
+ >>> messages = get_queue_messages('out')
+ >>> len(messages)
+ 1
+ >>> print messages[0].msg.as_string()
+ From: aperson@example.com
+ To: xtest@example.com
+ Message-ID: <first>
+ Subject: [Xtest] My first post
+ X-BeenThere: xtest@example.com
+ X-Mailman-Version: ...
+ Precedence: list
+ List-Id: <xtest.example.com>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Post: <mailto:xtest@example.com>
+ List-Subscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-join@example.com>
+ Archived-At:
+ http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Unsubscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-leave@example.com>
+ List-Archive: <http://lists.example.com/archives/xtest@example.com>
+ List-Help: <mailto:xtest-request@example.com?subject=help>
+ <BLANKLINE>
+ First post!
+ <BLANKLINE>
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : xtest@example.com
+ original_sender : aperson@example.com
+ origsubj : My first post
+ recipients : set([])
+ stripped_subject: My first post
+ version : 3
+
+There's now one message in the digest mailbox, getting ready to be sent.
+
+ >>> from mailman.testing.helpers import digest_mbox
+ >>> digest = digest_mbox(mlist)
+ >>> sum(1 for mboxmsg in digest)
+ 1
+ >>> print list(digest)[0].as_string()
+ From: aperson@example.com
+ To: xtest@example.com
+ Message-ID: <first>
+ Subject: [Xtest] My first post
+ X-BeenThere: xtest@example.com
+ X-Mailman-Version: ...
+ Precedence: list
+ List-Id: <xtest.example.com>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Post: <mailto:xtest@example.com>
+ List-Subscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-join@example.com>
+ Archived-At:
+ http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Unsubscribe:
+ <http://lists.example.com/listinfo/xtest@example.com>,
+ <mailto:xtest-leave@example.com>
+ List-Archive: <http://lists.example.com/archives/xtest@example.com>
+ List-Help: <mailto:xtest-request@example.com?subject=help>
+ <BLANKLINE>
+ First post!
+ <BLANKLINE>
+ <BLANKLINE>
+
+
+Clean up the digests
+====================
+
+ >>> digest.clear()
+ >>> digest.flush()
+ >>> sum(1 for msg in digest_mbox(mlist))
+ 0
diff --git a/src/mailman/app/docs/styles.txt b/src/mailman/app/docs/styles.txt
new file mode 100644
index 000000000..10312cd3a
--- /dev/null
+++ b/src/mailman/app/docs/styles.txt
@@ -0,0 +1,160 @@
+===========
+List styles
+===========
+
+List styles are a way to name and apply a canned collection of attribute
+settings. Every style has a name, which must be unique within the context of
+a specific style manager. There is usually only one global style manager.
+
+Styles also have a priority, which allows you to specify the order in which
+multiple styles will be applied. A style has a `match` function which is used
+to determine whether the style should be applied to a particular mailing list
+or not. And finally, application of a style to a mailing list can really
+modify the mailing list any way it wants.
+
+Let's start with a vanilla mailing list and a default style manager.
+
+ >>> from mailman.interfaces.listmanager import IListManager
+ >>> from zope.component import getUtility
+ >>> mlist = getUtility(IListManager).create('_xtest@example.com')
+
+ >>> from mailman.styles.manager import StyleManager
+ >>> style_manager = StyleManager()
+ >>> style_manager.populate()
+ >>> sorted(style.name for style in style_manager.styles)
+ ['default']
+
+
+The default style
+=================
+
+There is a default style which implements the legacy application of list
+defaults from previous versions of Mailman. This style only matching a
+mailing list when no other styles match, and it has the lowest priority. The
+low priority means that it is matched last and if it matches, it is applied
+last.
+
+ >>> default_style = style_manager.get('default')
+ >>> default_style.name
+ 'default'
+ >>> default_style.priority
+ 0
+ >>> sorted(style.name for style in style_manager.styles)
+ ['default']
+
+Given a mailing list, you can ask the style manager to find all the styles
+that match the list. The registered styles will be sorted by decreasing
+priority and each style's `match()` method will be called in turn. The sorted
+list of matching styles will be returned -- but not applied -- by the style
+manager's `lookup()` method.
+
+ >>> [style.name for style in style_manager.lookup(mlist)]
+ ['default']
+
+
+Registering styles
+==================
+
+New styles must implement the IStyle interface.
+
+ >>> from zope.interface import implements
+ >>> from mailman.interfaces.styles import IStyle
+ >>> class TestStyle(object):
+ ... implements(IStyle)
+ ... name = 'test'
+ ... priority = 10
+ ... def apply(self, mailing_list):
+ ... # Just does something very simple.
+ ... mailing_list.msg_footer = 'test footer'
+ ... def match(self, mailing_list, styles):
+ ... # Applies to any test list
+ ... if 'test' in mailing_list.fqdn_listname:
+ ... styles.append(self)
+
+You can register a new style with the style manager.
+
+ >>> style_manager.register(TestStyle())
+
+And now if you lookup matching styles, you should find only the new test
+style. This is because the default style only gets applied when no other
+styles match the mailing list.
+
+ >>> sorted(style.name for style in style_manager.lookup(mlist))
+ [u'test']
+ >>> for style in style_manager.lookup(mlist):
+ ... style.apply(mlist)
+ >>> print mlist.msg_footer
+ test footer
+
+
+Style priority
+==============
+
+When multiple styles match a particular mailing list, they are applied in
+descending order of priority. In other words, a priority zero style would be
+applied last.
+
+ >>> class AnotherTestStyle(TestStyle):
+ ... name = 'another'
+ ... priority = 5
+ ... # Use the base class's match() method.
+ ... def apply(self, mailing_list):
+ ... mailing_list.msg_footer = 'another footer'
+
+ >>> mlist.msg_footer = ''
+ >>> mlist.msg_footer
+ u''
+ >>> style_manager.register(AnotherTestStyle())
+ >>> for style in style_manager.lookup(mlist):
+ ... style.apply(mlist)
+ >>> print mlist.msg_footer
+ another footer
+
+You can change the priority of a style, and if you reapply the styles, they
+will take effect in the new priority order.
+
+ >>> style_1 = style_manager.get('test')
+ >>> style_1.priority = 5
+ >>> style_2 = style_manager.get('another')
+ >>> style_2.priority = 10
+ >>> for style in style_manager.lookup(mlist):
+ ... style.apply(mlist)
+ >>> print mlist.msg_footer
+ test footer
+
+
+Unregistering styles
+====================
+
+You can unregister a style, making it unavailable in the future.
+
+ >>> style_manager.unregister(style_2)
+ >>> sorted(style.name for style in style_manager.lookup(mlist))
+ [u'test']
+
+
+Corner cases
+============
+
+If you register a style with the same name as an already registered style, you
+get an exception.
+
+ >>> style_manager.register(TestStyle())
+ Traceback (most recent call last):
+ ...
+ DuplicateStyleError: test
+
+If you try to register an object that isn't a style, you get an exception.
+
+ >>> style_manager.register(object())
+ Traceback (most recent call last):
+ ...
+ DoesNotImplement: An object does not implement interface
+ <InterfaceClass mailman.interfaces.styles.IStyle>
+
+If you try to unregister a style that isn't registered, you get an exception.
+
+ >>> style_manager.unregister(style_2)
+ Traceback (most recent call last):
+ ...
+ KeyError: u'another'
diff --git a/src/mailman/app/docs/system.txt b/src/mailman/app/docs/system.txt
new file mode 100644
index 000000000..035833047
--- /dev/null
+++ b/src/mailman/app/docs/system.txt
@@ -0,0 +1,27 @@
+===============
+System versions
+===============
+
+Mailman system information is available through the System object, which
+implements the ISystem interface.
+
+ >>> from mailman.interfaces.system import ISystem
+ >>> from mailman.core.system import system
+ >>> from zope.interface.verify import verifyObject
+
+ >>> verifyObject(ISystem, system)
+ True
+
+The Mailman version is available via the system object.
+
+ >>> print system.mailman_version
+ GNU Mailman ...
+
+The Python version running underneath is also available via the system
+object.
+
+ # The entire python_version string is variable, so this is the best test
+ # we can do.
+ >>> import sys
+ >>> system.python_version == sys.version
+ True