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