diff options
| author | Barry Warsaw | 2008-07-02 22:29:20 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2008-07-02 22:29:20 -0400 |
| commit | ae08b9bd032410014124c0885e2ed4b7b9cb4591 (patch) | |
| tree | a11750fd86ee00175a92ce2e877dfa56213851bf | |
| parent | ee349897c99a75da4da4698c64bee8cd0d743d97 (diff) | |
| download | mailman-ae08b9bd032410014124c0885e2ed4b7b9cb4591.tar.gz mailman-ae08b9bd032410014124c0885e2ed4b7b9cb4591.tar.zst mailman-ae08b9bd032410014124c0885e2ed4b7b9cb4591.zip | |
Implement a prototypical archiver that supports Archived-At permalink header,
using the current concept of the hash. This can change, but at least now I
have the interfaces and infrastructure to support this header. Of course,
Pipermail doesn't support a permalink, so that archiver no-ops.
Add an adapter to provide the interface that Pipermail requires over and above
the IMailingList interface. Add an is_enabled flag to IArchiver.
| -rw-r--r-- | mailman/app/archiving.py | 69 | ||||
| -rw-r--r-- | mailman/docs/archivers.txt | 63 | ||||
| -rw-r--r-- | mailman/interfaces/archiver.py | 22 | ||||
| -rw-r--r-- | mailman/pipeline/cook_headers.py | 2 | ||||
| -rw-r--r-- | mailman/pipeline/docs/cook-headers.txt | 1 | ||||
| -rw-r--r-- | setup.py | 5 |
6 files changed, 157 insertions, 5 deletions
diff --git a/mailman/app/archiving.py b/mailman/app/archiving.py index 5a752063d..15e987daf 100644 --- a/mailman/app/archiving.py +++ b/mailman/app/archiving.py @@ -20,25 +20,34 @@ __metaclass__ = type __all__ = [ 'Pipermail', + 'Prototype', ] import os +import hashlib +from base64 import b32encode +from cStringIO import StringIO +from email.utils import make_msgid from string import Template +from urlparse import urljoin from zope.interface import implements +from zope.interface.interface import adapter_hooks from mailman.configuration import config -from mailman.interfaces import IArchiver +from mailman.interfaces.archiver import IArchiver, IPipermailMailingList +from mailman.interfaces.mailinglist import IMailingList from mailman.Archiver.HyperArch import HyperArchive -from cStringIO import StringIO class PipermailMailingListAdapter: """An adapter for MailingList objects to work with Pipermail.""" + implements(IPipermailMailingList) + def __init__(self, mlist): self._mlist = mlist @@ -46,7 +55,7 @@ class PipermailMailingListAdapter: return getattr(self._mlist, name) def archive_dir(self): - """The directory for storing Pipermail artifacts.""" + """See `IPipermailMailingList`.""" if self._mlist.archive_private: basedir = config.PRIVATE_ARCHIVE_FILE_DIR else: @@ -54,12 +63,24 @@ class PipermailMailingListAdapter: return os.path.join(basedir, self._mlist.fqdn_listname) +def adapt_mailing_list_for_pipermail(iface, obj): + """Adapt IMailingLists to IPipermailMailingList.""" + if IMailingList.providedBy(obj) and iface is IPipermailMailingList: + return PipermailMailingListAdapter(obj) + return None + +adapter_hooks.append(adapt_mailing_list_for_pipermail) + + class Pipermail: """The stock Pipermail archiver.""" implements(IArchiver) + name = 'pipermail' + is_enabled = True + @staticmethod def list_url(mlist): """See `IArchiver`.""" @@ -85,9 +106,49 @@ class Pipermail: """See `IArchiver`.""" text = str(message) fileobj = StringIO(text) - h = HyperArchive(PipermailMailingListAdapter(mlist)) + h = HyperArchive(IPipermailMailingList(mlist)) h.processUnixMailbox(fileobj) h.close() fileobj.close() # There's no good way to know the url for the archived message. return None + + + +class Prototype: + """A prototype of a third party archiver. + + Mailman proposes a draft specification for interoperability between list + servers and archivers: <http://wiki.list.org/display/DEV/Stable+URLs>. + """ + + implements(IArchiver) + + name = 'prototype' + is_enabled = False + + @staticmethod + def list_url(mlist): + """See `IArchiver`.""" + web_host = config.domains.get(mlist.host_name, mlist.host_name) + return 'http://' + web_host + + @staticmethod + def permalink(mlist, msg): + """See `IArchiver`.""" + message_id = msg.get('message-id') + # It is not the archiver's job to ensure the message has a Message-ID. + assert message_id is not None, 'No Message-ID found' + # The angle brackets are not part of the Message-ID. See RFC 2822. + if message_id.startswith('<') and message_id.endswith('>'): + message_id = message_id[1:-1] + digest = hashlib.sha1(message_id).digest() + message_id_hash = b32encode(digest) + del msg['x-message-id-hash'] + msg['X-Message-ID-Hash'] = message_id_hash + return urljoin(Prototype.list_url(mlist), message_id_hash) + + @staticmethod + def archive_message(mlist, message): + """See `IArchiver`.""" + raise NotImplementedError diff --git a/mailman/docs/archivers.txt b/mailman/docs/archivers.txt new file mode 100644 index 000000000..9e4fbc121 --- /dev/null +++ b/mailman/docs/archivers.txt @@ -0,0 +1,63 @@ += Archivers = + +Mailman supports pluggable archivers, and it comes with several default +archivers. + + >>> from mailman.app.lifecycle import create_list + >>> mlist = create_list(u'test@example.com') + >>> msg = message_from_string("""\ + ... From: aperson@example.org + ... To: test@example.com + ... Subject: An archived message + ... Message-ID: <12345> + ... + ... Here is an archived message. + ... """) + +Archivers support an interface which provides the RFC 2369 List-Archive +header, and one that provides a 'permalink' to the specific message object in +the archive. This latter is appropriate for the message footer or for the RFC +5064 Archived-At header. + +Pipermail does not support a permalink, so that interface returns None. +Mailman defines a draft spec for how list servers and archivers can +interoperate. + + >>> from operator import attrgetter + >>> name = attrgetter('name') + >>> from mailman.app.plugins import get_plugins + >>> archivers = {} + >>> for archiver in sorted(get_plugins('mailman.archiver'), key=name): + ... print archiver.name + ... print ' ', archiver.list_url(mlist) + ... print ' ', archiver.permalink(mlist, msg) + ... archivers[archiver.name] = archiver + pipermail + http://www.example.com/pipermail/test@example.com + None + prototype + http://www.example.com + http://www.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE + + +== Sending the message to the archiver == + +The archiver is also able to archive the message. + + >>> mlist.web_page_url = u'http://lists.example.com/' + >>> archivers['pipermail'].archive_message(mlist, msg) + + >>> import os + >>> from mailman.interfaces.archiver import IPipermailMailingList + >>> pckpath = os.path.join( + ... IPipermailMailingList(mlist).archive_dir(), + ... 'pipermail.pck') + >>> os.path.exists(pckpath) + True + +Note however that the prototype archiver can't archive messages. + + >>> archivers['prototype'].archive_message(mlist, msg) + Traceback (most recent call last): + ... + NotImplementedError diff --git a/mailman/interfaces/archiver.py b/mailman/interfaces/archiver.py index 40b05b76c..ac6efcb93 100644 --- a/mailman/interfaces/archiver.py +++ b/mailman/interfaces/archiver.py @@ -17,13 +17,24 @@ """Interface for archiving schemes.""" +__metaclass__ = type +__all__ = [ + 'IArchiver', + 'IPipermailMailingList', + ] + from zope.interface import Interface, Attribute +from mailman.interfaces.mailinglist import IMailingList class IArchiver(Interface): """An interface to the archiver.""" + name = Attribute('The name of this archiver') + + is_enabled = Attribute('True if this archiver is enabled.') + def list_url(mlist): """Return the url to the top of the list's archive. @@ -53,3 +64,14 @@ class IArchiver(Interface): """ # XXX How to handle attachments? + + + +class IPipermailMailingList(IMailingList): + """An interface that adapts IMailingList as needed for Pipermail.""" + + def archive_dir(): + """The directory for storing Pipermail artifacts. + + Pipermail expects this to be a function, not a property. + """ diff --git a/mailman/pipeline/cook_headers.py b/mailman/pipeline/cook_headers.py index 4cda42c81..ad4728be8 100644 --- a/mailman/pipeline/cook_headers.py +++ b/mailman/pipeline/cook_headers.py @@ -215,6 +215,8 @@ def process(mlist, msg, msgdata): # Add RFC 2369 and 5064 archiving headers, if archiving is enabled. if mlist.archive: for archiver in get_plugins('mailman.archiver'): + if not archiver.is_enabled: + continue headers['List-Archive'] = '<%s>' % archiver.list_url(mlist) permalink = archiver.permalink(mlist, msg) if permalink is not None: diff --git a/mailman/pipeline/docs/cook-headers.txt b/mailman/pipeline/docs/cook-headers.txt index a85ba9e63..d764bd796 100644 --- a/mailman/pipeline/docs/cook-headers.txt +++ b/mailman/pipeline/docs/cook-headers.txt @@ -186,6 +186,7 @@ But normally, a list will include these headers. >>> mlist.preferred_language = u'en' >>> msg = message_from_string("""\ ... From: aperson@example.com + ... Message-ID: <12345> ... ... """) >>> process(mlist, msg, {}) @@ -90,7 +90,10 @@ Any other spelling is incorrect.""", entry_points = { 'console_scripts': list(scripts), # Entry point for plugging in different database backends. - 'mailman.archiver' : 'pipermail = mailman.app.archiving:Pipermail', + 'mailman.archiver' : [ + 'pipermail = mailman.app.archiving:Pipermail', + 'prototype = mailman.app.archiving:Prototype', + ], 'mailman.scrubber' : 'stock = mailman.app.archiving:Pipermail', 'mailman.commands' : list(commands), 'mailman.database' : 'stock = mailman.database:StockDatabase', |
