From 41faffef13f11c793c140d7f18d3b0698685b7a2 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Tue, 12 Jan 2010 08:27:38 -0500 Subject: Documentation reorganization. --- src/mailman/app/docs/__init__.py | 0 src/mailman/app/docs/bounces.txt | 103 ++++ src/mailman/app/docs/chains.txt | 354 ++++++++++++ src/mailman/app/docs/hooks.txt | 108 ++++ src/mailman/app/docs/lifecycle.txt | 143 +++++ src/mailman/app/docs/message.txt | 48 ++ src/mailman/app/docs/pipelines.txt | 193 +++++++ src/mailman/app/docs/styles.txt | 160 ++++++ src/mailman/app/docs/system.txt | 27 + src/mailman/docs/ALPHA.txt | 81 --- src/mailman/docs/README.txt | 4 +- src/mailman/docs/START.txt | 81 +++ src/mailman/docs/addresses.txt | 236 -------- src/mailman/docs/autorespond.txt | 111 ---- src/mailman/docs/bounces.txt | 109 ---- src/mailman/docs/chains.txt | 354 ------------ src/mailman/docs/domains.txt | 119 ---- src/mailman/docs/hooks.txt | 108 ---- src/mailman/docs/languages.txt | 110 ---- src/mailman/docs/lifecycle.txt | 143 ----- src/mailman/docs/listmanager.txt | 101 ---- src/mailman/docs/membership.txt | 234 -------- src/mailman/docs/message.txt | 48 -- src/mailman/docs/messagestore.txt | 116 ---- src/mailman/docs/mlist-addresses.txt | 77 --- src/mailman/docs/pending.txt | 93 --- src/mailman/docs/pipelines.txt | 193 ------- src/mailman/docs/registration.txt | 350 ----------- src/mailman/docs/requests.txt | 895 ---------------------------- src/mailman/docs/styles.txt | 160 ------ src/mailman/docs/system.txt | 26 - src/mailman/docs/usermanager.txt | 125 ---- src/mailman/docs/users.txt | 208 ------- src/mailman/model/docs/__init__.py | 0 src/mailman/model/docs/addresses.txt | 236 ++++++++ src/mailman/model/docs/autorespond.txt | 112 ++++ src/mailman/model/docs/domains.txt | 119 ++++ src/mailman/model/docs/languages.txt | 110 ++++ src/mailman/model/docs/listmanager.txt | 101 ++++ src/mailman/model/docs/membership.txt | 234 ++++++++ src/mailman/model/docs/messagestore.txt | 116 ++++ src/mailman/model/docs/mlist-addresses.txt | 77 +++ src/mailman/model/docs/pending.txt | 94 +++ src/mailman/model/docs/registration.txt | 350 +++++++++++ src/mailman/model/docs/requests.txt | 896 +++++++++++++++++++++++++++++ src/mailman/model/docs/usermanager.txt | 125 ++++ src/mailman/model/docs/users.txt | 208 +++++++ 47 files changed, 3997 insertions(+), 3999 deletions(-) create mode 100644 src/mailman/app/docs/__init__.py create mode 100644 src/mailman/app/docs/bounces.txt create mode 100644 src/mailman/app/docs/chains.txt create mode 100644 src/mailman/app/docs/hooks.txt create mode 100644 src/mailman/app/docs/lifecycle.txt create mode 100644 src/mailman/app/docs/message.txt create mode 100644 src/mailman/app/docs/pipelines.txt create mode 100644 src/mailman/app/docs/styles.txt create mode 100644 src/mailman/app/docs/system.txt delete mode 100644 src/mailman/docs/ALPHA.txt create mode 100644 src/mailman/docs/START.txt delete mode 100644 src/mailman/docs/addresses.txt delete mode 100644 src/mailman/docs/autorespond.txt delete mode 100644 src/mailman/docs/bounces.txt delete mode 100644 src/mailman/docs/chains.txt delete mode 100644 src/mailman/docs/domains.txt delete mode 100644 src/mailman/docs/hooks.txt delete mode 100644 src/mailman/docs/languages.txt delete mode 100644 src/mailman/docs/lifecycle.txt delete mode 100644 src/mailman/docs/listmanager.txt delete mode 100644 src/mailman/docs/membership.txt delete mode 100644 src/mailman/docs/message.txt delete mode 100644 src/mailman/docs/messagestore.txt delete mode 100644 src/mailman/docs/mlist-addresses.txt delete mode 100644 src/mailman/docs/pending.txt delete mode 100644 src/mailman/docs/pipelines.txt delete mode 100644 src/mailman/docs/registration.txt delete mode 100644 src/mailman/docs/requests.txt delete mode 100644 src/mailman/docs/styles.txt delete mode 100644 src/mailman/docs/system.txt delete mode 100644 src/mailman/docs/usermanager.txt delete mode 100644 src/mailman/docs/users.txt create mode 100644 src/mailman/model/docs/__init__.py create mode 100644 src/mailman/model/docs/addresses.txt create mode 100644 src/mailman/model/docs/autorespond.txt create mode 100644 src/mailman/model/docs/domains.txt create mode 100644 src/mailman/model/docs/languages.txt create mode 100644 src/mailman/model/docs/listmanager.txt create mode 100644 src/mailman/model/docs/membership.txt create mode 100644 src/mailman/model/docs/messagestore.txt create mode 100644 src/mailman/model/docs/mlist-addresses.txt create mode 100644 src/mailman/model/docs/pending.txt create mode 100644 src/mailman/model/docs/registration.txt create mode 100644 src/mailman/model/docs/requests.txt create mode 100644 src/mailman/model/docs/usermanager.txt create mode 100644 src/mailman/model/docs/users.txt diff --git a/src/mailman/app/docs/__init__.py b/src/mailman/app/docs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/mailman/app/docs/bounces.txt b/src/mailman/app/docs/bounces.txt new file mode 100644 index 000000000..a12305154 --- /dev/null +++ b/src/mailman/app/docs/bounces.txt @@ -0,0 +1,103 @@ +======= +Bounces +======= + +An important feature of Mailman is automatic bounce process. + +XXX Many more converted tests go here. + + +Bounces, or message rejection +============================= + +Mailman can also bounce messages back to the original sender. This is +essentially equivalent to rejecting the message with notification. Mailing +lists can bounce a message with an optional error message. + + >>> mlist = create_list('_xtest@example.com') + +Any message can be bounced. + + >>> msg = message_from_string("""\ + ... To: _xtest@example.com + ... From: aperson@example.com + ... Subject: Something important + ... + ... I sometimes say something important. + ... """) + +Bounce a message by passing in the original message, and an optional error +message. The bounced message ends up in the virgin queue, awaiting sending +to the original messageauthor. + + >>> from mailman.app.bounces import bounce_message + >>> bounce_message(mlist, msg) + >>> from mailman.testing.helpers import get_queue_messages + >>> items = get_queue_messages('virgin') + >>> len(items) + 1 + >>> print items[0].msg.as_string() + Subject: Something important + From: _xtest-owner@example.com + To: aperson@example.com + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="..." + Message-ID: ... + Date: ... + Precedence: bulk + + --... + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + + [No bounce details are available] + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + To: _xtest@example.com + From: aperson@example.com + Subject: Something important + + I sometimes say something important. + + --...-- + +An error message can be given when the message is bounced, and this will be +included in the payload of the text/plain part. The error message must be +passed in as an instance of a RejectMessage exception. + + >>> from mailman.core.errors import RejectMessage + >>> error = RejectMessage("This wasn't very important after all.") + >>> bounce_message(mlist, msg, error) + >>> items = get_queue_messages('virgin') + >>> len(items) + 1 + >>> print items[0].msg.as_string() + Subject: Something important + From: _xtest-owner@example.com + To: aperson@example.com + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="..." + Message-ID: ... + Date: ... + Precedence: bulk + + --... + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + + This wasn't very important after all. + --... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + To: _xtest@example.com + From: aperson@example.com + Subject: Something important + + I sometimes say something important. + + --...-- diff --git a/src/mailman/app/docs/chains.txt b/src/mailman/app/docs/chains.txt new file mode 100644 index 000000000..f9ed156b1 --- /dev/null +++ b/src/mailman/app/docs/chains.txt @@ -0,0 +1,354 @@ +====== +Chains +====== + +When a new message comes into the system, Mailman uses a set of rule chains to +decide whether the message gets posted to the list, rejected, discarded, or +held for moderator approval. + +There are a number of built-in chains available that act as end-points in the +processing of messages. + + +The Discard chain +================= + +The Discard chain simply throws the message away. + + >>> from zope.interface.verify import verifyObject + >>> from mailman.interfaces.chain import IChain + >>> chain = config.chains['discard'] + >>> verifyObject(IChain, chain) + True + >>> print chain.name + discard + >>> print chain.description + Discard a message and stop processing. + + >>> mlist = create_list('_xtest@example.com') + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: _xtest@example.com + ... Subject: My first post + ... Message-ID: + ... + ... 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: + + + +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: + +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 + + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: + + An important message. + + ... + + +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=: n/a + + +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: + + List: _xtest@example.com + From: aperson@example.com + Subject: My first post + Reason: XXX + + At your convenience, visit: + + http://lists.example.com/admindb/_xtest@example.com + + to approve or deny the request. + + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + + An important message. + + ... + Content-Type: message/rfc822 + MIME-Version: 1.0 + + 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 + ... + + 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 + + My first post + + Is being held until the list moderator can review it for approval. + + The reason it is being held: + + XXX + + 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: + + http://lists.example.com/confirm/_xtest@example.com/... + + + +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.*)$', line) + ... if mo: + ... cookie = mo.group('cookie') + ... break + >>> assert cookie is not None, 'No confirmation token found' + + >>> from mailman.interfaces.pending import IPendings + >>> from zope.component import getUtility + + >>> data = getUtility(IPendings).confirm(cookie) + >>> sorted(data.items()) + [(u'id', ...), (u'type', u'held message')] + +The message itself is held in the message store. + + >>> from mailman.interfaces.requests import IRequests + >>> list_requests = getUtility(IRequests).get_list_requests(mlist) + >>> rkey, rdata = list_requests.get_request(data['id']) + + >>> from mailman.interfaces.messages import IMessageStore + >>> from zope.component import getUtility + >>> msg = getUtility(IMessageStore).get_message_by_id( + ... rdata['_mod_message_id']) + + >>> print msg.as_string() + From: aperson@example.com + To: _xtest@example.com + Subject: My first post + Message-ID: + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + + An important message. + + + +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: + + >>> 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: + X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW + + An important message. + + + +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: + + >>> 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: + 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 + + An important message. + + +In addition, the message metadata now contains lists of all rules that have +hit and all rules that have missed. + + >>> sorted(qdata['rule_hits']) + [] + >>> for rule_name in sorted(qdata['rule_misses']): + ... print rule_name + administrivia + approved + emergency + implicit-dest + loop + max-recipients + max-size + news-moderation + no-subject + suspicious-header diff --git a/src/mailman/app/docs/hooks.txt b/src/mailman/app/docs/hooks.txt new file mode 100644 index 000000000..14dc76667 --- /dev/null +++ b/src/mailman/app/docs/hooks.txt @@ -0,0 +1,108 @@ +===== +Hooks +===== + +Mailman defines two initialization hooks, one which is run early in the +initialization process and the other run late in the initialization process. +Hooks name an importable callable so it must be accessible on sys.path. + + >>> import os, sys + >>> from mailman.config import config + >>> config_directory = os.path.dirname(config.filename) + >>> sys.path.insert(0, config_directory) + + >>> hook_path = os.path.join(config_directory, 'hooks.py') + >>> with open(hook_path, 'w') as fp: + ... print >> fp, """\ + ... counter = 1 + ... def pre_hook(): + ... global counter + ... print 'pre-hook:', counter + ... counter += 1 + ... + ... def post_hook(): + ... global counter + ... print 'post-hook:', counter + ... counter += 1 + ... """ + >>> fp.close() + + +Pre-hook +======== + +We can set the pre-hook in the configuration file. + + >>> config_path = os.path.join(config_directory, 'hooks.cfg') + >>> with open(config_path, 'w') as fp: + ... print >> fp, """\ + ... [meta] + ... extends: test.cfg + ... + ... [mailman] + ... pre_hook: hooks.pre_hook + ... """ + +The hooks are run in the second and third steps of initialization. However, +we can't run those initialization steps in process, so call a command line +script that will produce no output to force the hooks to run. + + >>> import subprocess + >>> def call(): + ... proc = subprocess.Popen( + ... 'bin/mailman lists --domain ignore -q'.split(), + ... cwd='../..', # testrunner runs from ./parts/test + ... env=dict(MAILMAN_CONFIG_FILE=config_path, + ... PYTHONPATH=config_directory), + ... stdout=subprocess.PIPE, stderr=subprocess.PIPE) + ... stdout, stderr = proc.communicate() + ... assert proc.returncode == 0, stderr + ... print stdout + + >>> call() + pre-hook: 1 + + + >>> os.remove(config_path) + + +Post-hook +========= + +We can set the post-hook in the configuration file. + + >>> with open(config_path, 'w') as fp: + ... print >> fp, """\ + ... [meta] + ... extends: test.cfg + ... + ... [mailman] + ... post_hook: hooks.post_hook + ... """ + + >>> call() + post-hook: 1 + + + >>> os.remove(config_path) + + +Running both hooks +================== + +We can set the pre- and post-hooks in the configuration file. + + >>> with open(config_path, 'w') as fp: + ... print >> fp, """\ + ... [meta] + ... extends: test.cfg + ... + ... [mailman] + ... pre_hook: hooks.pre_hook + ... post_hook: hooks.post_hook + ... """ + + >>> call() + pre-hook: 1 + post-hook: 2 + diff --git a/src/mailman/app/docs/lifecycle.txt b/src/mailman/app/docs/lifecycle.txt new file mode 100644 index 000000000..a1cd50825 --- /dev/null +++ b/src/mailman/app/docs/lifecycle.txt @@ -0,0 +1,143 @@ +================================= +Application level list life cycle +================================= + +The low-level way to create and delete a mailing list is to use the +IListManager interface. This interface simply adds or removes the appropriate +database entries to record the list's creation. + +There is a higher level interface for creating and deleting mailing lists +which performs additional tasks such as: + + * validating the list's posting address (which also serves as the list's + fully qualified name); + * ensuring that the list's domain is registered; + * applying all matching styles to the new list; + * creating and assigning list owners; + * notifying watchers of list creation; + * creating ancillary artifacts (such as the list's on-disk directory) + + >>> from mailman.app.lifecycle import create_list + + +Posting address validation +========================== + +If you try to use the higher-level interface to create a mailing list with a +bogus posting address, you get an exception. + + >>> create_list('not a valid address') + Traceback (most recent call last): + ... + InvalidEmailAddressError: u'not a valid address' + +If the posting address is valid, but the domain has not been registered with +Mailman yet, you get an exception. + + >>> create_list('test@example.org') + Traceback (most recent call last): + ... + BadDomainSpecificationError: example.org + + +Creating a list applies its styles +================================== + +Start by registering a test style. + + >>> from zope.interface import implements + >>> from mailman.interfaces.styles import IStyle + >>> class TestStyle(object): + ... implements(IStyle) + ... name = 'test' + ... priority = 10 + ... def apply(self, mailing_list): + ... # Just does something very simple. + ... mailing_list.msg_footer = 'test footer' + ... def match(self, mailing_list, styles): + ... # Applies to any test list + ... if 'test' in mailing_list.fqdn_listname: + ... styles.append(self) + + >>> config.style_manager.register(TestStyle()) + +Using the higher level interface for creating a list, applies all matching +list styles. + + >>> mlist_1 = create_list('test_1@example.com') + >>> print mlist_1.fqdn_listname + test_1@example.com + >>> print mlist_1.msg_footer + test footer + + +Creating a list with owners +=========================== + +You can also specify a list of owner email addresses. If these addresses are +not yet known, they will be registered, and new users will be linked to them. +However the addresses are not verified. + + >>> owners = ['aperson@example.com', 'bperson@example.com', + ... 'cperson@example.com', 'dperson@example.com'] + >>> mlist_2 = create_list('test_2@example.com', owners) + >>> print mlist_2.fqdn_listname + test_2@example.com + >>> print mlist_2.msg_footer + test footer + >>> sorted(addr.address for addr in mlist_2.owners.addresses) + [u'aperson@example.com', u'bperson@example.com', + u'cperson@example.com', u'dperson@example.com'] + +None of the owner addresses are verified. + + >>> any(addr.verified_on is not None for addr in mlist_2.owners.addresses) + False + +However, all addresses are linked to users. + + >>> # The owners have no names yet + >>> len(list(mlist_2.owners.users)) + 4 + +If you create a mailing list with owner addresses that are already known to +the system, they won't be created again. + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> user_manager = getUtility(IUserManager) + + >>> user_a = user_manager.get_user('aperson@example.com') + >>> user_b = user_manager.get_user('bperson@example.com') + >>> user_c = user_manager.get_user('cperson@example.com') + >>> user_d = user_manager.get_user('dperson@example.com') + >>> user_a.real_name = 'Anne Person' + >>> user_b.real_name = 'Bart Person' + >>> user_c.real_name = 'Caty Person' + >>> user_d.real_name = 'Dirk Person' + + >>> mlist_3 = create_list('test_3@example.com', owners) + >>> sorted(user.real_name for user in mlist_3.owners.users) + [u'Anne Person', u'Bart Person', u'Caty Person', u'Dirk Person'] + + +Removing a list +=============== + +Removing a mailing list deletes the list, all its subscribers, and any related +artifacts. + + >>> from mailman.app.lifecycle import remove_list + >>> remove_list(mlist_2.fqdn_listname, mlist_2, True) + + >>> from mailman.interfaces.listmanager import IListManager + >>> from zope.component import getUtility + >>> print getUtility(IListManager).get('test_2@example.com') + None + +We should now be able to completely recreate the mailing list. + + >>> mlist_2a = create_list('test_2@example.com', owners) + >>> sorted(addr.address for addr in mlist_2a.owners.addresses) + [u'aperson@example.com', u'bperson@example.com', + u'cperson@example.com', u'dperson@example.com'] diff --git a/src/mailman/app/docs/message.txt b/src/mailman/app/docs/message.txt new file mode 100644 index 000000000..41607ff44 --- /dev/null +++ b/src/mailman/app/docs/message.txt @@ -0,0 +1,48 @@ +======== +Messages +======== + +Mailman has its own Message classes, derived from the standard +email.message.Message class, but providing additional useful methods. + + +User notifications +================== + +When Mailman needs to send a message to a user, it creates a UserNotification +instance, and then calls the .send() method on this object. This method +requires a mailing list instance. + + >>> mlist = create_list('_xtest@example.com') + +The UserNotification constructor takes the recipient address, the sender +address, an optional subject, optional body text, and optional language. + + >>> from mailman.email.message import UserNotification + >>> msg = UserNotification( + ... 'aperson@example.com', + ... '_xtest@example.com', + ... 'Something you need to know', + ... 'I needed to tell you this.') + >>> msg.send(mlist) + +The message will end up in the virgin queue. + + >>> switchboard = config.switchboards['virgin'] + >>> len(switchboard.files) + 1 + >>> filebase = switchboard.files[0] + >>> qmsg, qmsgdata = switchboard.dequeue(filebase) + >>> switchboard.finish(filebase) + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Something you need to know + From: _xtest@example.com + To: aperson@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + I needed to tell you this. diff --git a/src/mailman/app/docs/pipelines.txt b/src/mailman/app/docs/pipelines.txt new file mode 100644 index 000000000..cf848f1d9 --- /dev/null +++ b/src/mailman/app/docs/pipelines.txt @@ -0,0 +1,193 @@ +========= +Pipelines +========= + +This runner's purpose in life is to process messages that have been accepted +for posting, applying any modifications and also sending copies of the message +to the archives, digests, nntp, and outgoing queues. Pipelines are named and +consist of a sequence of handlers, each of which is applied in turn. Unlike +rules and chains, there is no way to stop a pipeline from processing the +message once it's started. + + >>> mlist = create_list('xtest@example.com') + >>> print mlist.pipeline + built-in + >>> from mailman.core.pipelines import process + + +Processing a message +==================== + +Messages hit the pipeline after they've been accepted for posting. + + >>> msg = message_from_string("""\ + ... From: aperson@example.com + ... To: xtest@example.com + ... Subject: My first post + ... Message-ID: + ... + ... First 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: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + +And the message metadata has information about recipients and other stuff. +However there are currently no recipients for this message. + + >>> dump_msgdata(msgdata) + original_sender : aperson@example.com + origsubj : My first post + recipients : set([]) + stripped_subject: My first post + +And the message is now sitting in various other processing queues. + + >>> from mailman.testing.helpers import get_queue_messages + >>> messages = get_queue_messages('archive') + >>> len(messages) + 1 + >>> print messages[0].msg.as_string() + From: aperson@example.com + To: xtest@example.com + Message-ID: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + >>> dump_msgdata(messages[0].msgdata) + _parsemsg : False + original_sender : aperson@example.com + origsubj : My first post + recipients : set([]) + stripped_subject: My first post + version : 3 + +This mailing list is not linked to an NNTP newsgroup, so there's nothing in +the outgoing nntp queue. + + >>> messages = get_queue_messages('news') + >>> len(messages) + 0 + +This is the message that will actually get delivered to end recipients. + + >>> messages = get_queue_messages('out') + >>> len(messages) + 1 + >>> print messages[0].msg.as_string() + From: aperson@example.com + To: xtest@example.com + Message-ID: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + >>> dump_msgdata(messages[0].msgdata) + _parsemsg : False + listname : xtest@example.com + original_sender : aperson@example.com + origsubj : My first post + recipients : set([]) + stripped_subject: My first post + version : 3 + +There's now one message in the digest mailbox, getting ready to be sent. + + >>> from mailman.testing.helpers import digest_mbox + >>> digest = digest_mbox(mlist) + >>> sum(1 for mboxmsg in digest) + 1 + >>> print list(digest)[0].as_string() + From: aperson@example.com + To: xtest@example.com + Message-ID: + Subject: [Xtest] My first post + X-BeenThere: xtest@example.com + X-Mailman-Version: ... + Precedence: list + List-Id: + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Post: + List-Subscribe: + , + + Archived-At: + http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + , + + List-Archive: + List-Help: + + First post! + + + + +Clean up the digests +==================== + + >>> digest.clear() + >>> digest.flush() + >>> sum(1 for msg in digest_mbox(mlist)) + 0 diff --git a/src/mailman/app/docs/styles.txt b/src/mailman/app/docs/styles.txt new file mode 100644 index 000000000..10312cd3a --- /dev/null +++ b/src/mailman/app/docs/styles.txt @@ -0,0 +1,160 @@ +=========== +List styles +=========== + +List styles are a way to name and apply a canned collection of attribute +settings. Every style has a name, which must be unique within the context of +a specific style manager. There is usually only one global style manager. + +Styles also have a priority, which allows you to specify the order in which +multiple styles will be applied. A style has a `match` function which is used +to determine whether the style should be applied to a particular mailing list +or not. And finally, application of a style to a mailing list can really +modify the mailing list any way it wants. + +Let's start with a vanilla mailing list and a default style manager. + + >>> from mailman.interfaces.listmanager import IListManager + >>> from zope.component import getUtility + >>> mlist = getUtility(IListManager).create('_xtest@example.com') + + >>> from mailman.styles.manager import StyleManager + >>> style_manager = StyleManager() + >>> style_manager.populate() + >>> sorted(style.name for style in style_manager.styles) + ['default'] + + +The default style +================= + +There is a default style which implements the legacy application of list +defaults from previous versions of Mailman. This style only matching a +mailing list when no other styles match, and it has the lowest priority. The +low priority means that it is matched last and if it matches, it is applied +last. + + >>> default_style = style_manager.get('default') + >>> default_style.name + 'default' + >>> default_style.priority + 0 + >>> sorted(style.name for style in style_manager.styles) + ['default'] + +Given a mailing list, you can ask the style manager to find all the styles +that match the list. The registered styles will be sorted by decreasing +priority and each style's `match()` method will be called in turn. The sorted +list of matching styles will be returned -- but not applied -- by the style +manager's `lookup()` method. + + >>> [style.name for style in style_manager.lookup(mlist)] + ['default'] + + +Registering styles +================== + +New styles must implement the IStyle interface. + + >>> from zope.interface import implements + >>> from mailman.interfaces.styles import IStyle + >>> class TestStyle(object): + ... implements(IStyle) + ... name = 'test' + ... priority = 10 + ... def apply(self, mailing_list): + ... # Just does something very simple. + ... mailing_list.msg_footer = 'test footer' + ... def match(self, mailing_list, styles): + ... # Applies to any test list + ... if 'test' in mailing_list.fqdn_listname: + ... styles.append(self) + +You can register a new style with the style manager. + + >>> style_manager.register(TestStyle()) + +And now if you lookup matching styles, you should find only the new test +style. This is because the default style only gets applied when no other +styles match the mailing list. + + >>> sorted(style.name for style in style_manager.lookup(mlist)) + [u'test'] + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.msg_footer + test footer + + +Style priority +============== + +When multiple styles match a particular mailing list, they are applied in +descending order of priority. In other words, a priority zero style would be +applied last. + + >>> class AnotherTestStyle(TestStyle): + ... name = 'another' + ... priority = 5 + ... # Use the base class's match() method. + ... def apply(self, mailing_list): + ... mailing_list.msg_footer = 'another footer' + + >>> mlist.msg_footer = '' + >>> mlist.msg_footer + u'' + >>> style_manager.register(AnotherTestStyle()) + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.msg_footer + another footer + +You can change the priority of a style, and if you reapply the styles, they +will take effect in the new priority order. + + >>> style_1 = style_manager.get('test') + >>> style_1.priority = 5 + >>> style_2 = style_manager.get('another') + >>> style_2.priority = 10 + >>> for style in style_manager.lookup(mlist): + ... style.apply(mlist) + >>> print mlist.msg_footer + test footer + + +Unregistering styles +==================== + +You can unregister a style, making it unavailable in the future. + + >>> style_manager.unregister(style_2) + >>> sorted(style.name for style in style_manager.lookup(mlist)) + [u'test'] + + +Corner cases +============ + +If you register a style with the same name as an already registered style, you +get an exception. + + >>> style_manager.register(TestStyle()) + Traceback (most recent call last): + ... + DuplicateStyleError: test + +If you try to register an object that isn't a style, you get an exception. + + >>> style_manager.register(object()) + Traceback (most recent call last): + ... + DoesNotImplement: An object does not implement interface + + +If you try to unregister a style that isn't registered, you get an exception. + + >>> style_manager.unregister(style_2) + Traceback (most recent call last): + ... + KeyError: u'another' diff --git a/src/mailman/app/docs/system.txt b/src/mailman/app/docs/system.txt new file mode 100644 index 000000000..035833047 --- /dev/null +++ b/src/mailman/app/docs/system.txt @@ -0,0 +1,27 @@ +=============== +System versions +=============== + +Mailman system information is available through the System object, which +implements the ISystem interface. + + >>> from mailman.interfaces.system import ISystem + >>> from mailman.core.system import system + >>> from zope.interface.verify import verifyObject + + >>> verifyObject(ISystem, system) + True + +The Mailman version is available via the system object. + + >>> print system.mailman_version + GNU Mailman ... + +The Python version running underneath is also available via the system +object. + + # The entire python_version string is variable, so this is the best test + # we can do. + >>> import sys + >>> system.python_version == sys.version + True diff --git a/src/mailman/docs/ALPHA.txt b/src/mailman/docs/ALPHA.txt deleted file mode 100644 index eb7b6aada..000000000 --- a/src/mailman/docs/ALPHA.txt +++ /dev/null @@ -1,81 +0,0 @@ -===================================== -GNU Mailman Alpha Release Information -===================================== - -Copyright (C) 2008-2010 by the Free Software Foundation, Inc. - - -Alpha Release -============= - -The Mailman 3 alpha releases are being provided to give developers and other -interested people an early look at the next major version. As such, some -things may not work yet. Your participation is encouraged. Your feedback and -contributions are welcome. Please submit bug reports on the Mailman bug -tracker at https://bugs.launchpad.net/mailman though you will currently need -to have a login on Launchpad to do so. You can also send email to the -mailman-developers@python.org mailing list. - - -Using the Alpha -=============== - -Python 2.6 is required. It can either be the default 'python' on your $PATH -or it can be accessible via the 'python2.6' binary. See http://www.python.org -for details on getting Python 2.6. - -Mailman 3 is now based on the `zc.buildout`_ infrastructure, which greatly -simplifies building and testing Mailman. - -You do not need anything other than Python 2.6 and an internet connection -to get all the other Mailman 3 dependencies. Here are the commands to -build everything:: - - % python2.6 bootstrap.py - % bin/buildout - -Now you can run the test suite via:: - - % bin/test - -You should see no failures. - -At this point you can read the doctests by looking in all the 'doc' -directories under the 'mailman' package. Doctests are documentation -first, so they should give you a pretty good idea how various components -of Mailman 3 works. - -What, you actually want to *run* Mailman 3? Oh well, if you insist. You -will need to set up a configuration file to override the defaults and set -things up for your environment. Mailman is configured via the `lazr.config`_ -package which is really just a fancy ini-style configuration system. - -For now though, start by looking through ``src/mailman/config/schema.cfg``. -Create a file for your overrides; it can be called anything and can live -anywhere, but I like to call it ``mailman.cfg``. For any value in -``schema.cfg`` you want to override, just add a section header (the -square-bracketed names) and then the key/value pair you want to override. - -Mailman searches for its configuration file using the following search path. -The first existing file found wins. - - * ``-C config`` command line option - * ``$MAILMAN_CONFIG_FILE`` environment variable - * ``./mailman.cfg`` - * ``~/.mailman.cfg`` - * ``/etc/mailman.cfg`` - -Run the ``bin/mailman info`` command to see which configuration file Mailman -will use, and where it will put its database file. The first time you run -this, Mailman will also create any necessary run-time directories and log -files. - -Try ``bin/mailman --help`` for more details. You can use the commands -``bin/mailman start`` to start the queue runner daemons, and of course -``bin/mailman stop`` to stop them. - -There is no web u/i right now. Contributions are welcome! - - -.. _`zc.buildout`: http://pypi.python.org/pypi/zc.buildout -.. _`lazr.config`: http://pypi.python.org/pypi/lazr.config diff --git a/src/mailman/docs/README.txt b/src/mailman/docs/README.txt index eb7361bd9..d0bbf6092 100644 --- a/src/mailman/docs/README.txt +++ b/src/mailman/docs/README.txt @@ -12,6 +12,8 @@ includes GNU/Linux and most other Unix-like operating systems (e.g. Solaris, mail clients on any platform should be able to interact with Mailman just fine. +Learn more about GNU Mailman in the `Getting Started`_ documentation. + Copyright ========= @@ -76,8 +78,6 @@ lists and archives, etc., are available at: http://www.list.org/help.html -For more information about the alpha releases, see `ALPHA.txt`_. - Requirements ============ diff --git a/src/mailman/docs/START.txt b/src/mailman/docs/START.txt new file mode 100644 index 000000000..eb7b6aada --- /dev/null +++ b/src/mailman/docs/START.txt @@ -0,0 +1,81 @@ +===================================== +GNU Mailman Alpha Release Information +===================================== + +Copyright (C) 2008-2010 by the Free Software Foundation, Inc. + + +Alpha Release +============= + +The Mailman 3 alpha releases are being provided to give developers and other +interested people an early look at the next major version. As such, some +things may not work yet. Your participation is encouraged. Your feedback and +contributions are welcome. Please submit bug reports on the Mailman bug +tracker at https://bugs.launchpad.net/mailman though you will currently need +to have a login on Launchpad to do so. You can also send email to the +mailman-developers@python.org mailing list. + + +Using the Alpha +=============== + +Python 2.6 is required. It can either be the default 'python' on your $PATH +or it can be accessible via the 'python2.6' binary. See http://www.python.org +for details on getting Python 2.6. + +Mailman 3 is now based on the `zc.buildout`_ infrastructure, which greatly +simplifies building and testing Mailman. + +You do not need anything other than Python 2.6 and an internet connection +to get all the other Mailman 3 dependencies. Here are the commands to +build everything:: + + % python2.6 bootstrap.py + % bin/buildout + +Now you can run the test suite via:: + + % bin/test + +You should see no failures. + +At this point you can read the doctests by looking in all the 'doc' +directories under the 'mailman' package. Doctests are documentation +first, so they should give you a pretty good idea how various components +of Mailman 3 works. + +What, you actually want to *run* Mailman 3? Oh well, if you insist. You +will need to set up a configuration file to override the defaults and set +things up for your environment. Mailman is configured via the `lazr.config`_ +package which is really just a fancy ini-style configuration system. + +For now though, start by looking through ``src/mailman/config/schema.cfg``. +Create a file for your overrides; it can be called anything and can live +anywhere, but I like to call it ``mailman.cfg``. For any value in +``schema.cfg`` you want to override, just add a section header (the +square-bracketed names) and then the key/value pair you want to override. + +Mailman searches for its configuration file using the following search path. +The first existing file found wins. + + * ``-C config`` command line option + * ``$MAILMAN_CONFIG_FILE`` environment variable + * ``./mailman.cfg`` + * ``~/.mailman.cfg`` + * ``/etc/mailman.cfg`` + +Run the ``bin/mailman info`` command to see which configuration file Mailman +will use, and where it will put its database file. The first time you run +this, Mailman will also create any necessary run-time directories and log +files. + +Try ``bin/mailman --help`` for more details. You can use the commands +``bin/mailman start`` to start the queue runner daemons, and of course +``bin/mailman stop`` to stop them. + +There is no web u/i right now. Contributions are welcome! + + +.. _`zc.buildout`: http://pypi.python.org/pypi/zc.buildout +.. _`lazr.config`: http://pypi.python.org/pypi/lazr.config diff --git a/src/mailman/docs/addresses.txt b/src/mailman/docs/addresses.txt deleted file mode 100644 index 5388a3cc8..000000000 --- a/src/mailman/docs/addresses.txt +++ /dev/null @@ -1,236 +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. - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - - -Creating addresses -================== - -Addresses are created directly through the user manager, which starts out with -no addresses. - - >>> sorted(address.address for address in user_manager.addresses) - [] - -Creating an unlinked email address is straightforward. - - >>> address_1 = user_manager.create_address('aperson@example.com') - >>> sorted(address.address for address in user_manager.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 = user_manager.create_address( - ... 'bperson@example.com', 'Ben Person') - >>> sorted(address.address for address in user_manager.addresses) - [u'aperson@example.com', u'bperson@example.com'] - >>> sorted(address.real_name for address in user_manager.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 ' - >>> repr(address_2) - ' [not verified] at 0x...>' - -You can assign real names to existing addresses. - - >>> address_1.real_name = 'Anne Person' - >>> sorted(address.real_name for address in user_manager.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 user_manager.get_user('aperson@example.com') - None - >>> print user_manager.get_user('bperson@example.com') - None - -You can create email addresses that are linked to users by using a different -interface. - - >>> user_1 = user_manager.create_user( - ... '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 user_manager.addresses) - [u'aperson@example.com', u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.real_name for address in user_manager.addresses) - [u'Anne Person', u'Ben Person', u'Claire Person'] - -And now you can find the associated user. - - >>> print user_manager.get_user('aperson@example.com') - None - >>> print user_manager.get_user('bperson@example.com') - None - >>> user_manager.get_user('cperson@example.com') - - - -Deleting addresses -================== - -You can remove an unlinked address from the user manager. - - >>> user_manager.delete_address(address_1) - >>> sorted(address.address for address in user_manager.addresses) - [u'bperson@example.com', u'cperson@example.com'] - >>> sorted(address.real_name for address in user_manager.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('cperson@example.com') - True - >>> address_3 = list(user_1.addresses)[0] - >>> user_manager.delete_address(address_3) - >>> sorted(address.address for address in user_1.addresses) - [] - >>> user_1.controls('cperson@example.com') - False - >>> sorted(address.address for address in user_manager.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 = user_manager.create_address( - ... 'dperson@example.com', '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 = user_manager.create_address( - ... 'eperson@example.com', 'Elly Person') - >>> mlist = create_list('_xtext@example.com') - - >>> from mailman.interfaces.member import MemberRole - >>> address_5.subscribe(mlist, MemberRole.owner) - on - _xtext@example.com as MemberRole.owner> - >>> address_5.subscribe(mlist, MemberRole.member) - on - _xtext@example.com as MemberRole.member> - -Now Elly is both an owner and a member of the mailing list. - - >>> sorted(mlist.owners.members) - [ on - _xtext@example.com as MemberRole.owner>] - >>> sorted(mlist.moderators.members) - [] - >>> sorted(mlist.administrators.members) - [ on - _xtext@example.com as MemberRole.owner>] - >>> sorted(mlist.members.members) - [ on - _xtext@example.com as MemberRole.member>] - >>> sorted(mlist.regular_members.members) - [ 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 = user_manager.create_address( - ... 'FPERSON@example.com', '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 ' - >>> repr(address_6) - ' [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. - - >>> print address_6.address - fperson@example.com - >>> print address_6.original_address - FPERSON@example.com - -Because addresses are case-insensitive for all other purposes, you cannot -create an address that differs only in case. - - >>> user_manager.create_address('fperson@example.com') - Traceback (most recent call last): - ... - ExistingAddressError: FPERSON@example.com - >>> user_manager.create_address('fperson@EXAMPLE.COM') - Traceback (most recent call last): - ... - ExistingAddressError: FPERSON@example.com - >>> user_manager.create_address('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. - - >>> print user_manager.get_address('fperson@example.com').address - fperson@example.com - >>> print user_manager.get_address('FPERSON@example.com').address - fperson@example.com diff --git a/src/mailman/docs/autorespond.txt b/src/mailman/docs/autorespond.txt deleted file mode 100644 index 5d37927fa..000000000 --- a/src/mailman/docs/autorespond.txt +++ /dev/null @@ -1,111 +0,0 @@ -Automatic responder -=================== - -In various situations, Mailman will send an automatic response to the author -of an email message. For example, if someone sends a command to the -request -address, Mailman will send a response, but to cut down on third party spam, -the sender will only get a certain number of responses per day. - -First, given a mailing list you need to adapt it to an IAutoResponseSet. - - >>> mlist = create_list('test@example.com') - >>> from mailman.interfaces.autorespond import IAutoResponseSet - >>> response_set = IAutoResponseSet(mlist) - - >>> from zope.interface.verify import verifyObject - >>> verifyObject(IAutoResponseSet, response_set) - True - -You can't adapt other objects to an IAutoResponseSet. - - >>> IAutoResponseSet(object()) - Traceback (most recent call last): - ... - TypeError: ('Could not adapt', ... - -There are various kinds of response types. For example, Mailman will send an -automatic response when messages are held for approval, or when it receives an -email command. You can find out how many responses for a particular address -have already been sent today. - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> address = getUtility(IUserManager).create_address( - ... 'aperson@example.com') - - >>> from mailman.interfaces.autorespond import Response - >>> response_set.todays_count(address, Response.hold) - 0 - >>> response_set.todays_count(address, Response.command) - 0 - -Using the response set, we can record that a hold response is sent to the -address. - - >>> response_set.response_sent(address, Response.hold) - >>> response_set.todays_count(address, Response.hold) - 1 - >>> response_set.todays_count(address, Response.command) - 0 - -We can also record that a command response was sent. - - >>> response_set.response_sent(address, Response.command) - >>> response_set.todays_count(address, Response.hold) - 1 - >>> response_set.todays_count(address, Response.command) - 1 - -Let's send one more. - - >>> response_set.response_sent(address, Response.command) - >>> response_set.todays_count(address, Response.hold) - 1 - >>> response_set.todays_count(address, Response.command) - 2 - -Now the day flips over and all the counts reset. - - >>> from mailman.utilities.datetime import factory - >>> factory.fast_forward() - - >>> response_set.todays_count(address, Response.hold) - 0 - >>> response_set.todays_count(address, Response.command) - 0 - - -Response dates --------------- - -You can also use the response set to get the date of the last response sent. - - >>> response = response_set.last_response(address, Response.hold) - >>> response.mailing_list - - >>> response.address - - >>> response.response_type - - >>> response.date_sent - datetime.date(2005, 8, 1) - -When another response is sent today, that becomes the last one sent. - - >>> response_set.response_sent(address, Response.command) - >>> response_set.last_response(address, Response.command).date_sent - datetime.date(2005, 8, 2) - - >>> factory.fast_forward(days=3) - >>> response_set.response_sent(address, Response.command) - >>> response_set.last_response(address, Response.command).date_sent - datetime.date(2005, 8, 5) - -If there's been no response sent to a particular address, None is returned. - - >>> address = getUtility(IUserManager).create_address( - ... 'bperson@example.com') - >>> response_set.todays_count(address, Response.command) - 0 - >>> print response_set.last_response(address, Response.command) - None diff --git a/src/mailman/docs/bounces.txt b/src/mailman/docs/bounces.txt deleted file mode 100644 index 736eda19d..000000000 --- a/src/mailman/docs/bounces.txt +++ /dev/null @@ -1,109 +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. - - >>> from mailman.interfaces.listmanager import IListManager - >>> from zope.component import getUtility - >>> mlist = getUtility(IListManager).create('_xtest@example.com') - >>> mlist.preferred_language = '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 - - --... - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - - [No bounce details are available] - --... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - To: _xtest@example.com - From: aperson@example.com - Subject: Something important - - I sometimes say something important. - - --...-- - -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 - - --... - Content-Type: text/plain; charset="us-ascii" - MIME-Version: 1.0 - Content-Transfer-Encoding: 7bit - - This wasn't very important after all. - --... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - To: _xtest@example.com - From: aperson@example.com - Subject: Something important - - I sometimes say something important. - - --...-- diff --git a/src/mailman/docs/chains.txt b/src/mailman/docs/chains.txt deleted file mode 100644 index f9ed156b1..000000000 --- a/src/mailman/docs/chains.txt +++ /dev/null @@ -1,354 +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. - - >>> mlist = create_list('_xtest@example.com') - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: _xtest@example.com - ... Subject: My first post - ... Message-ID: - ... - ... 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: - - - -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: - -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 - - From: aperson@example.com - To: _xtest@example.com - Subject: My first post - Message-ID: - - An important message. - - ... - - -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=: n/a - - -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: - - List: _xtest@example.com - From: aperson@example.com - Subject: My first post - Reason: XXX - - At your convenience, visit: - - http://lists.example.com/admindb/_xtest@example.com - - to approve or deny the request. - - ... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - From: aperson@example.com - To: _xtest@example.com - Subject: My first post - Message-ID: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - - An important message. - - ... - Content-Type: message/rfc822 - MIME-Version: 1.0 - - 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 - ... - - 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 - - My first post - - Is being held until the list moderator can review it for approval. - - The reason it is being held: - - XXX - - 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: - - http://lists.example.com/confirm/_xtest@example.com/... - - - -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.*)$', line) - ... if mo: - ... cookie = mo.group('cookie') - ... break - >>> assert cookie is not None, 'No confirmation token found' - - >>> from mailman.interfaces.pending import IPendings - >>> from zope.component import getUtility - - >>> data = getUtility(IPendings).confirm(cookie) - >>> sorted(data.items()) - [(u'id', ...), (u'type', u'held message')] - -The message itself is held in the message store. - - >>> from mailman.interfaces.requests import IRequests - >>> list_requests = getUtility(IRequests).get_list_requests(mlist) - >>> rkey, rdata = list_requests.get_request(data['id']) - - >>> from mailman.interfaces.messages import IMessageStore - >>> from zope.component import getUtility - >>> msg = getUtility(IMessageStore).get_message_by_id( - ... rdata['_mod_message_id']) - - >>> print msg.as_string() - From: aperson@example.com - To: _xtest@example.com - Subject: My first post - Message-ID: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - - An important message. - - - -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: - - >>> 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: - X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW - - An important message. - - - -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: - - >>> 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: - 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 - - An important message. - - -In addition, the message metadata now contains lists of all rules that have -hit and all rules that have missed. - - >>> sorted(qdata['rule_hits']) - [] - >>> for rule_name in sorted(qdata['rule_misses']): - ... print rule_name - administrivia - approved - emergency - implicit-dest - loop - max-recipients - max-size - news-moderation - no-subject - suspicious-header diff --git a/src/mailman/docs/domains.txt b/src/mailman/docs/domains.txt deleted file mode 100644 index 5673e6ee9..000000000 --- a/src/mailman/docs/domains.txt +++ /dev/null @@ -1,119 +0,0 @@ -======= -Domains -======= - - # The test framework starts out with an example domain, so let's delete - # that first. - >>> from mailman.interfaces.domain import IDomainManager - >>> from zope.component import getUtility - >>> manager = getUtility(IDomainManager) - >>> manager.remove('example.com') - - -Domains are how Mailman interacts with email host names and web host names. - - >>> from operator import attrgetter - >>> def show_domains(): - ... if len(manager) == 0: - ... print 'no domains' - ... return - ... for domain in sorted(manager, key=attrgetter('email_host')): - ... print domain - - >>> show_domains() - no domains - -Adding a domain requires some basic information, of which the email host name -is the only required piece. The other parts are inferred from that. - - >>> manager.add('example.org') - - >>> show_domains() - - -We can remove domains too. - - >>> manager.remove('example.org') - - >>> show_domains() - no domains - -Sometimes the email host name is different than the base url for hitting the -web interface for the domain. - - >>> manager.add('example.com', base_url='https://mail.example.com') - - >>> show_domains() - - -Domains can have explicit descriptions and contact addresses. - - >>> manager.add( - ... 'example.net', - ... base_url='http://lists.example.net', - ... contact_address='postmaster@example.com', - ... description='The example domain') - - - >>> show_domains() - - - -In the global domain manager, domains are indexed by their email host name. - - >>> for domain in sorted(manager, key=attrgetter('email_host')): - ... print domain.email_host - example.com - example.net - - >>> print manager['example.net'] - - - >>> print manager['doesnotexist.com'] - Traceback (most recent call last): - ... - KeyError: u'doesnotexist.com' - -As with a dictionary, you can also get the domain. If the domain does not -exist, None or a default is returned. - - >>> print manager.get('example.net') - - - >>> print manager.get('doesnotexist.com') - None - - >>> print manager.get('doesnotexist.com', 'blahdeblah') - blahdeblah - -Non-existent domains cannot be removed. - - >>> manager.remove('doesnotexist.com') - Traceback (most recent call last): - ... - KeyError: u'doesnotexist.com' - - -Confirmation tokens -=================== - -Confirmation tokens can be added to the domain's url to generate the URL to a -page users can use to confirm their subscriptions. - - >>> domain = manager['example.net'] - >>> print domain.confirm_url('abc') - http://lists.example.net/confirm/abc diff --git a/src/mailman/docs/hooks.txt b/src/mailman/docs/hooks.txt deleted file mode 100644 index 14dc76667..000000000 --- a/src/mailman/docs/hooks.txt +++ /dev/null @@ -1,108 +0,0 @@ -===== -Hooks -===== - -Mailman defines two initialization hooks, one which is run early in the -initialization process and the other run late in the initialization process. -Hooks name an importable callable so it must be accessible on sys.path. - - >>> import os, sys - >>> from mailman.config import config - >>> config_directory = os.path.dirname(config.filename) - >>> sys.path.insert(0, config_directory) - - >>> hook_path = os.path.join(config_directory, 'hooks.py') - >>> with open(hook_path, 'w') as fp: - ... print >> fp, """\ - ... counter = 1 - ... def pre_hook(): - ... global counter - ... print 'pre-hook:', counter - ... counter += 1 - ... - ... def post_hook(): - ... global counter - ... print 'post-hook:', counter - ... counter += 1 - ... """ - >>> fp.close() - - -Pre-hook -======== - -We can set the pre-hook in the configuration file. - - >>> config_path = os.path.join(config_directory, 'hooks.cfg') - >>> with open(config_path, 'w') as fp: - ... print >> fp, """\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... pre_hook: hooks.pre_hook - ... """ - -The hooks are run in the second and third steps of initialization. However, -we can't run those initialization steps in process, so call a command line -script that will produce no output to force the hooks to run. - - >>> import subprocess - >>> def call(): - ... proc = subprocess.Popen( - ... 'bin/mailman lists --domain ignore -q'.split(), - ... cwd='../..', # testrunner runs from ./parts/test - ... env=dict(MAILMAN_CONFIG_FILE=config_path, - ... PYTHONPATH=config_directory), - ... stdout=subprocess.PIPE, stderr=subprocess.PIPE) - ... stdout, stderr = proc.communicate() - ... assert proc.returncode == 0, stderr - ... print stdout - - >>> call() - pre-hook: 1 - - - >>> os.remove(config_path) - - -Post-hook -========= - -We can set the post-hook in the configuration file. - - >>> with open(config_path, 'w') as fp: - ... print >> fp, """\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... post_hook: hooks.post_hook - ... """ - - >>> call() - post-hook: 1 - - - >>> os.remove(config_path) - - -Running both hooks -================== - -We can set the pre- and post-hooks in the configuration file. - - >>> with open(config_path, 'w') as fp: - ... print >> fp, """\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... pre_hook: hooks.pre_hook - ... post_hook: hooks.post_hook - ... """ - - >>> call() - pre-hook: 1 - post-hook: 2 - diff --git a/src/mailman/docs/languages.txt b/src/mailman/docs/languages.txt deleted file mode 100644 index a724a0510..000000000 --- a/src/mailman/docs/languages.txt +++ /dev/null @@ -1,110 +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 mailman.interfaces.languages import ILanguageManager - >>> from zope.component import getUtility - >>> from zope.interface.verify import verifyObject - - >>> mgr = getUtility(ILanguageManager) - >>> verifyObject(ILanguageManager, mgr) - True - - # The language manager component comes pre-populated; clear it out. - >>> mgr.clear() - -A language manager keeps track of the languages it knows about. - - >>> list(mgr.codes) - [] - >>> list(mgr.languages) - [] - - -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('en', 'us-ascii', 'English') - >>> mgr.add('it', 'iso-8859-1', 'Italian') - -And you can get information for all known languages. - - >>> print mgr['en'].description - English - >>> print mgr['en'].charset - us-ascii - >>> print mgr['it'].description - Italian - >>> print mgr['it'].charset - iso-8859-1 - - -Other iterations -================ - -You can iterate over all the known language codes. - - >>> mgr.add('pl', 'iso-8859-2', 'Polish') - >>> sorted(mgr.codes) - [u'en', u'it', u'pl'] - -You can iterate over all the known languages. - - >>> from operator import attrgetter - >>> languages = sorted((language for language in mgr.languages), - ... key=attrgetter('code')) - >>> for language in languages: - ... print language.code, language.charset, language.description - en us-ascii English - it iso-8859-1 Italian - pl iso-8859-2 Polish - -You can ask whether a particular language code is known. - - >>> 'it' in mgr - True - >>> 'xx' in mgr - False - -You can get a particular language by its code. - - >>> print mgr['it'].description - Italian - >>> print mgr['xx'].code - Traceback (most recent call last): - ... - KeyError: u'xx' - >>> print mgr.get('it').description - Italian - >>> print mgr.get('xx') - None - >>> print mgr.get('xx', 'missing') - missing - - -Clearing the known languages -============================ - -The language manager can forget about all the language codes it knows about. - - >>> 'en' in mgr - True - - # Make a copy of the language manager's dictionary, so we can restore it - # after the test. Currently the test layer doesn't manage this. - >>> saved = mgr._languages.copy() - - >>> mgr.clear() - >>> 'en' in mgr - False - - # Restore the data. - >>> mgr._languages = saved diff --git a/src/mailman/docs/lifecycle.txt b/src/mailman/docs/lifecycle.txt deleted file mode 100644 index a1cd50825..000000000 --- a/src/mailman/docs/lifecycle.txt +++ /dev/null @@ -1,143 +0,0 @@ -================================= -Application level list life cycle -================================= - -The low-level way to create and delete a mailing list is to use the -IListManager interface. This interface simply adds or removes the appropriate -database entries to record the list's creation. - -There is a higher level interface for creating and deleting mailing lists -which performs additional tasks such as: - - * validating the list's posting address (which also serves as the list's - fully qualified name); - * ensuring that the list's domain is registered; - * applying all matching styles to the new list; - * creating and assigning list owners; - * notifying watchers of list creation; - * creating ancillary artifacts (such as the list's on-disk directory) - - >>> from mailman.app.lifecycle import create_list - - -Posting address validation -========================== - -If you try to use the higher-level interface to create a mailing list with a -bogus posting address, you get an exception. - - >>> create_list('not a valid address') - Traceback (most recent call last): - ... - InvalidEmailAddressError: u'not a valid address' - -If the posting address is valid, but the domain has not been registered with -Mailman yet, you get an exception. - - >>> create_list('test@example.org') - Traceback (most recent call last): - ... - BadDomainSpecificationError: example.org - - -Creating a list applies its styles -================================== - -Start by registering a test style. - - >>> from zope.interface import implements - >>> from mailman.interfaces.styles import IStyle - >>> class TestStyle(object): - ... implements(IStyle) - ... name = 'test' - ... priority = 10 - ... def apply(self, mailing_list): - ... # Just does something very simple. - ... mailing_list.msg_footer = 'test footer' - ... def match(self, mailing_list, styles): - ... # Applies to any test list - ... if 'test' in mailing_list.fqdn_listname: - ... styles.append(self) - - >>> config.style_manager.register(TestStyle()) - -Using the higher level interface for creating a list, applies all matching -list styles. - - >>> mlist_1 = create_list('test_1@example.com') - >>> print mlist_1.fqdn_listname - test_1@example.com - >>> print mlist_1.msg_footer - test footer - - -Creating a list with owners -=========================== - -You can also specify a list of owner email addresses. If these addresses are -not yet known, they will be registered, and new users will be linked to them. -However the addresses are not verified. - - >>> owners = ['aperson@example.com', 'bperson@example.com', - ... 'cperson@example.com', 'dperson@example.com'] - >>> mlist_2 = create_list('test_2@example.com', owners) - >>> print mlist_2.fqdn_listname - test_2@example.com - >>> print mlist_2.msg_footer - test footer - >>> sorted(addr.address for addr in mlist_2.owners.addresses) - [u'aperson@example.com', u'bperson@example.com', - u'cperson@example.com', u'dperson@example.com'] - -None of the owner addresses are verified. - - >>> any(addr.verified_on is not None for addr in mlist_2.owners.addresses) - False - -However, all addresses are linked to users. - - >>> # The owners have no names yet - >>> len(list(mlist_2.owners.users)) - 4 - -If you create a mailing list with owner addresses that are already known to -the system, they won't be created again. - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - - >>> user_a = user_manager.get_user('aperson@example.com') - >>> user_b = user_manager.get_user('bperson@example.com') - >>> user_c = user_manager.get_user('cperson@example.com') - >>> user_d = user_manager.get_user('dperson@example.com') - >>> user_a.real_name = 'Anne Person' - >>> user_b.real_name = 'Bart Person' - >>> user_c.real_name = 'Caty Person' - >>> user_d.real_name = 'Dirk Person' - - >>> mlist_3 = create_list('test_3@example.com', owners) - >>> sorted(user.real_name for user in mlist_3.owners.users) - [u'Anne Person', u'Bart Person', u'Caty Person', u'Dirk Person'] - - -Removing a list -=============== - -Removing a mailing list deletes the list, all its subscribers, and any related -artifacts. - - >>> from mailman.app.lifecycle import remove_list - >>> remove_list(mlist_2.fqdn_listname, mlist_2, True) - - >>> from mailman.interfaces.listmanager import IListManager - >>> from zope.component import getUtility - >>> print getUtility(IListManager).get('test_2@example.com') - None - -We should now be able to completely recreate the mailing list. - - >>> mlist_2a = create_list('test_2@example.com', owners) - >>> sorted(addr.address for addr in mlist_2a.owners.addresses) - [u'aperson@example.com', u'bperson@example.com', - u'cperson@example.com', u'dperson@example.com'] diff --git a/src/mailman/docs/listmanager.txt b/src/mailman/docs/listmanager.txt deleted file mode 100644 index e07659066..000000000 --- a/src/mailman/docs/listmanager.txt +++ /dev/null @@ -1,101 +0,0 @@ -======================== -The mailing list manager -======================== - -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 - >>> from zope.component import getUtility - >>> list_manager = getUtility(IListManager) - - -Creating a mailing list -======================= - -Creating the list returns the newly created IMailList object. - - >>> from mailman.interfaces.mailinglist import IMailingList - >>> mlist = list_manager.create('_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. - - >>> print mlist.list_name - _xtest - >>> print mlist.host_name - example.com - >>> print mlist.fqdn_listname - _xtest@example.com - -If you try to create a mailing list with the same name as an existing list, -you will get an exception. - - >>> list_manager.create('_xtest@example.com') - Traceback (most recent call last): - ... - ListAlreadyExistsError: _xtest@example.com - -It is an error to create a mailing list that isn't a fully qualified list name -(i.e. posting address). - - >>> list_manager.create('foo') - Traceback (most recent call last): - ... - InvalidEmailAddressError: foo - - -Deleting a mailing list -======================= - -Use the list manager to delete a mailing list. - - >>> list_manager.delete(mlist) - >>> sorted(list_manager.names) - [] - -After deleting the list, you can create it again. - - >>> mlist = list_manager.create('_xtest@example.com') - >>> print mlist.fqdn_listname - _xtest@example.com - - -Retrieving a mailing list -========================= - -When a mailing list exists, you can ask the list manager for it and you will -always get the same object back. - - >>> mlist_2 = list_manager.get('_xtest@example.com') - >>> mlist_2 is mlist - True - -If you try to get a list that doesn't existing yet, you get None. - - >>> print list_manager.get('_xtest_2@example.com') - None - -You also get None if the list name is invalid. - - >>> print list_manager.get('foo') - 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 = list_manager.create('_xtest_3@example.com') - >>> mlist_4 = list_manager.create('_xtest_4@example.com') - >>> sorted(list_manager.names) - [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] - >>> sorted(m.fqdn_listname for m in list_manager.mailing_lists) - [u'_xtest@example.com', u'_xtest_3@example.com', u'_xtest_4@example.com'] diff --git a/src/mailman/docs/membership.txt b/src/mailman/docs/membership.txt deleted file mode 100644 index 27d2d9552..000000000 --- a/src/mailman/docs/membership.txt +++ /dev/null @@ -1,234 +0,0 @@ -================ -List memberships -================ - -Users represent people in Mailman. Users control email addresses, and rosters -are collections 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 = create_list('_xtest@example.com') - >>> mlist - - >>> 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. - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - >>> user_1 = user_manager.create_user('aperson@example.com', 'Anne Person') - >>> print user_1.real_name - 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] - >>> print address_1.address - aperson@example.com - >>> address_1.subscribe(mlist, MemberRole.owner) - 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 = user_manager.create_user('bperson@example.com', 'Ben Person') - >>> print user_2.real_name - Ben Person - >>> address_2 = list(user_2.addresses)[0] - >>> print address_2.address - bperson@example.com - >>> address_2.subscribe(mlist, MemberRole.moderator) - - 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 = user_manager.create_user( - ... 'cperson@example.com', 'Claire Person') - >>> print user_3.real_name - Claire Person - >>> address_3 = list(user_3.addresses)[0] - >>> print address_3.address - cperson@example.com - >>> address_3.subscribe(mlist, MemberRole.member) - - 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) - [ on - _xtest@example.com as MemberRole.member>, - 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('aperson@example.com') - on - _xtest@example.com as MemberRole.owner> - >>> mlist.administrators.get_member('aperson@example.com') - on - _xtest@example.com as MemberRole.owner> - >>> mlist.members.get_member('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('zperson@example.com') - None - >>> print mlist.moderators.get_member('aperson@example.com') - None - >>> print mlist.members.get_member('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/src/mailman/docs/message.txt b/src/mailman/docs/message.txt deleted file mode 100644 index 41607ff44..000000000 --- a/src/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 = create_list('_xtest@example.com') - -The UserNotification constructor takes the recipient address, the sender -address, an optional subject, optional body text, and optional language. - - >>> from mailman.email.message import UserNotification - >>> msg = UserNotification( - ... 'aperson@example.com', - ... '_xtest@example.com', - ... 'Something you need to know', - ... 'I needed to tell you this.') - >>> msg.send(mlist) - -The message will end up in the virgin queue. - - >>> switchboard = config.switchboards['virgin'] - >>> len(switchboard.files) - 1 - >>> filebase = switchboard.files[0] - >>> qmsg, qmsgdata = switchboard.dequeue(filebase) - >>> switchboard.finish(filebase) - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Something you need to know - From: _xtest@example.com - To: aperson@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - I needed to tell you this. diff --git a/src/mailman/docs/messagestore.txt b/src/mailman/docs/messagestore.txt deleted file mode 100644 index aabfd55fb..000000000 --- a/src/mailman/docs/messagestore.txt +++ /dev/null @@ -1,116 +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. - - >>> from mailman.interfaces.messages import IMessageStore - >>> from zope.component import getUtility - >>> message_store = getUtility(IMessageStore) - -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. - ... """) - >>> message_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>' - >>> message_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 - - This message is very important. - - - -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 message_store.get_message_by_id('nothing') - None - >>> print message_store.get_message_by_hash('nothing') - None - -Given an existing Message-ID, the message can be found. - - >>> message = 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 - - This message is very important. - - -Similarly, we can find messages by the X-Message-ID-Hash: - - >>> message = 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 - - This message is very important. - - - -Iterating over all messages -=========================== - -The message store provides a means to iterate over all the messages it -contains. - - >>> messages = list(message_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 - - This message is very important. - - - -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. - - >>> message_store.delete_message('nothing') - Traceback (most recent call last): - ... - LookupError: nothing - -But if you delete an existing message, it really gets deleted. - - >>> message_id = message['message-id'] - >>> message_store.delete_message(message_id) - >>> list(message_store.messages) - [] - >>> print message_store.get_message_by_id(message_id) - None - >>> print message_store.get_message_by_hash(message['x-message-id-hash']) - None diff --git a/src/mailman/docs/mlist-addresses.txt b/src/mailman/docs/mlist-addresses.txt deleted file mode 100644 index 3f44008fb..000000000 --- a/src/mailman/docs/mlist-addresses.txt +++ /dev/null @@ -1,77 +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 = create_list('_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. - - >>> print mlist.fqdn_listname - _xtest@example.com - >>> print mlist.posting_address - _xtest@example.com - -Messages to the mailing list's 'no reply' address always get discarded without -prejudice. - - >>> print mlist.no_reply_address - noreply@example.com - -The mailing list's owner address reaches the human moderators. - - >>> print mlist.owner_address - _xtest-owner@example.com - -The request address goes to the list's email command robot. - - >>> print mlist.request_address - _xtest-request@example.com - -The bounces address accepts and processes all potential bounces. - - >>> print mlist.bounces_address - _xtest-bounces@example.com - -The join (a.k.a. subscribe) address is where someone can email to get added to -the mailing list. The subscribe alias is a synonym for join, but it's -deprecated. - - >>> print mlist.join_address - _xtest-join@example.com - >>> print mlist.subscribe_address - _xtest-subscribe@example.com - -The leave (a.k.a. unsubscribe) address is where someone can email to get added -to the mailing list. The unsubscribe alias is a synonym for leave, but it's -deprecated. - - >>> print mlist.leave_address - _xtest-leave@example.com - >>> print mlist.unsubscribe_address - _xtest-unsubscribe@example.com - - -Email confirmations -=================== - -Email confirmation messages are sent when actions such as subscriptions need -to be confirmed. It requires that a cookie be provided, which will be -included in the local part of the email address. The exact format of this is -dependent on the VERP_CONFIRM_FORMAT configuration variable. - - >>> print mlist.confirm_address('cookie') - _xtest-confirm+cookie@example.com - >>> print mlist.confirm_address('wookie') - _xtest-confirm+wookie@example.com - - >>> config.push('test config', """ - ... [mta] - ... verp_confirm_format: $address---$cookie - ... """) - >>> print mlist.confirm_address('cookie') - _xtest-confirm---cookie@example.com - >>> config.pop('test config') diff --git a/src/mailman/docs/pending.txt b/src/mailman/docs/pending.txt deleted file mode 100644 index bc6d3c470..000000000 --- a/src/mailman/docs/pending.txt +++ /dev/null @@ -1,93 +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 - >>> from zope.component import getUtility - >>> pendingdb = getUtility(IPendings) - -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(bytes('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/src/mailman/docs/pipelines.txt b/src/mailman/docs/pipelines.txt deleted file mode 100644 index cf848f1d9..000000000 --- a/src/mailman/docs/pipelines.txt +++ /dev/null @@ -1,193 +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. - - >>> mlist = create_list('xtest@example.com') - >>> print mlist.pipeline - built-in - >>> from mailman.core.pipelines import process - - -Processing a message -==================== - -Messages hit the pipeline after they've been accepted for posting. - - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... To: xtest@example.com - ... Subject: My first post - ... Message-ID: - ... - ... First 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: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - -And the message metadata has information about recipients and other stuff. -However there are currently no recipients for this message. - - >>> dump_msgdata(msgdata) - original_sender : aperson@example.com - origsubj : My first post - recipients : set([]) - stripped_subject: My first post - -And the message is now sitting in various other processing queues. - - >>> from mailman.testing.helpers import get_queue_messages - >>> messages = get_queue_messages('archive') - >>> len(messages) - 1 - >>> print messages[0].msg.as_string() - From: aperson@example.com - To: xtest@example.com - Message-ID: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - >>> dump_msgdata(messages[0].msgdata) - _parsemsg : False - original_sender : aperson@example.com - origsubj : My first post - recipients : set([]) - stripped_subject: My first post - version : 3 - -This mailing list is not linked to an NNTP newsgroup, so there's nothing in -the outgoing nntp queue. - - >>> messages = get_queue_messages('news') - >>> len(messages) - 0 - -This is the message that will actually get delivered to end recipients. - - >>> messages = get_queue_messages('out') - >>> len(messages) - 1 - >>> print messages[0].msg.as_string() - From: aperson@example.com - To: xtest@example.com - Message-ID: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - >>> dump_msgdata(messages[0].msgdata) - _parsemsg : False - listname : xtest@example.com - original_sender : aperson@example.com - origsubj : My first post - recipients : set([]) - stripped_subject: My first post - version : 3 - -There's now one message in the digest mailbox, getting ready to be sent. - - >>> from mailman.testing.helpers import digest_mbox - >>> digest = digest_mbox(mlist) - >>> sum(1 for mboxmsg in digest) - 1 - >>> print list(digest)[0].as_string() - From: aperson@example.com - To: xtest@example.com - Message-ID: - Subject: [Xtest] My first post - X-BeenThere: xtest@example.com - X-Mailman-Version: ... - Precedence: list - List-Id: - X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Post: - List-Subscribe: - , - - Archived-At: - http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB - List-Unsubscribe: - , - - List-Archive: - List-Help: - - First post! - - - - -Clean up the digests -==================== - - >>> digest.clear() - >>> digest.flush() - >>> sum(1 for msg in digest_mbox(mlist)) - 0 diff --git a/src/mailman/docs/registration.txt b/src/mailman/docs/registration.txt deleted file mode 100644 index abc7f2c93..000000000 --- a/src/mailman/docs/registration.txt +++ /dev/null @@ -1,350 +0,0 @@ -==================== -Address registration -==================== - -Before users can join a mailing list, they 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. - -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. - - >>> from mailman.interfaces.registrar import IRegistrar - >>> from zope.component import getUtility - >>> registrar = getUtility(IRegistrar) - -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://lists.example.com/confirm/(.*)') - >>> def extract_token(msg): - ... mo = cre.search(msg.get_payload()) - ... return mo.group(1) - - -Invalid email addresses -======================= - -Addresses are registered within the context of a mailing list, mostly so that -confirmation emails can come from some place. You also need the email -address of the user who is registering. - - >>> mlist = create_list('alpha@example.com') - -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(mlist, '') - Traceback (most recent call last): - ... - InvalidEmailAddressError: u'' - >>> registrar.register(mlist, 'some name@example.com') - Traceback (most recent call last): - ... - InvalidEmailAddressError: u'some name@example.com' - >>> registrar.register(mlist, '