diff options
| author | Barry Warsaw | 2009-01-25 13:01:41 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2009-01-25 13:01:41 -0500 |
| commit | eefd06f1b88b8ecbb23a9013cd223b72ca85c20d (patch) | |
| tree | 72c947fe16fce0e07e996ee74020b26585d7e846 /mailman/docs | |
| parent | 07871212f74498abd56bef3919bf3e029eb8b930 (diff) | |
| download | mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.gz mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.tar.zst mailman-eefd06f1b88b8ecbb23a9013cd223b72ca85c20d.zip | |
Diffstat (limited to 'mailman/docs')
| -rw-r--r-- | mailman/docs/__init__.py | 0 | ||||
| -rw-r--r-- | mailman/docs/addresses.txt | 231 | ||||
| -rw-r--r-- | mailman/docs/archivers.txt | 184 | ||||
| -rw-r--r-- | mailman/docs/bounces.txt | 107 | ||||
| -rw-r--r-- | mailman/docs/chains.txt | 345 | ||||
| -rw-r--r-- | mailman/docs/domains.txt | 46 | ||||
| -rw-r--r-- | mailman/docs/languages.txt | 104 | ||||
| -rw-r--r-- | mailman/docs/lifecycle.txt | 136 | ||||
| -rw-r--r-- | mailman/docs/listmanager.txt | 88 | ||||
| -rw-r--r-- | mailman/docs/membership.txt | 230 | ||||
| -rw-r--r-- | mailman/docs/message.txt | 48 | ||||
| -rw-r--r-- | mailman/docs/messagestore.txt | 113 | ||||
| -rw-r--r-- | mailman/docs/mlist-addresses.txt | 76 | ||||
| -rw-r--r-- | mailman/docs/pending.txt | 94 | ||||
| -rw-r--r-- | mailman/docs/pipelines.txt | 186 | ||||
| -rw-r--r-- | mailman/docs/registration.txt | 362 | ||||
| -rw-r--r-- | mailman/docs/requests.txt | 883 | ||||
| -rw-r--r-- | mailman/docs/styles.txt | 156 | ||||
| -rw-r--r-- | mailman/docs/usermanager.txt | 124 | ||||
| -rw-r--r-- | mailman/docs/users.txt | 195 |
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 |
