summaryrefslogtreecommitdiff
path: root/mailman/docs
diff options
context:
space:
mode:
Diffstat (limited to 'mailman/docs')
-rw-r--r--mailman/docs/__init__.py0
-rw-r--r--mailman/docs/addresses.txt231
-rw-r--r--mailman/docs/archivers.txt184
-rw-r--r--mailman/docs/bounces.txt107
-rw-r--r--mailman/docs/chains.txt345
-rw-r--r--mailman/docs/domains.txt46
-rw-r--r--mailman/docs/languages.txt104
-rw-r--r--mailman/docs/lifecycle.txt136
-rw-r--r--mailman/docs/listmanager.txt88
-rw-r--r--mailman/docs/membership.txt230
-rw-r--r--mailman/docs/message.txt48
-rw-r--r--mailman/docs/messagestore.txt113
-rw-r--r--mailman/docs/mlist-addresses.txt76
-rw-r--r--mailman/docs/pending.txt94
-rw-r--r--mailman/docs/pipelines.txt186
-rw-r--r--mailman/docs/registration.txt362
-rw-r--r--mailman/docs/requests.txt883
-rw-r--r--mailman/docs/styles.txt156
-rw-r--r--mailman/docs/usermanager.txt124
-rw-r--r--mailman/docs/users.txt195
20 files changed, 0 insertions, 3708 deletions
diff --git a/mailman/docs/__init__.py b/mailman/docs/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/mailman/docs/__init__.py
+++ /dev/null
diff --git a/mailman/docs/addresses.txt b/mailman/docs/addresses.txt
deleted file mode 100644
index 9eccb2673..000000000
--- a/mailman/docs/addresses.txt
+++ /dev/null
@@ -1,231 +0,0 @@
-Email addresses
-===============
-
-Addresses represent a text email address, along with some meta data about
-those addresses, such as their registration date, and whether and when they've
-been validated. Addresses may be linked to the users that Mailman knows
-about. Addresses are subscribed to mailing lists though members.
-
- >>> usermgr = config.db.user_manager
-
-
-Creating addresses
-------------------
-
-Addresses are created directly through the user manager, which starts out with
-no addresses.
-
- >>> sorted(address.address for address in usermgr.addresses)
- []
-
-Creating an unlinked email address is straightforward.
-
- >>> address_1 = usermgr.create_address(u'aperson@example.com')
- >>> sorted(address.address for address in usermgr.addresses)
- [u'aperson@example.com']
-
-However, such addresses have no real name.
-
- >>> address_1.real_name
- u''
-
-You can also create an email address object with a real name.
-
- >>> address_2 = usermgr.create_address(
- ... u'bperson@example.com', u'Ben Person')
- >>> sorted(address.address for address in usermgr.addresses)
- [u'aperson@example.com', u'bperson@example.com']
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'', u'Ben Person']
-
-The str() of the address is the RFC 2822 preferred originator format, while
-the repr() carries more information.
-
- >>> str(address_2)
- 'Ben Person <bperson@example.com>'
- >>> repr(address_2)
- '<Address: Ben Person <bperson@example.com> [not verified] at 0x...>'
-
-You can assign real names to existing addresses.
-
- >>> address_1.real_name = u'Anne Person'
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'Anne Person', u'Ben Person']
-
-These addresses are not linked to users, and can be seen by searching the user
-manager for an associated user.
-
- >>> print usermgr.get_user(u'aperson@example.com')
- None
- >>> print usermgr.get_user(u'bperson@example.com')
- None
-
-You can create email addresses that are linked to users by using a different
-interface.
-
- >>> user_1 = usermgr.create_user(u'cperson@example.com', u'Claire Person')
- >>> sorted(address.address for address in user_1.addresses)
- [u'cperson@example.com']
- >>> sorted(address.address for address in usermgr.addresses)
- [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com']
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'Anne Person', u'Ben Person', u'Claire Person']
-
-And now you can find the associated user.
-
- >>> print usermgr.get_user(u'aperson@example.com')
- None
- >>> print usermgr.get_user(u'bperson@example.com')
- None
- >>> usermgr.get_user(u'cperson@example.com')
- <User "Claire Person" at ...>
-
-
-Deleting addresses
-------------------
-
-You can remove an unlinked address from the user manager.
-
- >>> usermgr.delete_address(address_1)
- >>> sorted(address.address for address in usermgr.addresses)
- [u'bperson@example.com', u'cperson@example.com']
- >>> sorted(address.real_name for address in usermgr.addresses)
- [u'Ben Person', u'Claire Person']
-
-Deleting a linked address does not delete the user, but it does unlink the
-address from the user.
-
- >>> sorted(address.address for address in user_1.addresses)
- [u'cperson@example.com']
- >>> user_1.controls(u'cperson@example.com')
- True
- >>> address_3 = list(user_1.addresses)[0]
- >>> usermgr.delete_address(address_3)
- >>> sorted(address.address for address in user_1.addresses)
- []
- >>> user_1.controls(u'cperson@example.com')
- False
- >>> sorted(address.address for address in usermgr.addresses)
- [u'bperson@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 is set by default.
-
- >>> address_4 = usermgr.create_address(
- ... u'dperson@example.com', u'Dan Person')
- >>> print address_4.registered_on
- None
- >>> print address_4.verified_on
- None
-
-The registered date takes a Python datetime object.
-
- >>> from datetime import datetime
- >>> address_4.registered_on = datetime(2007, 5, 8, 22, 54, 1)
- >>> print address_4.registered_on
- 2007-05-08 22:54:01
- >>> print address_4.verified_on
- None
-
-And of course, you can also set the validation date.
-
- >>> address_4.verified_on = datetime(2007, 5, 13, 22, 54, 1)
- >>> print address_4.registered_on
- 2007-05-08 22:54:01
- >>> print address_4.verified_on
- 2007-05-13 22:54:01
-
-
-Subscriptions
--------------
-
-Addresses get subscribed to mailing lists, not users. When the address is
-subscribed, a role is specified.
-
- >>> address_5 = usermgr.create_address(
- ... u'eperson@example.com', u'Elly Person')
- >>> mlist = config.db.list_manager.create(u'_xtext@example.com')
- >>> from mailman.interfaces.member import MemberRole
- >>> address_5.subscribe(mlist, MemberRole.owner)
- <Member: Elly Person <eperson@example.com> on
- _xtext@example.com as MemberRole.owner>
- >>> address_5.subscribe(mlist, MemberRole.member)
- <Member: Elly Person <eperson@example.com> on
- _xtext@example.com as MemberRole.member>
-
-Now Elly is both an owner and a member of the mailing list.
-
- >>> sorted(mlist.owners.members)
- [<Member: Elly Person <eperson@example.com> on
- _xtext@example.com as MemberRole.owner>]
- >>> sorted(mlist.moderators.members)
- []
- >>> sorted(mlist.administrators.members)
- [<Member: Elly Person <eperson@example.com> on
- _xtext@example.com as MemberRole.owner>]
- >>> sorted(mlist.members.members)
- [<Member: Elly Person <eperson@example.com> on
- _xtext@example.com as MemberRole.member>]
- >>> sorted(mlist.regular_members.members)
- [<Member: Elly Person <eperson@example.com> on
- _xtext@example.com as MemberRole.member>]
- >>> sorted(mlist.digest_members.members)
- []
-
-
-Case-preserved addresses
-------------------------
-
-Technically speaking, email addresses are case sensitive in the local part.
-Mailman preserves the case of addresses and uses the case preserved version
-when sending the user a message, but it treats addresses that are different in
-case equivalently in all other situations.
-
- >>> address_6 = usermgr.create_address(
- ... u'FPERSON@example.com', u'Frank Person')
-
-The str() of such an address prints the RFC 2822 preferred originator format
-with the original case-preserved address. The repr() contains all the gory
-details.
-
- >>> str(address_6)
- 'Frank Person <FPERSON@example.com>'
- >>> repr(address_6)
- '<Address: Frank Person <FPERSON@example.com> [not verified]
- key: fperson@example.com at 0x...>'
-
-Both the case-insensitive version of the address and the original
-case-preserved version are available on attributes of the IAddress object.
-
- >>> address_6.address
- u'fperson@example.com'
- >>> address_6.original_address
- u'FPERSON@example.com'
-
-Because addresses are case-insensitive for all other purposes, you cannot
-create an address that differs only in case.
-
- >>> usermgr.create_address(u'fperson@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> usermgr.create_address(u'fperson@EXAMPLE.COM')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> usermgr.create_address(u'FPERSON@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
-
-You can get the address using either the lower cased version or case-preserved
-version. In fact, searching for an address is case insensitive.
-
- >>> usermgr.get_address(u'fperson@example.com').address
- u'fperson@example.com'
- >>> usermgr.get_address(u'FPERSON@example.com').address
- u'fperson@example.com'
diff --git a/mailman/docs/archivers.txt b/mailman/docs/archivers.txt
deleted file mode 100644
index ef36a25ac..000000000
--- a/mailman/docs/archivers.txt
+++ /dev/null
@@ -1,184 +0,0 @@
-Archivers
-=========
-
-Mailman supports pluggable archivers, and it comes with several default
-archivers.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'test@example.com')
- >>> msg = message_from_string("""\
- ... From: aperson@example.org
- ... To: test@example.com
- ... Subject: An archived message
- ... Message-ID: <12345>
- ...
- ... Here is an archived message.
- ... """)
-
-Archivers support an interface which provides the RFC 2369 List-Archive
-header, and one that provides a 'permalink' to the specific message object in
-the archive. This latter is appropriate for the message footer or for the RFC
-5064 Archived-At header.
-
-Pipermail does not support a permalink, so that interface returns None.
-Mailman defines a draft spec for how list servers and archivers can
-interoperate.
-
- >>> archivers = {}
- >>> from operator import attrgetter
- >>> for archiver in sorted(config.archivers, key=attrgetter('name')):
- ... print archiver.name
- ... print ' ', archiver.list_url(mlist)
- ... print ' ', archiver.permalink(mlist, msg)
- ... archivers[archiver.name] = archiver
- mail-archive
- http://go.mail-archive.dev/test%40example.com
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
- mhonarc
- http://lists.example.com/.../test@example.com
- http://lists.example.com/.../RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
- pipermail
- http://www.example.com/pipermail/test@example.com
- None
- prototype
- http://lists.example.com
- http://lists.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
-
-
-Sending the message to the archiver
------------------------------------
-
-The archiver is also able to archive the message.
-
- >>> archivers['pipermail'].archive_message(mlist, msg)
-
- >>> import os
- >>> from mailman.interfaces.archiver import IPipermailMailingList
- >>> pckpath = os.path.join(
- ... IPipermailMailingList(mlist).archive_dir(),
- ... 'pipermail.pck')
- >>> os.path.exists(pckpath)
- True
-
-Note however that the prototype archiver can't archive messages.
-
- >>> archivers['prototype'].archive_message(mlist, msg)
- Traceback (most recent call last):
- ...
- NotImplementedError
-
-
-The Mail-Archive.com
---------------------
-
-The Mail-Archive <http://www.mail-archive.com> is a public archiver that can
-be used to archive message for free. Mailman comes with a plugin for this
-archiver; by enabling it messages to public lists will get sent there
-automatically.
-
- >>> archiver = archivers['mail-archive']
- >>> print archiver.list_url(mlist)
- http://go.mail-archive.dev/test%40example.com
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
-
-To archive the message, the archiver actually mails the message to a special
-address at the Mail-Archive.
-
- >>> archiver.archive_message(mlist, msg)
-
- >>> from mailman.queue.outgoing import OutgoingRunner
- >>> from mailman.testing.helpers import make_testable_runner
- >>> outgoing = make_testable_runner(OutgoingRunner, 'out')
- >>> outgoing.run()
-
- >>> from operator import itemgetter
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> print messages[0].as_string()
- From: aperson@example.org
- To: test@example.com
- Subject: An archived message
- Message-ID: <12345>
- X-Message-ID-Hash: ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Sender: test-bounces@example.com
- Errors-To: test-bounces@example.com
- X-Peer: 127.0.0.1:...
- X-MailFrom: test-bounces@example.com
- X-RcptTo: archive@mail-archive.dev
- <BLANKLINE>
- Here is an archived message.
- _______________________________________________
- Test mailing list
- test@example.com
- http://lists.example.com/listinfo/test@example.com
-
- >>> smtpd.clear()
-
-However, if the mailing list is not public, the message will never be archived
-at this service.
-
- >>> mlist.archive_private = True
- >>> print archiver.list_url(mlist)
- None
- >>> print archiver.permalink(mlist, msg)
- None
- >>> archiver.archive_message(mlist, msg)
- >>> list(smtpd.messages)
- []
-
-Additionally, this archiver can handle malformed Message-IDs.
-
- >>> mlist.archive_private = False
- >>> del msg['message-id']
- >>> msg['Message-ID'] = '12345>'
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8=
-
- >>> del msg['message-id']
- >>> msg['Message-ID'] = '<12345'
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY=
-
- >>> del msg['message-id']
- >>> msg['Message-ID'] = '12345'
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
-
- >>> del msg['message-id']
- >>> msg['Message-ID'] = ' 12345 '
- >>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
-
-
-MHonArc
--------
-
-The MHonArc archiver <http://www.mhonarc.org> is also available.
-
- >>> archiver = archivers['mhonarc']
- >>> print archiver.name
- mhonarc
-
-Messages sent to a local MHonArc instance are added to its archive via a
-subprocess call.
-
- >>> archiver.archive_message(mlist, msg)
- >>> archive_log = open(os.path.join(config.LOG_DIR, 'archiver'))
- >>> try:
- ... contents = archive_log.read()
- ... finally:
- ... archive_log.close()
- >>> print 'LOG:', contents
- LOG: ... /usr/bin/mhonarc -add
- -dbfile /.../private/test@example.com.mbox/mhonarc.db
- -outdir /.../mhonarc/test@example.com
- -stderr /.../logs/mhonarc
- -stdout /.../logs/mhonarc
- -spammode -umask 022
- ...
diff --git a/mailman/docs/bounces.txt b/mailman/docs/bounces.txt
deleted file mode 100644
index 9e8bcd23b..000000000
--- a/mailman/docs/bounces.txt
+++ /dev/null
@@ -1,107 +0,0 @@
-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 = config.db.list_manager.create(u'_xtest@example.com')
- >>> mlist.preferred_language = u'en'
-
-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.
-
- >>> switchboard = config.switchboards['virgin']
- >>> from mailman.app.bounces import bounce_message
- >>> bounce_message(mlist, msg)
- >>> len(switchboard.files)
- 1
- >>> filebase = switchboard.files[0]
- >>> qmsg, qmsgdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> print qmsg.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)
- >>> len(switchboard.files)
- 1
- >>> filebase = switchboard.files[0]
- >>> qmsg, qmsgdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> print qmsg.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/mailman/docs/chains.txt b/mailman/docs/chains.txt
deleted file mode 100644
index b6e75e6e1..000000000
--- a/mailman/docs/chains.txt
+++ /dev/null
@@ -1,345 +0,0 @@
-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.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'_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'
- >>> data = config.db.pendings.confirm(cookie)
- >>> sorted(data.items())
- [(u'id', ...), (u'type', u'held message')]
-
-The message itself is held in the message store.
-
- >>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request(
- ... data['id'])
- >>> msg = config.db.message_store.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/mailman/docs/domains.txt b/mailman/docs/domains.txt
deleted file mode 100644
index b71689520..000000000
--- a/mailman/docs/domains.txt
+++ /dev/null
@@ -1,46 +0,0 @@
-Domains
-=======
-
-Domains are how Mailman interacts with email host names and web host names.
-Generally, new domains are registered in the mailman.cfg configuration file.
-We simulate that here by pushing new configurations.
-
- >>> config.push('example.org', """
- ... [domain.example_dot_org]
- ... email_host: example.org
- ... base_url: https://mail.example.org
- ... description: The example domain
- ... contact_address: postmaster@mail.example.org
- ... """)
-
- >>> domain = config.domains['example.org']
- >>> print domain.email_host
- example.org
- >>> print domain.base_url
- https://mail.example.org
- >>> print domain.description
- The example domain
- >>> print domain.contact_address
- postmaster@mail.example.org
- >>> print domain.url_host
- mail.example.org
-
-
-Confirmation tokens
--------------------
-
-Confirmation tokens can be added to either the email confirmation address...
-
- >>> print domain.confirm_address('xyz')
- confirm-xyz@example.org
-
-...or the confirmation url.
-
- >>> print domain.confirm_url('abc')
- https://mail.example.org/confirm/abc
-
-
-Clean up
---------
-
- >>> config.pop('example.org')
diff --git a/mailman/docs/languages.txt b/mailman/docs/languages.txt
deleted file mode 100644
index 775b933e8..000000000
--- a/mailman/docs/languages.txt
+++ /dev/null
@@ -1,104 +0,0 @@
-Languages
-=========
-
-Mailman is multilingual. A language manager handles the known set of
-languages at run time, as well as enabling those languages for use in a
-running Mailman instance.
-
- >>> from zope.interface.verify import verifyObject
- >>> from mailman.interfaces.languages import ILanguageManager
- >>> from mailman.languages import LanguageManager
- >>> mgr = LanguageManager()
- >>> verifyObject(ILanguageManager, mgr)
- True
-
-A language manager keeps track of the languages it knows about as well as the
-languages which are enabled. By default, none are known or enabled.
-
- >>> sorted(mgr.known_codes)
- []
- >>> sorted(mgr.enabled_codes)
- []
-
-The language manager also keeps track of information for each known language,
-but you obviously can't get information for an unknown language.
-
- >>> mgr.get_description('en')
- Traceback (most recent call last):
- ...
- KeyError: 'en'
- >>> mgr.get_charset('en')
- Traceback (most recent call last):
- ...
- KeyError: 'en'
-
-
-Adding languages
-----------------
-
-Adding a new language requires three pieces of information, the 2-character
-language code, the English description of the language, and the character set
-used by the language.
-
- >>> mgr.add_language('en', 'English', 'us-ascii')
- >>> mgr.add_language('it', 'Italian', 'iso-8859-1')
-
-By default, added languages are also enabled.
-
- >>> sorted(mgr.known_codes)
- ['en', 'it']
- >>> sorted(mgr.enabled_codes)
- ['en', 'it']
-
-And you can get information for all known languages.
-
- >>> mgr.get_description('en')
- 'English'
- >>> mgr.get_charset('en')
- 'us-ascii'
- >>> mgr.get_description('it')
- 'Italian'
- >>> mgr.get_charset('it')
- 'iso-8859-1'
-
-You can also add a language without enabling it.
-
- >>> mgr.add_language('pl', 'Polish', 'iso-8859-2', enable=False)
- >>> sorted(mgr.known_codes)
- ['en', 'it', 'pl']
- >>> sorted(mgr.enabled_codes)
- ['en', 'it']
-
-You can get language data for disabled languages.
-
- >>> mgr.get_description('pl')
- 'Polish'
- >>> mgr.get_charset('pl')
- 'iso-8859-2'
-
-And of course you can enable a known language.
-
- >>> mgr.enable_language('pl')
- >>> sorted(mgr.enabled_codes)
- ['en', 'it', 'pl']
-
-But you cannot enable languages that the manager does not know about.
-
- >>> mgr.enable_language('xx')
- Traceback (most recent call last):
- ...
- KeyError: 'xx'
-
-
-Other iterations
-----------------
-
-You can iterate over the descriptions (names) of all enabled languages.
-
- >>> sorted(mgr.enabled_names)
- ['English', 'Italian', 'Polish']
-
-You can ask whether a particular language code is enabled.
-
- >>> 'it' in mgr.enabled_codes
- True
diff --git a/mailman/docs/lifecycle.txt b/mailman/docs/lifecycle.txt
deleted file mode 100644
index c6c0c0671..000000000
--- a/mailman/docs/lifecycle.txt
+++ /dev/null
@@ -1,136 +0,0 @@
-Application level list lifecycle
---------------------------------
-
-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):
- ...
- InvalidEmailAddress: '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 = u'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(u'test_1@example.com')
- >>> mlist_1.fqdn_listname
- u'test_1@example.com'
- >>> mlist_1.msg_footer
- u'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 = [u'aperson@example.com', u'bperson@example.com',
- ... u'cperson@example.com', u'dperson@example.com']
- >>> mlist_2 = create_list(u'test_2@example.com', owners)
- >>> mlist_2.fqdn_listname
- u'test_2@example.com'
- >>> mlist_2.msg_footer
- u'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.
-
- >>> usermgr = config.db.user_manager
- >>> user_a = usermgr.get_user(u'aperson@example.com')
- >>> user_b = usermgr.get_user(u'bperson@example.com')
- >>> user_c = usermgr.get_user(u'cperson@example.com')
- >>> user_d = usermgr.get_user(u'dperson@example.com')
- >>> user_a.real_name = u'Anne Person'
- >>> user_b.real_name = u'Bart Person'
- >>> user_c.real_name = u'Caty Person'
- >>> user_d.real_name = u'Dirk Person'
-
- >>> mlist_3 = create_list(u'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)
- >>> print config.db.list_manager.get('test_2@example.com')
- None
-
-We should now be able to completely recreate the mailing list.
-
- >>> mlist_2a = create_list(u'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/mailman/docs/listmanager.txt b/mailman/docs/listmanager.txt
deleted file mode 100644
index 830f6d962..000000000
--- a/mailman/docs/listmanager.txt
+++ /dev/null
@@ -1,88 +0,0 @@
-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.interfaces.listmanager import IListManager
- >>> listmgr = config.db.list_manager
- >>> IListManager.providedBy(listmgr)
- True
-
-
-Creating a mailing list
------------------------
-
-Creating the list returns the newly created IMailList object.
-
- >>> from mailman.interfaces.mailinglist import IMailingList
- >>> mlist = listmgr.create(u'_xtest@example.com')
- >>> IMailingList.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
- u'_xtest'
- >>> mlist.host_name
- u'example.com'
- >>> mlist.fqdn_listname
- u'_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 = listmgr.create(u'_xtest@example.com')
- Traceback (most recent call last):
- ...
- ListAlreadyExistsError: _xtest@example.com
-
-
-Deleting a mailing list
------------------------
-
-Use the list manager to delete a mailing list.
-
- >>> listmgr.delete(mlist)
- >>> sorted(listmgr.names)
- []
-
-After deleting the list, you can create it again.
-
- >>> mlist = listmgr.create(u'_xtest@example.com')
- >>> mlist.fqdn_listname
- u'_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 = listmgr.get(u'_xtest@example.com')
- >>> mlist_2 is mlist
- True
-
-If you try to get a list that doesn't existing yet, you get None.
-
- >>> print listmgr.get(u'_xtest_2@example.com')
- None
-
-
-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 = listmgr.create(u'_xtest_3@example.com')
- >>> mlist_4 = listmgr.create(u'_xtest_4@example.com')
- >>> sorted(listmgr.names)
- [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com']
- >>> sorted(m.fqdn_listname for m in listmgr.mailing_lists)
- [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com']
diff --git a/mailman/docs/membership.txt b/mailman/docs/membership.txt
deleted file mode 100644
index 7f9f16738..000000000
--- a/mailman/docs/membership.txt
+++ /dev/null
@@ -1,230 +0,0 @@
-List memberships
-================
-
-Users represent people in Mailman. Users control email addresses, and rosters
-are collectons of members. A member gives an email address a role, such as
-'member', 'administrator', or 'moderator'. Roster sets are collections of
-rosters and a mailing list has a single roster set that contains all its
-members, regardless of that member's role.
-
-Mailing lists and roster sets have an indirect relationship, through the
-roster set's name. Roster also have names, but are related to roster sets
-by a more direct containment relationship. This is because it is possible to
-store mailing list data in a different database than user data.
-
-When we create a mailing list, it starts out with no members...
-
- >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
- >>> mlist
- <mailing list "_xtest@example.com" at ...>
- >>> sorted(member.address.address for member in mlist.members.members)
- []
- >>> sorted(user.real_name for user in mlist.members.users)
- []
- >>> sorted(address.address for member in mlist.members.addresses)
- []
-
-...no owners...
-
- >>> sorted(member.address.address for member in mlist.owners.members)
- []
- >>> sorted(user.real_name for user in mlist.owners.users)
- []
- >>> sorted(address.address for member in mlist.owners.addresses)
- []
-
-...no moderators...
-
- >>> sorted(member.address.address for member in mlist.moderators.members)
- []
- >>> sorted(user.real_name for user in mlist.moderators.users)
- []
- >>> sorted(address.address for member in mlist.moderators.addresses)
- []
-
-...and no administrators.
-
- >>> sorted(member.address.address
- ... for member in mlist.administrators.members)
- []
- >>> sorted(user.real_name for user in mlist.administrators.users)
- []
- >>> sorted(address.address for member in mlist.administrators.addresses)
- []
-
-
-
-Administrators
---------------
-
-A mailing list's administrators are defined as union of the list's owners and
-the list's moderators. We can add new owners or moderators to this list by
-assigning roles to users. First we have to create the user, because there are
-no users in the user database yet.
-
- >>> usermgr = config.db.user_manager
- >>> user_1 = usermgr.create_user(u'aperson@example.com', u'Anne Person')
- >>> user_1.real_name
- u'Anne Person'
- >>> sorted(address.address for address in user_1.addresses)
- [u'aperson@example.com']
-
-We can add Anne as an owner of the mailing list, by creating a member role for
-her.
-
- >>> from mailman.interfaces.member import MemberRole
- >>> address_1 = list(user_1.addresses)[0]
- >>> address_1.address
- u'aperson@example.com'
- >>> address_1.subscribe(mlist, MemberRole.owner)
- <Member: Anne Person <aperson@example.com> on
- _xtest@example.com as MemberRole.owner>
- >>> sorted(member.address.address for member in mlist.owners.members)
- [u'aperson@example.com']
- >>> sorted(user.real_name for user in mlist.owners.users)
- [u'Anne Person']
- >>> sorted(address.address for address in mlist.owners.addresses)
- [u'aperson@example.com']
-
-Adding Anne as a list owner also makes her an administrator, but does not make
-her a moderator. Nor does it make her a member of the list.
-
- >>> sorted(user.real_name for user in mlist.administrators.users)
- [u'Anne Person']
- >>> sorted(user.real_name for user in mlist.moderators.users)
- []
- >>> sorted(user.real_name for user in mlist.members.users)
- []
-
-We can add Ben as a moderator of the list, by creating a different member role
-for him.
-
- >>> user_2 = usermgr.create_user(u'bperson@example.com', u'Ben Person')
- >>> user_2.real_name
- u'Ben Person'
- >>> address_2 = list(user_2.addresses)[0]
- >>> address_2.address
- u'bperson@example.com'
- >>> address_2.subscribe(mlist, MemberRole.moderator)
- <Member: Ben Person <bperson@example.com>
- on _xtest@example.com as MemberRole.moderator>
- >>> sorted(member.address.address for member in mlist.moderators.members)
- [u'bperson@example.com']
- >>> sorted(user.real_name for user in mlist.moderators.users)
- [u'Ben Person']
- >>> sorted(address.address for address in mlist.moderators.addresses)
- [u'bperson@example.com']
-
-Now, both Anne and Ben are list administrators.
-
- >>> sorted(member.address.address
- ... for member in mlist.administrators.members)
- [u'aperson@example.com', u'bperson@example.com']
- >>> sorted(user.real_name for user in mlist.administrators.users)
- [u'Anne Person', u'Ben Person']
- >>> sorted(address.address for address in mlist.administrators.addresses)
- [u'aperson@example.com', u'bperson@example.com']
-
-
-Members
--------
-
-Similarly, list members are born of users being given the proper role. It's
-more interesting here because these roles should have a preference which can
-be used to decide whether the member is to get regular delivery or digest
-delivery. Without a preference, Mailman will fall back first to the address's
-preference, then the user's preference, then the list's preference. Start
-without any member preference to see the system defaults.
-
- >>> user_3 = usermgr.create_user(u'cperson@example.com', u'Claire Person')
- >>> user_3.real_name
- u'Claire Person'
- >>> address_3 = list(user_3.addresses)[0]
- >>> address_3.address
- u'cperson@example.com'
- >>> address_3.subscribe(mlist, MemberRole.member)
- <Member: Claire Person <cperson@example.com>
- on _xtest@example.com as MemberRole.member>
-
-Claire will be a regular delivery member but not a digest member.
-
- >>> sorted(address.address for address in mlist.members.addresses)
- [u'cperson@example.com']
- >>> sorted(address.address for address in mlist.regular_members.addresses)
- [u'cperson@example.com']
- >>> sorted(address.address for address in mlist.digest_members.addresses)
- []
-
-It's easy to make the list administrators members of the mailing list too.
-
- >>> members = []
- >>> for address in mlist.administrators.addresses:
- ... member = address.subscribe(mlist, MemberRole.member)
- ... members.append(member)
- >>> sorted(members, key=lambda m: m.address.address)
- [<Member: Anne Person <aperson@example.com> on
- _xtest@example.com as MemberRole.member>,
- <Member: Ben Person <bperson@example.com> on
- _xtest@example.com as MemberRole.member>]
- >>> sorted(address.address for address in mlist.members.addresses)
- [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com']
- >>> sorted(address.address for address in mlist.regular_members.addresses)
- [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com']
- >>> sorted(address.address for address in mlist.digest_members.addresses)
- []
-
-
-Finding members
----------------
-
-You can find the IMember object that is a member of a roster for a given text
-email address by using an IRoster's .get_member() method.
-
- >>> mlist.owners.get_member(u'aperson@example.com')
- <Member: Anne Person <aperson@example.com> on
- _xtest@example.com as MemberRole.owner>
- >>> mlist.administrators.get_member(u'aperson@example.com')
- <Member: Anne Person <aperson@example.com> on
- _xtest@example.com as MemberRole.owner>
- >>> mlist.members.get_member(u'aperson@example.com')
- <Member: Anne Person <aperson@example.com> on
- _xtest@example.com as MemberRole.member>
-
-However, if the address is not subscribed with the appropriate role, then None
-is returned.
-
- >>> print mlist.administrators.get_member(u'zperson@example.com')
- None
- >>> print mlist.moderators.get_member(u'aperson@example.com')
- None
- >>> print mlist.members.get_member(u'zperson@example.com')
- None
-
-
-All subscribers
----------------
-
-There is also a roster containing all the subscribers of a mailing list,
-regardless of their role.
-
- >>> def sortkey(member):
- ... return (member.address.address, int(member.role))
- >>> [(member.address.address, str(member.role))
- ... for member in sorted(mlist.subscribers.members, key=sortkey)]
- [(u'aperson@example.com', 'MemberRole.member'),
- (u'aperson@example.com', 'MemberRole.owner'),
- (u'bperson@example.com', 'MemberRole.member'),
- (u'bperson@example.com', 'MemberRole.moderator'),
- (u'cperson@example.com', 'MemberRole.member')]
-
-
-Double subscriptions
---------------------
-
-It is an error to subscribe someone to a list with the same role twice.
-
- >>> address_1.subscribe(mlist, MemberRole.owner)
- Traceback (most recent call last):
- ...
- AlreadySubscribedError: aperson@example.com is already a MemberRole.owner
- of mailing list _xtest@example.com
diff --git a/mailman/docs/message.txt b/mailman/docs/message.txt
deleted file mode 100644
index dab9ddf0e..000000000
--- a/mailman/docs/message.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-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 = config.db.list_manager.create(u'_xtest@example.com')
- >>> mlist.preferred_language = u'en'
-
-The UserNotification constructor takes the recipient address, the sender
-address, an optional subject, optional body text, and optional language.
-
- >>> from mailman.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/mailman/docs/messagestore.txt b/mailman/docs/messagestore.txt
deleted file mode 100644
index 6e04568c5..000000000
--- a/mailman/docs/messagestore.txt
+++ /dev/null
@@ -1,113 +0,0 @@
-The message store
-=================
-
-The message store is a collection of messages keyed off of Message-ID and
-X-Message-ID-Hash headers. Either of these values can be combined with the
-message's List-Archive header to create a globally unique URI to the message
-object in the internet facing interface of the message store. The
-X-Message-ID-Hash is the Base32 SHA1 hash of the Message-ID.
-
- >>> store = config.db.message_store
-
-If you try to add a message to the store which is missing the Message-ID
-header, you will get an exception.
-
- >>> msg = message_from_string("""\
- ... Subject: An important message
- ...
- ... This message is very important.
- ... """)
- >>> store.add(msg)
- Traceback (most recent call last):
- ...
- ValueError: Exactly one Message-ID header required
-
-However, if the message has a Message-ID header, it can be stored.
-
- >>> msg['Message-ID'] = '<87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>'
- >>> store.add(msg)
- 'AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35'
- >>> print msg.as_string()
- Subject: An important message
- Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
- X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
- <BLANKLINE>
- This message is very important.
- <BLANKLINE>
-
-
-Finding messages
-----------------
-
-There are several ways to find a message given either the Message-ID or
-X-Message-ID-Hash headers. In either case, if no matching message is found,
-None is returned.
-
- >>> print store.get_message_by_id(u'nothing')
- None
- >>> print store.get_message_by_hash(u'nothing')
- None
-
-Given an existing Message-ID, the message can be found.
-
- >>> message = store.get_message_by_id(msg['message-id'])
- >>> print message.as_string()
- Subject: An important message
- Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
- X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
- <BLANKLINE>
- This message is very important.
- <BLANKLINE>
-
-Similarly, we can find messages by the X-Message-ID-Hash:
-
- >>> message = store.get_message_by_hash(msg['x-message-id-hash'])
- >>> print message.as_string()
- Subject: An important message
- Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
- X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
- <BLANKLINE>
- This message is very important.
- <BLANKLINE>
-
-
-Iterating over all messages
----------------------------
-
-The message store provides a means to iterate over all the messages it
-contains.
-
- >>> messages = list(store.messages)
- >>> len(messages)
- 1
- >>> print messages[0].as_string()
- Subject: An important message
- Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
- X-Message-ID-Hash: AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
- <BLANKLINE>
- This message is very important.
- <BLANKLINE>
-
-
-Deleting messages from the store
---------------------------------
-
-You delete a message from the storage service by providing the Message-ID for
-the message you want to delete. If you try to delete a Message-ID that isn't
-in the store, you get an exception.
-
- >>> store.delete_message(u'nothing')
- Traceback (most recent call last):
- ...
- LookupError: nothing
-
-But if you delete an existing message, it really gets deleted.
-
- >>> message_id = message['message-id']
- >>> store.delete_message(message_id)
- >>> list(store.messages)
- []
- >>> print store.get_message_by_id(message_id)
- None
- >>> print store.get_message_by_hash(message['x-message-id-hash'])
- None
diff --git a/mailman/docs/mlist-addresses.txt b/mailman/docs/mlist-addresses.txt
deleted file mode 100644
index 75ec3df37..000000000
--- a/mailman/docs/mlist-addresses.txt
+++ /dev/null
@@ -1,76 +0,0 @@
-Mailing list addresses
-======================
-
-Every mailing list has a number of addresses which are publicly available.
-These are defined in the IMailingListAddresses interface.
-
- >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
-
-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
- u'_xtest@example.com'
- >>> mlist.posting_address
- u'_xtest@example.com'
-
-Messages to the mailing list's 'no reply' address always get discarded without
-prejudice.
-
- >>> mlist.no_reply_address
- u'noreply@example.com'
-
-The mailing list's owner address reaches the human moderators.
-
- >>> mlist.owner_address
- u'_xtest-owner@example.com'
-
-The request address goes to the list's email command robot.
-
- >>> mlist.request_address
- u'_xtest-request@example.com'
-
-The bounces address accepts and processes all potential bounces.
-
- >>> mlist.bounces_address
- u'_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
- u'_xtest-join@example.com'
- >>> mlist.subscribe_address
- u'_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
- u'_xtest-leave@example.com'
- >>> mlist.unsubscribe_address
- u'_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')
- u'_xtest-confirm+cookie@example.com'
- >>> mlist.confirm_address('wookie')
- u'_xtest-confirm+wookie@example.com'
-
- >>> config.push('test config', """
- ... [mta]
- ... verp_confirm_format: $address---$cookie
- ... """)
- >>> mlist.confirm_address('cookie')
- u'_xtest-confirm---cookie@example.com'
- >>> config.pop('test config')
diff --git a/mailman/docs/pending.txt b/mailman/docs/pending.txt
deleted file mode 100644
index abfba4885..000000000
--- a/mailman/docs/pending.txt
+++ /dev/null
@@ -1,94 +0,0 @@
-The pending database
-====================
-
-The pending database is where various types of events which need confirmation
-are stored. These can include email address registration events, held
-messages (but only for user confirmation), auto-approvals, and probe bounces.
-This is not where messages held for administrator approval are kept.
-
- >>> from zope.interface import implements
- >>> from zope.interface.verify import verifyObject
-
-In order to pend an event, you first need a pending database, which is
-available by adapting the list manager.
-
- >>> from mailman.interfaces.pending import IPendings
- >>> pendingdb = config.db.pendings
- >>> verifyObject(IPendings, pendingdb)
- True
-
-The pending database can add any IPendable to the database, returning a token
-that can be used in urls and such.
-
- >>> from mailman.interfaces.pending import IPendable
- >>> class SimplePendable(dict):
- ... implements(IPendable)
- >>> subscription = SimplePendable(
- ... type='subscription',
- ... address='aperson@example.com',
- ... realname='Anne Person',
- ... language='en',
- ... password='xyz')
- >>> token = pendingdb.add(subscription)
- >>> len(token)
- 40
-
-There's not much you can do with tokens except to 'confirm' them, which
-basically means returning the IPendable structure (as a dict) from the
-database that matches the token. If the token isn't in the database, None is
-returned.
-
- >>> pendable = pendingdb.confirm('missing')
- >>> print pendable
- None
- >>> pendable = pendingdb.confirm(token)
- >>> sorted(pendable.items())
- [(u'address', u'aperson@example.com'),
- (u'language', u'en'),
- (u'password', u'xyz'),
- (u'realname', u'Anne Person'),
- (u'type', u'subscription')]
-
-After confirmation, the token is no longer in the database.
-
- >>> pendable = pendingdb.confirm(token)
- >>> print pendable
- None
-
-There are a few other things you can do with the pending database. When you
-confirm a token, you can leave it in the database, or in otherwords, not
-expunge it.
-
- >>> event_1 = SimplePendable(type='one')
- >>> token_1 = pendingdb.add(event_1)
- >>> event_2 = SimplePendable(type='two')
- >>> token_2 = pendingdb.add(event_2)
- >>> event_3 = SimplePendable(type='three')
- >>> token_3 = pendingdb.add(event_3)
- >>> pendable = pendingdb.confirm(token_1, expunge=False)
- >>> pendable.items()
- [(u'type', u'one')]
- >>> pendable = pendingdb.confirm(token_1, expunge=True)
- >>> pendable.items()
- [(u'type', u'one')]
- >>> pendable = pendingdb.confirm(token_1)
- >>> print pendable
- None
-
-An event can be given a lifetime when it is pended, otherwise it just uses a
-default lifetime.
-
- >>> from datetime import timedelta
- >>> yesterday = timedelta(days=-1)
- >>> event_4 = SimplePendable(type='four')
- >>> token_4 = pendingdb.add(event_4, lifetime=yesterday)
-
-Every once in a while the pending database is cleared of old records.
-
- >>> pendingdb.evict()
- >>> pendable = pendingdb.confirm(token_4)
- >>> print pendable
- None
- >>> pendable = pendingdb.confirm(token_2)
- >>> pendable.items()
- [(u'type', u'two')]
diff --git a/mailman/docs/pipelines.txt b/mailman/docs/pipelines.txt
deleted file mode 100644
index 0e6dad8e8..000000000
--- a/mailman/docs/pipelines.txt
+++ /dev/null
@@ -1,186 +0,0 @@
-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.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'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
- recips : 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
- recips : 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
- recips : 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>
-
- >>> digest.clear()
diff --git a/mailman/docs/registration.txt b/mailman/docs/registration.txt
deleted file mode 100644
index d243188bc..000000000
--- a/mailman/docs/registration.txt
+++ /dev/null
@@ -1,362 +0,0 @@
-Address registration
-====================
-
-When a user wants to join a mailing list -- any mailing list -- in the running
-instance, he or she must first register with Mailman. The only thing they
-must supply is an email address, although there is additional information they
-may supply. All registered email addresses must be verified before Mailman
-will send them any list traffic.
-
- >>> from mailman.app.registrar import Registrar
- >>> from mailman.interfaces.registrar import IRegistrar
-
-The IUserManager manages users, but it does so at a fairly low level.
-Specifically, it does not handle verifications, email address syntax validity
-checks, etc. The IRegistrar is the interface to the object handling all this
-stuff.
-
-Add a domain, which will provide the context for the verification email
-message.
-
- >>> config.push('mail', """
- ... [domain.mail_example_dot_com]
- ... email_host: mail.example.com
- ... base_url: http://mail.example.com
- ... contact_address: postmaster@mail.example.com
- ... """)
-
- >>> domain = config.domains['mail.example.com']
-
-Get a registrar by adapting a context to the interface.
-
- >>> from zope.interface.verify import verifyObject
- >>> registrar = IRegistrar(domain)
- >>> verifyObject(IRegistrar, registrar)
- True
-
-Here is a helper function to check the token strings.
-
- >>> def check_token(token):
- ... assert isinstance(token, basestring), 'Not a string'
- ... assert len(token) == 40, 'Unexpected length: %d' % len(token)
- ... assert token.isalnum(), 'Not alphanumeric'
- ... print 'ok'
-
-Here is a helper function to extract tokens from confirmation messages.
-
- >>> import re
- >>> cre = re.compile('http://mail.example.com/confirm/(.*)')
- >>> def extract_token(msg):
- ... mo = cre.search(qmsg.get_payload())
- ... return mo.group(1)
-
-
-Invalid email addresses
------------------------
-
-The only piece of information you need to register is the email address.
-Some amount of sanity checks are performed on the email address, although
-honestly, not as much as probably should be done. Still, some patently bad
-addresses are rejected outright.
-
- >>> registrar.register('')
- Traceback (most recent call last):
- ...
- InvalidEmailAddress: ''
- >>> registrar.register('some name@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddress: 'some name@example.com'
- >>> registrar.register('<script>@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddress: '<script>@example.com'
- >>> registrar.register('\xa0@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddress: '\xa0@example.com'
- >>> registrar.register('noatsign')
- Traceback (most recent call last):
- ...
- InvalidEmailAddress: 'noatsign'
- >>> registrar.register('nodom@ain')
- Traceback (most recent call last):
- ...
- InvalidEmailAddress: 'nodom@ain'
-
-
-Register an email address
--------------------------
-
-Registration of an unknown address creates nothing until the confirmation step
-is complete. No IUser or IAddress is created at registration time, but a
-record is added to the pending database, and the token for that record is
-returned.
-
- >>> token = registrar.register(u'aperson@example.com', u'Anne Person')
- >>> check_token(token)
- ok
-
-There should be no records in the user manager for this address yet.
-
- >>> usermgr = config.db.user_manager
- >>> print usermgr.get_user(u'aperson@example.com')
- None
- >>> print usermgr.get_address(u'aperson@example.com')
- None
-
-But this address is waiting for confirmation.
-
- >>> pendingdb = config.db.pendings
- >>> sorted(pendingdb.confirm(token, expunge=False).items())
- [(u'address', u'aperson@example.com'),
- (u'real_name', u'Anne Person'),
- (u'type', u'registration')]
-
-
-Verification by email
----------------------
-
-There is also a verification email sitting in the virgin queue now. This
-message is sent to the user in order to verify the registered address.
-
- >>> switchboard = config.switchboards['virgin']
- >>> len(switchboard.files)
- 1
- >>> filebase = switchboard.files[0]
- >>> qmsg, qdata = 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: confirm ...
- From: confirm-...@mail.example.com
- To: aperson@example.com
- Message-ID: <...>
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Email Address Registration Confirmation
- <BLANKLINE>
- Hello, this is the GNU Mailman server at mail.example.com.
- <BLANKLINE>
- We have received a registration request for the email address
- <BLANKLINE>
- aperson@example.com
- <BLANKLINE>
- Before you can start using GNU Mailman at this site, you must first
- confirm that this is your email address. You can do this by replying to
- this message, keeping the Subject header intact. Or you can visit this
- web page
- <BLANKLINE>
- http://mail.example.com/confirm/...
- <BLANKLINE>
- If you do not wish to register this email address simply disregard this
- message. If you think you are being maliciously subscribed to the list,
- or have any other questions, you may contact
- <BLANKLINE>
- postmaster@mail.example.com
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- nodecorate : True
- recips : [u'aperson@example.com']
- reduced_list_headers: True
- version : 3
-
-The confirmation token shows up in several places, each of which provides an
-easy way for the user to complete the confirmation. The token will always
-appear in a URL in the body of the message.
-
- >>> sent_token = extract_token(qmsg)
- >>> sent_token == token
- True
-
-The same token will appear in the From header.
-
- >>> qmsg['from'] == 'confirm-' + token + '@mail.example.com'
- True
-
-It will also appear in the Subject header.
-
- >>> qmsg['subject'] == 'confirm ' + token
- True
-
-The user would then validate their just registered address by clicking on a
-url or responding to the message. Either way, the confirmation process
-extracts the token and uses that to confirm the pending registration.
-
- >>> registrar.confirm(token)
- True
-
-Now, there is an IAddress in the database matching the address, as well as an
-IUser linked to this address. The IAddress is verified.
-
- >>> found_address = usermgr.get_address(u'aperson@example.com')
- >>> found_address
- <Address: Anne Person <aperson@example.com> [verified] at ...>
- >>> found_user = usermgr.get_user(u'aperson@example.com')
- >>> found_user
- <User "Anne Person" at ...>
- >>> found_user.controls(found_address.address)
- True
- >>> from datetime import datetime
- >>> isinstance(found_address.verified_on, datetime)
- True
-
-
-Non-standard registrations
---------------------------
-
-If you try to confirm a registration token twice, of course only the first one
-will work. The second one is ignored.
-
- >>> token = registrar.register(u'bperson@example.com')
- >>> check_token(token)
- ok
- >>> filebase = switchboard.files[0]
- >>> qmsg, qdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> sent_token = extract_token(qmsg)
- >>> token == sent_token
- True
- >>> registrar.confirm(token)
- True
- >>> registrar.confirm(token)
- False
-
-If an address is in the system, but that address is not linked to a user yet
-and the address is not yet validated, then no user is created until the
-confirmation step is completed.
-
- >>> usermgr.create_address(u'cperson@example.com')
- <Address: cperson@example.com [not verified] at ...>
- >>> token = registrar.register(u'cperson@example.com', u'Claire Person')
- >>> print usermgr.get_user(u'cperson@example.com')
- None
- >>> filebase = switchboard.files[0]
- >>> qmsg, qdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> registrar.confirm(token)
- True
- >>> usermgr.get_user(u'cperson@example.com')
- <User "Claire Person" at ...>
- >>> usermgr.get_address(u'cperson@example.com')
- <Address: cperson@example.com [verified] at ...>
-
-Even if the address being registered has already been verified, the
-registration sends a confirmation.
-
- >>> token = registrar.register(u'cperson@example.com')
- >>> token is not None
- True
-
-
-Discarding
-----------
-
-A confirmation token can also be discarded, say if the user changes his or her
-mind about registering. When discarded, no IAddress or IUser is created.
-
- >>> token = registrar.register(u'eperson@example.com', u'Elly Person')
- >>> check_token(token)
- ok
- >>> registrar.discard(token)
- >>> print pendingdb.confirm(token)
- None
- >>> print usermgr.get_address(u'eperson@example.com')
- None
- >>> print usermgr.get_user(u'eperson@example.com')
- None
-
-
-Registering a new address for an existing user
-----------------------------------------------
-
-When a new address for an existing user is registered, there isn't too much
-different except that the new address will still need to be verified before it
-can be used.
-
- >>> dperson = usermgr.create_user(u'dperson@example.com', u'Dave Person')
- >>> dperson
- <User "Dave Person" at ...>
- >>> address = usermgr.get_address(u'dperson@example.com')
- >>> address.verified_on = datetime.now()
-
- >>> from operator import attrgetter
- >>> sorted((addr for addr in dperson.addresses), key=attrgetter('address'))
- [<Address: Dave Person <dperson@example.com> [verified] at ...>]
- >>> dperson.register(u'david.person@example.com', u'David Person')
- <Address: David Person <david.person@example.com> [not verified] at ...>
- >>> token = registrar.register(u'david.person@example.com')
- >>> filebase = switchboard.files[0]
- >>> qmsg, qdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> registrar.confirm(token)
- True
- >>> user = usermgr.get_user(u'david.person@example.com')
- >>> user is dperson
- True
- >>> user
- <User "Dave Person" at ...>
- >>> sorted((addr for addr in user.addresses), key=attrgetter('address'))
- [<Address: David Person <david.person@example.com> [verified] at ...>,
- <Address: Dave Person <dperson@example.com> [verified] at ...>]
-
-
-Corner cases
-------------
-
-If you try to confirm a token that doesn't exist in the pending database, the
-confirm method will just return None.
-
- >>> registrar.confirm('no token')
- False
-
-Likewise, if you try to confirm, through the IUserRegistrar interface, a token
-that doesn't match a registration even, you will get None. However, the
-pending even matched with that token will still be removed.
-
- >>> from mailman.interfaces.pending import IPendable
- >>> from zope.interface import implements
-
- >>> class SimplePendable(dict):
- ... implements(IPendable)
- >>> pendable = SimplePendable(type='foo', bar='baz')
- >>> token = pendingdb.add(pendable)
- >>> registrar.confirm(token)
- False
- >>> print pendingdb.confirm(token)
- None
-
-
-Registration and subscription
------------------------------
-
-Fred registers with Mailman at the same time that he subscribes to a mailing
-list.
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist = create_list(u'alpha@example.com')
- >>> token = registrar.register(
- ... u'fred.person@example.com', 'Fred Person', mlist)
-
-Before confirmation, Fred is not a member of the mailing list.
-
- >>> print mlist.members.get_member(u'fred.person@example.com')
- None
-
-But after confirmation, he is.
-
- >>> registrar.confirm(token)
- True
- >>> print mlist.members.get_member(u'fred.person@example.com')
- <Member: Fred Person <fred.person@example.com>
- on alpha@example.com as MemberRole.member>
-
-
-Clean up
---------
-
- >>> config.pop('mail')
diff --git a/mailman/docs/requests.txt b/mailman/docs/requests.txt
deleted file mode 100644
index 87b835fb8..000000000
--- a/mailman/docs/requests.txt
+++ /dev/null
@@ -1,883 +0,0 @@
-Moderator requests
-==================
-
-Various actions will be held for moderator approval, such as subscriptions to
-closed lists, or postings by non-members. The requests database is the low
-level interface to these actions requiring approval.
-
-Here is a helper function for printing out held requests.
-
- >>> def show_holds(requests):
- ... for request in requests.held_requests:
- ... key, data = requests.get_request(request.id)
- ... print request.id, str(request.request_type), key
- ... if data is not None:
- ... for key in sorted(data):
- ... print ' {0}: {1}'.format(key, data[key])
-
-And another helper for displaying messages in the virgin queue.
-
- >>> virginq = config.switchboards['virgin']
- >>> def dequeue(whichq=None, expected_count=1):
- ... if whichq is None:
- ... whichq = virginq
- ... assert len(whichq.files) == expected_count, (
- ... 'Unexpected file count: %d' % len(whichq.files))
- ... filebase = whichq.files[0]
- ... qmsg, qdata = whichq.dequeue(filebase)
- ... whichq.finish(filebase)
- ... return qmsg, qdata
-
-
-Mailing list centric
---------------------
-
-A set of requests are always related to a particular mailing list, so given a
-mailing list you need to get its requests object.
-
- >>> from mailman.interfaces.requests import IListRequests, IRequests
- >>> from zope.interface.verify import verifyObject
- >>> verifyObject(IRequests, config.db.requests)
- True
- >>> mlist = config.db.list_manager.create(u'test@example.com')
- >>> requests = config.db.requests.get_list_requests(mlist)
- >>> verifyObject(IListRequests, requests)
- True
- >>> requests.mailing_list
- <mailing list "test@example.com" at ...>
-
-
-Holding requests
-----------------
-
-The list's requests database starts out empty.
-
- >>> requests.count
- 0
- >>> list(requests.held_requests)
- []
-
-At the lowest level, the requests database is very simple. Holding a request
-requires a request type (as an enum value), a key, and an optional dictionary
-of associated data. The request database assigns no semantics to the held
-data, except for the request type. Here we hold some simple bits of data.
-
- >>> from mailman.interfaces.requests import RequestType
- >>> id_1 = requests.hold_request(RequestType.held_message, u'hold_1')
- >>> id_2 = requests.hold_request(RequestType.subscription, u'hold_2')
- >>> id_3 = requests.hold_request(RequestType.unsubscription, u'hold_3')
- >>> id_4 = requests.hold_request(RequestType.held_message, u'hold_4')
- >>> id_1, id_2, id_3, id_4
- (1, 2, 3, 4)
-
-And of course, now we can see that there are four requests being held.
-
- >>> requests.count
- 4
- >>> requests.count_of(RequestType.held_message)
- 2
- >>> requests.count_of(RequestType.subscription)
- 1
- >>> requests.count_of(RequestType.unsubscription)
- 1
- >>> show_holds(requests)
- 1 RequestType.held_message hold_1
- 2 RequestType.subscription hold_2
- 3 RequestType.unsubscription hold_3
- 4 RequestType.held_message hold_4
-
-If we try to hold a request with a bogus type, we get an exception.
-
- >>> requests.hold_request(5, 'foo')
- Traceback (most recent call last):
- ...
- TypeError: 5
-
-We can hold requests with additional data.
-
- >>> data = dict(foo='yes', bar='no')
- >>> id_5 = requests.hold_request(RequestType.held_message, u'hold_5', data)
- >>> id_5
- 5
- >>> requests.count
- 5
- >>> show_holds(requests)
- 1 RequestType.held_message hold_1
- 2 RequestType.subscription hold_2
- 3 RequestType.unsubscription hold_3
- 4 RequestType.held_message hold_4
- 5 RequestType.held_message hold_5
- bar: no
- foo: yes
-
-
-Getting requests
-----------------
-
-We can ask the requests database for a specific request, by providing the id
-of the request data we want. This returns a 2-tuple of the key and data we
-originally held.
-
- >>> key, data = requests.get_request(2)
- >>> print key
- hold_2
-
-Because we did not store additional data with request 2, it comes back as None
-now.
-
- >>> print data
- None
-
-However, if we ask for a request that had data, we'd get it back now.
-
- >>> key, data = requests.get_request(5)
- >>> print key
- hold_5
- >>> dump_msgdata(data)
- bar: no
- foo: yes
-
-If we ask for a request that is not in the database, we get None back.
-
- >>> print requests.get_request(801)
- None
-
-
-Iterating over requests
------------------------
-
-To make it easier to find specific requests, the list requests can be iterated
-over by type.
-
- >>> requests.count_of(RequestType.held_message)
- 3
- >>> for request in requests.of_type(RequestType.held_message):
- ... assert request.request_type is RequestType.held_message
- ... key, data = requests.get_request(request.id)
- ... print request.id, key
- ... if data is not None:
- ... for key in sorted(data):
- ... print ' {0}: {1}'.format(key, data[key])
- 1 hold_1
- 4 hold_4
- 5 hold_5
- bar: no
- foo: yes
-
-
-Deleting requests
------------------
-
-Once a specific request has been handled, it will be deleted from the requests
-database.
-
- >>> requests.delete_request(2)
- >>> requests.count
- 4
- >>> show_holds(requests)
- 1 RequestType.held_message hold_1
- 3 RequestType.unsubscription hold_3
- 4 RequestType.held_message hold_4
- 5 RequestType.held_message hold_5
- bar: no
- foo: yes
- >>> print requests.get_request(2)
- None
-
-We get an exception if we ask to delete a request that isn't in the database.
-
- >>> requests.delete_request(801)
- Traceback (most recent call last):
- ...
- KeyError: 801
-
-For the next section, we first clean up all the current requests.
-
- >>> for request in requests.held_requests:
- ... requests.delete_request(request.id)
- >>> requests.count
- 0
-
-
-Application support
--------------------
-
-There are several higher level interfaces available in the mailman.app package
-which can be used to hold messages, subscription, and unsubscriptions. There
-are also interfaces for disposing of these requests in an application specific
-and consistent way.
-
- >>> from mailman.app import moderator
-
-
-Holding messages
-----------------
-
-For this section, we need a mailing list and at least one message.
-
- >>> mlist = config.db.list_manager.create(u'alist@example.com')
- >>> mlist.preferred_language = u'en'
- >>> mlist.real_name = u'A Test List'
- >>> msg = message_from_string("""\
- ... From: aperson@example.org
- ... To: alist@example.com
- ... Subject: Something important
- ...
- ... Here's something important about our mailing list.
- ... """)
-
-Holding a message means keeping a copy of it that a moderator must approve
-before the message is posted to the mailing list. To hold the message, you
-must supply the message, message metadata, and a text reason for the hold. In
-this case, we won't include any additional metadata.
-
- >>> id_1 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
- >>> requests.get_request(id_1) is not None
- True
-
-We can also hold a message with some additional metadata.
-
- # Delete the Message-ID from the previous hold so we don't try to store
- # collisions in the message storage.
- >>> del msg['message-id']
- >>> msgdata = dict(sender='aperson@example.com',
- ... approved=True,
- ... received_time=123.45)
- >>> id_2 = moderator.hold_message(mlist, msg, msgdata, u'Feeling ornery')
- >>> requests.get_request(id_2) is not None
- True
-
-Once held, the moderator can select one of several dispositions. The most
-trivial is to simply defer a decision for now.
-
- >>> from mailman.interfaces import Action
- >>> moderator.handle_message(mlist, id_1, Action.defer)
- >>> requests.get_request(id_1) is not None
- True
-
-The moderator can also discard the message. This is often done with spam.
-Bye bye message!
-
- >>> moderator.handle_message(mlist, id_1, Action.discard)
- >>> print requests.get_request(id_1)
- None
- >>> virginq.files
- []
-
-The message can be rejected, meaning it is bounced back to the sender.
-
- >>> moderator.handle_message(mlist, id_2, Action.reject, 'Off topic')
- >>> print requests.get_request(id_2)
- None
- >>> qmsg, qdata = dequeue()
- >>> print qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: Request to mailing list "A Test List" rejected
- From: alist-bounces@example.com
- To: aperson@example.org
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Your request to the alist@example.com mailing list
- <BLANKLINE>
- Posting of your message titled "Something important"
- <BLANKLINE>
- has been rejected by the list moderator. The moderator gave the
- following reason for rejecting your request:
- <BLANKLINE>
- "Off topic"
- <BLANKLINE>
- Any questions or comments should be directed to the list administrator
- at:
- <BLANKLINE>
- alist-owner@example.com
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'aperson@example.org']
- reduced_list_headers: True
- version : 3
-
-Or the message can be approved. This actually places the message back into
-the incoming queue for further processing, however the message metadata
-indicates that the message has been approved.
-
- >>> id_3 = moderator.hold_message(mlist, msg, msgdata, 'Needs approval')
- >>> moderator.handle_message(mlist, id_3, Action.accept)
- >>> inq = config.switchboards['in']
- >>> qmsg, qdata = dequeue(inq)
- >>> print qmsg.as_string()
- From: aperson@example.org
- To: alist@example.com
- Subject: Something important
- Message-ID: ...
- X-Message-ID-Hash: ...
- X-Mailman-Approved-At: ...
- <BLANKLINE>
- Here's something important about our mailing list.
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- approved : True
- moderator_approved: True
- sender : aperson@example.com
- version : 3
-
-In addition to any of the above dispositions, the message can also be
-preserved for further study. Ordinarily the message is removed from the
-global message store after its disposition (though approved messages may be
-re-added to the message store). When handling a message, we can tell the
-moderator interface to also preserve a copy, essentially telling it not to
-delete the message from the storage. First, without the switch, the message
-is deleted.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.org
- ... To: alist@example.com
- ... Subject: Something important
- ... Message-ID: <12345>
- ...
- ... Here's something important about our mailing list.
- ... """)
- >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
- >>> moderator.handle_message(mlist, id_4, Action.discard)
- >>> print config.db.message_store.get_message_by_id(u'<12345>')
- None
-
-But if we ask to preserve the message when we discard it, it will be held in
-the message store after disposition.
-
- >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
- >>> moderator.handle_message(mlist, id_4, Action.discard, preserve=True)
- >>> stored_msg = config.db.message_store.get_message_by_id(u'<12345>')
- >>> print stored_msg.as_string()
- From: aperson@example.org
- To: alist@example.com
- Subject: Something important
- Message-ID: <12345>
- X-Message-ID-Hash: 4CF7EAU3SIXBPXBB5S6PEUMO62MWGQN6
- <BLANKLINE>
- Here's something important about our mailing list.
- <BLANKLINE>
-
-Orthogonal to preservation, the message can also be forwarded to another
-address. This is helpful for getting the message into the inbox of one of the
-moderators.
-
- # Set a new Message-ID from the previous hold so we don't try to store
- # collisions in the message storage.
- >>> del msg['message-id']
- >>> msg['Message-ID'] = u'<abcde>'
- >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval')
- >>> moderator.handle_message(mlist, id_4, Action.discard,
- ... forward=[u'zperson@example.com'])
- >>> qmsg, qdata = dequeue()
- >>> print qmsg.as_string()
- Subject: Forward of moderated message
- From: alist-bounces@example.com
- To: zperson@example.com
- MIME-Version: 1.0
- Content-Type: message/rfc822
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- From: aperson@example.org
- To: alist@example.com
- Subject: Something important
- Message-ID: <abcde>
- X-Message-ID-Hash: EN2R5UQFMOUTCL44FLNNPLSXBIZW62ER
- <BLANKLINE>
- Here's something important about our mailing list.
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'zperson@example.com']
- reduced_list_headers: True
- version : 3
-
-
-Holding subscription requests
------------------------------
-
-For closed lists, subscription requests will also be held for moderator
-approval. In this case, several pieces of information related to the
-subscription must be provided, including the subscriber's address and real
-name, their password (possibly hashed), what kind of delivery option they are
-chosing and their preferred language.
-
- >>> from mailman.interfaces.member import DeliveryMode
- >>> mlist.admin_immed_notify = False
- >>> id_3 = moderator.hold_subscription(mlist,
- ... u'bperson@example.org', u'Ben Person',
- ... u'{NONE}abcxyz', DeliveryMode.regular, u'en')
- >>> requests.get_request(id_3) is not None
- True
-
-In the above case the mailing list was not configured to send the list
-moderators a notice about the hold, so no email message is in the virgin
-queue.
-
- >>> virginq.files
- []
-
-But if we set the list up to notify the list moderators immediately when a
-message is held for approval, there will be a message placed in the virgin
-queue when the message is held.
-
- >>> mlist.admin_immed_notify = True
- >>> # XXX This will almost certainly change once we've worked out the web
- >>> # space layout for mailing lists now.
- >>> id_4 = moderator.hold_subscription(mlist,
- ... u'cperson@example.org', u'Claire Person',
- ... u'{NONE}zyxcba', DeliveryMode.regular, u'en')
- >>> requests.get_request(id_4) is not None
- True
- >>> qmsg, qdata = dequeue()
- >>> print qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: New subscription request to list A Test List from
- cperson@example.org
- From: alist-owner@example.com
- To: alist-owner@example.com
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Your authorization is required for a mailing list subscription request
- approval:
- <BLANKLINE>
- For: cperson@example.org
- List: alist@example.com
- <BLANKLINE>
- At your convenience, visit:
- <BLANKLINE>
- http://lists.example.com/admindb/alist@example.com
- <BLANKLINE>
- to process the request.
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'alist-owner@example.com']
- reduced_list_headers: True
- tomoderators : True
- version : 3
-
-Once held, the moderator can select one of several dispositions. The most
-trivial is to simply defer a decision for now.
-
- >>> moderator.handle_subscription(mlist, id_3, Action.defer)
- >>> requests.get_request(id_3) is not None
- True
-
-The held subscription can also be discarded.
-
- >>> moderator.handle_subscription(mlist, id_3, Action.discard)
- >>> print requests.get_request(id_3)
- None
-
-The request can be rejected, in which case a message is sent to the
-subscriber.
-
- >>> moderator.handle_subscription(mlist, id_4, Action.reject,
- ... 'This is a closed list')
- >>> print requests.get_request(id_4)
- None
- >>> qmsg, qdata = dequeue()
- >>> print qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: Request to mailing list "A Test List" rejected
- From: alist-bounces@example.com
- To: cperson@example.org
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Your request to the alist@example.com mailing list
- <BLANKLINE>
- Subscription request
- <BLANKLINE>
- has been rejected by the list moderator. The moderator gave the
- following reason for rejecting your request:
- <BLANKLINE>
- "This is a closed list"
- <BLANKLINE>
- Any questions or comments should be directed to the list administrator
- at:
- <BLANKLINE>
- alist-owner@example.com
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'cperson@example.org']
- reduced_list_headers: True
- version : 3
-
-The subscription can also be accepted. This subscribes the address to the
-mailing list.
-
- >>> mlist.send_welcome_msg = True
- >>> id_4 = moderator.hold_subscription(mlist,
- ... u'fperson@example.org', u'Frank Person',
- ... u'{NONE}abcxyz', DeliveryMode.regular, u'en')
-
-A message will be sent to the moderators telling them about the held
-subscription and the fact that they may need to approve it.
-
- >>> qmsg, qdata = dequeue()
- >>> print qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: New subscription request to list A Test List from
- fperson@example.org
- From: alist-owner@example.com
- To: alist-owner@example.com
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Your authorization is required for a mailing list subscription request
- approval:
- <BLANKLINE>
- For: fperson@example.org
- List: alist@example.com
- <BLANKLINE>
- At your convenience, visit:
- <BLANKLINE>
- http://lists.example.com/admindb/alist@example.com
- <BLANKLINE>
- to process the request.
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'alist-owner@example.com']
- reduced_list_headers: True
- tomoderators : True
- version : 3
-
-Accept the subscription request.
-
- >>> mlist.admin_notify_mchanges = True
- >>> moderator.handle_subscription(mlist, id_4, Action.accept)
-
-There are now two messages in the virgin queue. One is a welcome message
-being sent to the user and the other is a subscription notification that is
-sent to the moderators. The only good way to tell which is which is to look
-at the recipient list.
-
- >>> qmsg_1, qdata_1 = dequeue(expected_count=2)
- >>> qmsg_2, qdata_2 = dequeue()
- >>> if 'fperson@example.org' in qdata_1['recips']:
- ... # The first message is the welcome message
- ... welcome_qmsg = qmsg_1
- ... welcome_qdata = qdata_1
- ... admin_qmsg = qmsg_2
- ... admin_qdata = qdata_2
- ... else:
- ... welcome_qmsg = qmsg_2
- ... welcome_qdata = qdata_2
- ... admin_qmsg = qmsg_1
- ... admin_qdata = qdata_1
-
-The welcome message is sent to the person who just subscribed.
-
- >>> print welcome_qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: Welcome to the "A Test List" mailing list
- From: alist-request@example.com
- To: fperson@example.org
- X-No-Archive: yes
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Welcome to the "A Test List" mailing list!
- <BLANKLINE>
- To post to this list, send your email to:
- <BLANKLINE>
- alist@example.com
- <BLANKLINE>
- General information about the mailing list is at:
- <BLANKLINE>
- http://lists.example.com/listinfo/alist@example.com
- <BLANKLINE>
- If you ever want to unsubscribe or change your options (eg, switch to
- or from digest mode, change your password, etc.), visit your
- subscription page at:
- <BLANKLINE>
- http://example.com/fperson@example.org
- <BLANKLINE>
- You can also make such adjustments via email by sending a message to:
- <BLANKLINE>
- alist-request@example.com
- <BLANKLINE>
- with the word 'help' in the subject or body (don't include the
- quotes), and you will get back a message with instructions. You will
- need your password to change your options, but for security purposes,
- this email is not included here. There is also a button on your
- options page that will send your current password to you.
- <BLANKLINE>
- >>> dump_msgdata(welcome_qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'fperson@example.org']
- reduced_list_headers: True
- verp : False
- version : 3
-
-The admin message is sent to the moderators.
-
- >>> print admin_qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: A Test List subscription notification
- From: changeme@example.com
- To: alist-owner@example.com
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Frank Person <fperson@example.org> has been successfully subscribed to
- A Test List.
- <BLANKLINE>
- >>> dump_msgdata(admin_qdata)
- _parsemsg : False
- envsender : changeme@example.com
- listname : alist@example.com
- nodecorate : True
- recips : []
- reduced_list_headers: True
- version : 3
-
-Frank Person is now a member of the mailing list.
-
- >>> member = mlist.members.get_member(u'fperson@example.org')
- >>> member
- <Member: Frank Person <fperson@example.org>
- on alist@example.com as MemberRole.member>
- >>> member.preferred_language
- u'en'
- >>> print member.delivery_mode
- DeliveryMode.regular
- >>> user = config.db.user_manager.get_user(member.address.address)
- >>> user.real_name
- u'Frank Person'
- >>> user.password
- u'{NONE}abcxyz'
-
-
-Holding unsubscription requests
--------------------------------
-
-Some lists, though it is rare, require moderator approval for unsubscriptions.
-In this case, only the unsubscribing address is required. Like subscriptions,
-unsubscription holds can send the list's moderators an immediate notification.
-
- >>> mlist.admin_immed_notify = False
- >>> from mailman.interfaces.member import MemberRole
- >>> user_1 = config.db.user_manager.create_user(u'gperson@example.com')
- >>> address_1 = list(user_1.addresses)[0]
- >>> address_1.subscribe(mlist, MemberRole.member)
- <Member: gperson@example.com on alist@example.com as MemberRole.member>
- >>> user_2 = config.db.user_manager.create_user(u'hperson@example.com')
- >>> address_2 = list(user_2.addresses)[0]
- >>> address_2.subscribe(mlist, MemberRole.member)
- <Member: hperson@example.com on alist@example.com as MemberRole.member>
- >>> id_5 = moderator.hold_unsubscription(mlist, u'gperson@example.com')
- >>> requests.get_request(id_5) is not None
- True
- >>> virginq.files
- []
- >>> mlist.admin_immed_notify = True
- >>> id_6 = moderator.hold_unsubscription(mlist, u'hperson@example.com')
- >>> qmsg, qdata = dequeue()
- >>> print qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: New unsubscription request from A Test List by hperson@example.com
- From: alist-owner@example.com
- To: alist-owner@example.com
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Your authorization is required for a mailing list unsubscription
- request approval:
- <BLANKLINE>
- By: hperson@example.com
- From: alist@example.com
- <BLANKLINE>
- At your convenience, visit:
- <BLANKLINE>
- http://lists.example.com/admindb/alist@example.com
- <BLANKLINE>
- to process the request.
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'alist-owner@example.com']
- reduced_list_headers: True
- tomoderators : True
- version : 3
-
-There are now two addresses with held unsubscription requests. As above, one
-of the actions we can take is to defer to the decision.
-
- >>> moderator.handle_unsubscription(mlist, id_5, Action.defer)
- >>> requests.get_request(id_5) is not None
- True
-
-The held unsubscription can also be discarded, and the member will remain
-subscribed.
-
- >>> moderator.handle_unsubscription(mlist, id_5, Action.discard)
- >>> print requests.get_request(id_5)
- None
- >>> mlist.members.get_member(u'gperson@example.com')
- <Member: gperson@example.com on alist@example.com as MemberRole.member>
-
-The request can be rejected, in which case a message is sent to the member,
-and the person remains a member of the mailing list.
-
- >>> moderator.handle_unsubscription(mlist, id_6, Action.reject,
- ... 'This list is a prison.')
- >>> print requests.get_request(id_6)
- None
- >>> qmsg, qdata = dequeue()
- >>> print qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: Request to mailing list "A Test List" rejected
- From: alist-bounces@example.com
- To: hperson@example.com
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- Your request to the alist@example.com mailing list
- <BLANKLINE>
- Unsubscription request
- <BLANKLINE>
- has been rejected by the list moderator. The moderator gave the
- following reason for rejecting your request:
- <BLANKLINE>
- "This list is a prison."
- <BLANKLINE>
- Any questions or comments should be directed to the list administrator
- at:
- <BLANKLINE>
- alist-owner@example.com
- <BLANKLINE>
- >>> dump_msgdata(qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'hperson@example.com']
- reduced_list_headers: True
- version : 3
-
- >>> mlist.members.get_member(u'hperson@example.com')
- <Member: hperson@example.com on alist@example.com as MemberRole.member>
-
-The unsubscription request can also be accepted. This removes the member from
-the mailing list.
-
- >>> mlist.send_goodbye_msg = True
- >>> mlist.goodbye_msg = u'So long!'
- >>> mlist.admin_immed_notify = False
- >>> id_7 = moderator.hold_unsubscription(mlist, u'gperson@example.com')
- >>> moderator.handle_unsubscription(mlist, id_7, Action.accept)
- >>> print mlist.members.get_member(u'gperson@example.com')
- None
-
-There are now two messages in the virgin queue, one to the member who was just
-unsubscribed and another to the moderators informing them of this membership
-change.
-
- >>> qmsg_1, qdata_1 = dequeue(expected_count=2)
- >>> qmsg_2, qdata_2 = dequeue()
- >>> if 'gperson@example.com' in qdata_1['recips']:
- ... # The first message is the goodbye message
- ... goodbye_qmsg = qmsg_1
- ... goodbye_qdata = qdata_1
- ... admin_qmsg = qmsg_2
- ... admin_qdata = qdata_2
- ... else:
- ... goodbye_qmsg = qmsg_2
- ... goodbye_qdata = qdata_2
- ... admin_qmsg = qmsg_1
- ... admin_qdata = qdata_1
-
-The goodbye message...
-
- >>> print goodbye_qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: You have been unsubscribed from the A Test List mailing list
- From: alist-bounces@example.com
- To: gperson@example.com
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- So long!
- <BLANKLINE>
- >>> dump_msgdata(goodbye_qdata)
- _parsemsg : False
- listname : alist@example.com
- nodecorate : True
- recips : [u'gperson@example.com']
- reduced_list_headers: True
- verp : False
- version : 3
-
-...and the admin message.
-
- >>> print admin_qmsg.as_string()
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: A Test List unsubscription notification
- From: changeme@example.com
- To: alist-owner@example.com
- Message-ID: ...
- Date: ...
- Precedence: bulk
- <BLANKLINE>
- gperson@example.com has been removed from A Test List.
- <BLANKLINE>
- >>> dump_msgdata(admin_qdata)
- _parsemsg : False
- envsender : changeme@example.com
- listname : alist@example.com
- nodecorate : True
- recips : []
- reduced_list_headers: True
- version : 3
diff --git a/mailman/docs/styles.txt b/mailman/docs/styles.txt
deleted file mode 100644
index d12e57b08..000000000
--- a/mailman/docs/styles.txt
+++ /dev/null
@@ -1,156 +0,0 @@
-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.
-
- >>> mlist = config.db.list_manager.create(u'_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 = u'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))
- ['test']
- >>> for style in style_manager.lookup(mlist):
- ... style.apply(mlist)
- >>> mlist.msg_footer
- u'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 = u'another footer'
-
- >>> mlist.msg_footer = u''
- >>> mlist.msg_footer
- u''
- >>> style_manager.register(AnotherTestStyle())
- >>> for style in style_manager.lookup(mlist):
- ... style.apply(mlist)
- >>> mlist.msg_footer
- u'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)
- >>> mlist.msg_footer
- u'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))
- ['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: 'another'
diff --git a/mailman/docs/usermanager.txt b/mailman/docs/usermanager.txt
deleted file mode 100644
index f8cbfeef2..000000000
--- a/mailman/docs/usermanager.txt
+++ /dev/null
@@ -1,124 +0,0 @@
-The user manager
-================
-
-The IUserManager is how you create, delete, and manage 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.interfaces.usermanager import IUserManager
- >>> from zope.interface.verify import verifyObject
- >>> usermgr = config.db.user_manager
- >>> verifyObject(IUserManager, usermgr)
- True
-
-
-Creating users
---------------
-
-There are several ways you can create a user object. The simplest is to
-create a 'blank' user by not providing an address or real name at creation
-time. This user will have an empty string as their real name, but will not
-have a password.
-
- >>> from mailman.interfaces.user import IUser
- >>> user = usermgr.create_user()
- >>> verifyObject(IUser, user)
- True
- >>> sorted(address.address for address in user.addresses)
- []
- >>> user.real_name
- u''
- >>> print user.password
- None
-
-The user has preferences, but none of them will be specified.
-
- >>> print user.preferences
- <Preferences ...>
-
-A user can be assigned a real name.
-
- >>> user.real_name = u'Anne Person'
- >>> sorted(user.real_name for user in usermgr.users)
- [u'Anne Person']
-
-A user can be assigned a password.
-
- >>> user.password = u'secret'
- >>> sorted(user.password for user in usermgr.users)
- [u'secret']
-
-You can also create a user with an address to start out with.
-
- >>> user_2 = usermgr.create_user(u'bperson@example.com')
- >>> verifyObject(IUser, user_2)
- True
- >>> sorted(address.address for address in user_2.addresses)
- [u'bperson@example.com']
- >>> sorted(user.real_name for user in usermgr.users)
- [u'', u'Anne Person']
-
-As above, you can assign a real name to such users.
-
- >>> user_2.real_name = u'Ben Person'
- >>> sorted(user.real_name for user in usermgr.users)
- [u'Anne Person', u'Ben Person']
-
-You can also create a user with just a real name.
-
- >>> user_3 = usermgr.create_user(real_name=u'Claire Person')
- >>> verifyObject(IUser, user_3)
- True
- >>> sorted(address.address for address in user.addresses)
- []
- >>> sorted(user.real_name for user in usermgr.users)
- [u'Anne Person', u'Ben Person', u'Claire Person']
-
-Finally, you can create a user with both an address and a real name.
-
- >>> user_4 = usermgr.create_user(u'dperson@example.com', u'Dan Person')
- >>> verifyObject(IUser, user_3)
- True
- >>> sorted(address.address for address in user_4.addresses)
- [u'dperson@example.com']
- >>> sorted(address.real_name for address in user_4.addresses)
- [u'Dan Person']
- >>> sorted(user.real_name for user in usermgr.users)
- [u'Anne Person', u'Ben Person', u'Claire Person', u'Dan Person']
-
-
-Deleting users
---------------
-
-You delete users by going through the user manager. The deleted user is no
-longer available through the user manager iterator.
-
- >>> usermgr.delete_user(user)
- >>> sorted(user.real_name for user in usermgr.users)
- [u'Ben Person', u'Claire Person', u'Dan Person']
-
-
-Finding users
--------------
-
-You can ask the user manager to find the IUser that controls a particular
-email address. You'll get back the original user object if it's found. Note
-that the .get_user() method takes a string email address, not an IAddress
-object.
-
- >>> address = list(user_4.addresses)[0]
- >>> found_user = usermgr.get_user(address.address)
- >>> found_user
- <User "Dan Person" at ...>
- >>> found_user is user_4
- True
-
-If the address is not in the user database or does not have a user associated
-with it, you will get None back.
-
- >>> print usermgr.get_user(u'zperson@example.com')
- None
- >>> user_4.unlink(address)
- >>> print usermgr.get_user(address.address)
- None
diff --git a/mailman/docs/users.txt b/mailman/docs/users.txt
deleted file mode 100644
index ff7b9ca84..000000000
--- a/mailman/docs/users.txt
+++ /dev/null
@@ -1,195 +0,0 @@
-Users
-=====
-
-Users are entities that represent people. A user has a real name and a
-password. Optionally a user may have some preferences and a set of addresses
-they control. A user also knows which mailing lists they are subscribed to.
-
-See usermanager.txt for examples of how to create, delete, and find users.
-
- >>> usermgr = config.db.user_manager
-
-
-User data
----------
-
-Users may have a real name and a password.
-
- >>> user_1 = usermgr.create_user()
- >>> user_1.password = u'my password'
- >>> user_1.real_name = u'Zoe Person'
- >>> sorted(user.real_name for user in usermgr.users)
- [u'Zoe Person']
- >>> sorted(user.password for user in usermgr.users)
- [u'my password']
-
-The password and real name can be changed at any time.
-
- >>> user_1.real_name = u'Zoe X. Person'
- >>> user_1.password = u'another password'
- >>> sorted(user.real_name for user in usermgr.users)
- [u'Zoe X. Person']
- >>> sorted(user.password for user in usermgr.users)
- [u'another password']
-
-
-Users addresses
----------------
-
-One of the pieces of information that a user links to is a set of email
-addresses they control, in the form of IAddress objects. A user can control
-many addresses, but addresses may be controlled by only one user.
-
-The easiest way to link a user to an address is to just register the new
-address on a user object.
-
- >>> user_1.register(u'zperson@example.com', u'Zoe Person')
- <Address: Zoe Person <zperson@example.com> [not verified] at 0x...>
- >>> user_1.register(u'zperson@example.org')
- <Address: zperson@example.org [not verified] at 0x...>
- >>> sorted(address.address for address in user_1.addresses)
- [u'zperson@example.com', u'zperson@example.org']
- >>> sorted(address.real_name for address in user_1.addresses)
- [u'', u'Zoe Person']
-
-You can also create the address separately and then link it to the user.
-
- >>> address_1 = usermgr.create_address(u'zperson@example.net')
- >>> user_1.link(address_1)
- >>> sorted(address.address for address in user_1.addresses)
- [u'zperson@example.com', u'zperson@example.net', u'zperson@example.org']
- >>> sorted(address.real_name for address in user_1.addresses)
- [u'', u'', u'Zoe Person']
-
-But don't try to link an address to more than one user.
-
- >>> another_user = usermgr.create_user()
- >>> another_user.link(address_1)
- Traceback (most recent call last):
- ...
- AddressAlreadyLinkedError: zperson@example.net
-
-You can also ask whether a given user controls a given address.
-
- >>> user_1.controls(address_1.address)
- True
- >>> user_1.controls(u'bperson@example.com')
- False
-
-Given a text email address, the user manager can find the user that controls
-that address.
-
- >>> usermgr.get_user(u'zperson@example.com') is user_1
- True
- >>> usermgr.get_user(u'zperson@example.net') is user_1
- True
- >>> usermgr.get_user(u'zperson@example.org') is user_1
- True
- >>> print usermgr.get_user(u'bperson@example.com')
- None
-
-Addresses can also be unlinked from a user.
-
- >>> user_1.unlink(address_1)
- >>> user_1.controls(u'zperson@example.net')
- False
- >>> print usermgr.get_user(u'aperson@example.net')
- None
-
-But don't try to unlink the address from a user it's not linked to.
-
- >>> user_1.unlink(address_1)
- Traceback (most recent call last):
- ...
- AddressNotLinkedError: zperson@example.net
- >>> another_user.unlink(address_1)
- Traceback (most recent call last):
- ...
- AddressNotLinkedError: zperson@example.net
-
-
-Users and preferences
----------------------
-
-This is a helper function for the following section.
-
- >>> def show_prefs(prefs):
- ... print 'acknowledge_posts :', prefs.acknowledge_posts
- ... print 'preferred_language :', prefs.preferred_language
- ... print 'receive_list_copy :', prefs.receive_list_copy
- ... print 'receive_own_postings :', prefs.receive_own_postings
- ... print 'delivery_mode :', prefs.delivery_mode
-
-Users have preferences, but these preferences have no default settings.
-
- >>> from mailman.interfaces.preferences import IPreferences
- >>> show_prefs(user_1.preferences)
- acknowledge_posts : None
- preferred_language : None
- receive_list_copy : None
- receive_own_postings : None
- delivery_mode : None
-
-Some of these preferences are booleans and they can be set to True or False.
-
- >>> from mailman.constants import DeliveryMode
- >>> prefs = user_1.preferences
- >>> prefs.acknowledge_posts = True
- >>> prefs.preferred_language = u'it'
- >>> prefs.receive_list_copy = False
- >>> prefs.receive_own_postings = False
- >>> prefs.delivery_mode = DeliveryMode.regular
- >>> show_prefs(user_1.preferences)
- acknowledge_posts : True
- preferred_language : it
- receive_list_copy : False
- receive_own_postings : False
- delivery_mode : DeliveryMode.regular
-
-
-Subscriptions
--------------
-
-Users know which mailing lists they are subscribed to, regardless of
-membership role.
-
- >>> user_1.link(address_1)
- >>> sorted(address.address for address in user_1.addresses)
- [u'zperson@example.com', u'zperson@example.net', u'zperson@example.org']
- >>> com = usermgr.get_address(u'zperson@example.com')
- >>> org = usermgr.get_address(u'zperson@example.org')
- >>> net = usermgr.get_address(u'zperson@example.net')
-
- >>> from mailman.app.lifecycle import create_list
- >>> mlist_1 = create_list(u'xtest_1@example.com')
- >>> mlist_2 = create_list(u'xtest_2@example.com')
- >>> mlist_3 = create_list(u'xtest_3@example.com')
- >>> from mailman.interfaces.member import MemberRole
-
- >>> com.subscribe(mlist_1, MemberRole.member)
- <Member: Zoe Person <zperson@example.com> on xtest_1@example.com as
- MemberRole.member>
- >>> org.subscribe(mlist_2, MemberRole.member)
- <Member: zperson@example.org on xtest_2@example.com as MemberRole.member>
- >>> org.subscribe(mlist_2, MemberRole.owner)
- <Member: zperson@example.org on xtest_2@example.com as MemberRole.owner>
- >>> net.subscribe(mlist_3, MemberRole.moderator)
- <Member: zperson@example.net on xtest_3@example.com as
- MemberRole.moderator>
-
- >>> memberships = user_1.memberships
- >>> from mailman.interfaces.roster import IRoster
- >>> from zope.interface.verify import verifyObject
- >>> verifyObject(IRoster, memberships)
- True
- >>> members = sorted(memberships.members)
- >>> len(members)
- 4
- >>> def sortkey(member):
- ... return member.address.address, member.mailing_list, int(member.role)
- >>> for member in sorted(members, key=sortkey):
- ... print member.address.address, member.mailing_list, member.role
- zperson@example.com xtest_1@example.com MemberRole.member
- zperson@example.net xtest_3@example.com MemberRole.moderator
- zperson@example.org xtest_2@example.com MemberRole.member
- zperson@example.org xtest_2@example.com MemberRole.owner