summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2011-11-06 18:20:08 -0500
committerBarry Warsaw2011-11-06 18:20:08 -0500
commitac49e4c485345058d2ee582b9966f441b2349738 (patch)
treeed6bbe0bf525058f00d83892b5caaae51e1ca258 /src
parentb475b1314386a574166d9c1d8095b363e7ceba9c (diff)
downloadmailman-ac49e4c485345058d2ee582b9966f441b2349738.tar.gz
mailman-ac49e4c485345058d2ee582b9966f441b2349738.tar.zst
mailman-ac49e4c485345058d2ee582b9966f441b2349738.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/docs/pipelines.rst79
-rw-r--r--src/mailman/database/sql/postgres.sql1
-rw-r--r--src/mailman/database/sql/sqlite.sql1
-rw-r--r--src/mailman/docs/NEWS.rst3
-rw-r--r--src/mailman/interfaces/mailinglist.py3
-rw-r--r--src/mailman/model/mailinglist.py1
-rw-r--r--src/mailman/pipeline/cook_headers.py70
-rw-r--r--src/mailman/pipeline/docs/cook-headers.rst209
-rw-r--r--src/mailman/pipeline/docs/rfc-2369.rst230
-rw-r--r--src/mailman/pipeline/rfc_2369.py95
-rw-r--r--src/mailman/rest/configuration.py1
-rw-r--r--src/mailman/rest/docs/configuration.rst1
-rw-r--r--src/mailman/testing/helpers.py7
13 files changed, 349 insertions, 352 deletions
diff --git a/src/mailman/app/docs/pipelines.rst b/src/mailman/app/docs/pipelines.rst
index cf848f1d9..599a8087c 100644
--- a/src/mailman/app/docs/pipelines.rst
+++ b/src/mailman/app/docs/pipelines.rst
@@ -9,7 +9,7 @@ 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')
+ >>> mlist = create_list('test@example.com')
>>> print mlist.pipeline
built-in
>>> from mailman.core.pipelines import process
@@ -22,7 +22,7 @@ Messages hit the pipeline after they've been accepted for posting.
>>> msg = message_from_string("""\
... From: aperson@example.com
- ... To: xtest@example.com
+ ... To: test@example.com
... Subject: My first post
... Message-ID: <first>
...
@@ -36,25 +36,11 @@ etc.
>>> print msg.as_string()
From: aperson@example.com
- To: xtest@example.com
+ To: test@example.com
Message-ID: <first>
- Subject: [Xtest] My first post
- X-BeenThere: xtest@example.com
+ Subject: [Test] My first post
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>
@@ -76,25 +62,11 @@ And the message is now sitting in various other processing queues.
1
>>> print messages[0].msg.as_string()
From: aperson@example.com
- To: xtest@example.com
+ To: test@example.com
Message-ID: <first>
- Subject: [Xtest] My first post
- X-BeenThere: xtest@example.com
+ Subject: [Test] My first post
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>
@@ -120,31 +92,17 @@ This is the message that will actually get delivered to end recipients.
1
>>> print messages[0].msg.as_string()
From: aperson@example.com
- To: xtest@example.com
+ To: test@example.com
Message-ID: <first>
- Subject: [Xtest] My first post
- X-BeenThere: xtest@example.com
+ Subject: [Test] My first post
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
+ listname : test@example.com
original_sender : aperson@example.com
origsubj : My first post
recipients : set([])
@@ -159,29 +117,14 @@ There's now one message in the digest mailbox, getting ready to be sent.
1
>>> print list(digest)[0].as_string()
From: aperson@example.com
- To: xtest@example.com
+ To: test@example.com
Message-ID: <first>
- Subject: [Xtest] My first post
- X-BeenThere: xtest@example.com
+ Subject: [Test] My first post
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
diff --git a/src/mailman/database/sql/postgres.sql b/src/mailman/database/sql/postgres.sql
index 2ca94217f..60c05e340 100644
--- a/src/mailman/database/sql/postgres.sql
+++ b/src/mailman/database/sql/postgres.sql
@@ -3,7 +3,6 @@ CREATE TABLE mailinglist (
-- List identity
list_name TEXT,
mail_host TEXT,
- list_id TEXT,
include_list_post_header BOOLEAN,
include_rfc2369_headers BOOLEAN,
-- Attributes not directly modifiable via the web u/i
diff --git a/src/mailman/database/sql/sqlite.sql b/src/mailman/database/sql/sqlite.sql
index 7368f2159..5987f1879 100644
--- a/src/mailman/database/sql/sqlite.sql
+++ b/src/mailman/database/sql/sqlite.sql
@@ -99,7 +99,6 @@ CREATE TABLE mailinglist (
-- List identity
list_name TEXT,
mail_host TEXT,
- list_id TEXT,
include_list_post_header BOOLEAN,
include_rfc2369_headers BOOLEAN,
-- Attributes not directly modifiable via the web u/i
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 226517b86..ae9f502ec 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -17,6 +17,9 @@ Architecture
* Implement the style manager as a utility instead of an attribute hanging
off the `mailman.config.config` object.
* PostgreSQL support contributed by Stephen A. Goss. (LP: #860159)
+ * Separate out the RFC 2369 header adding handler.
+ * Dynamically calculate the `List-Id` header instead of storing it in the
+ database. This means it cannot be changed.
Bug fixes
---------
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index 4ef35c115..d96e844bf 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -105,9 +105,6 @@ class IMailingList(Interface):
mailing lists, or in headers, and so forth. It should be as succinct
as you can get it, while still identifying what the list is.""")
- list_id = Attribute(
- """The RFC 2919 List-ID header value.""")
-
include_list_post_header = Attribute(
"""Flag specifying whether to include the RFC 2369 List-Post header.
This is usually set to True, except for announce-only lists.""")
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 9a332f48b..7e05e94ef 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -78,7 +78,6 @@ class MailingList(Model):
# List identity
list_name = Unicode()
mail_host = Unicode()
- list_id = Unicode()
include_list_post_header = Bool()
include_rfc2369_headers = Bool()
advertised = Bool()
diff --git a/src/mailman/pipeline/cook_headers.py b/src/mailman/pipeline/cook_headers.py
index 7e7b2078b..0afa52b0c 100644
--- a/src/mailman/pipeline/cook_headers.py
+++ b/src/mailman/pipeline/cook_headers.py
@@ -32,14 +32,12 @@ from email.header import Header, decode_header, make_header
from email.utils import parseaddr, formataddr, getaddresses
from zope.interface import implements
-from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
from mailman.version import VERSION
-CONTINUATION = ',\n\t'
COMMASPACE = ', '
MAXLINELEN = 78
@@ -87,9 +85,6 @@ def process(mlist, msg, msgdata):
# TK: Sometimes subject header is not MIME encoded for 8bit
# simply abort prefixing.
pass
- # Mark message so we know we've been here, but leave any existing
- # X-BeenThere's intact.
- msg['X-BeenThere'] = mlist.posting_address
# Add Precedence: and other useful headers. None of these are standard
# and finding information on some of them are fairly difficult. Some are
# just common practice, and we'll add more here as they become necessary.
@@ -173,75 +168,12 @@ def process(mlist, msg, msgdata):
add((str(i18ndesc), mlist.posting_address))
del msg['Cc']
msg['Cc'] = COMMASPACE.join([formataddr(pair) for pair in new])
- # Add list-specific headers as defined in RFC 2369 and RFC 2919, but only
- # if the message is being crafted for a specific list (e.g. not for the
- # password reminders).
- #
- # BAW: Some people really hate the List-* headers. It seems that the free
- # version of Eudora (possibly on for some platforms) does not hide these
- # headers by default, pissing off their users. Too bad. Fix the MUAs.
- if msgdata.get('_nolist') or not mlist.include_rfc2369_headers:
- return
- # This will act like an email address for purposes of formataddr()
- if mlist.description:
- # Don't wrap the header since here we just want to get it properly RFC
- # 2047 encoded.
- i18ndesc = uheader(mlist, mlist.description, 'List-Id', maxlinelen=998)
- listid_h = formataddr((str(i18ndesc), mlist.list_id))
- else:
- # without desc we need to ensure the MUST brackets
- listid_h = '<{0}>'.format(mlist.list_id)
- # No other agent should add a List-ID header except Mailman.
- del msg['list-id']
- msg['List-Id'] = listid_h
- # For internally crafted messages, we also add a (nonstandard),
- # "X-List-Administrivia: yes" header. For all others (i.e. those coming
- # from list posts), we add a bunch of other RFC 2369 headers.
- requestaddr = mlist.request_address
- subfieldfmt = '<{0}>, <mailto:{1}>'
- listinfo = mlist.script_url('listinfo')
- headers = {}
- # XXX reduced_list_headers used to suppress List-Help, List-Subject, and
- # List-Unsubscribe from UserNotification. That doesn't seem to make sense
- # any more, so always add those three headers (others will still be
- # suppressed).
- headers.update({
- 'List-Help' : '<mailto:{0}?subject=help>'.format(requestaddr),
- 'List-Unsubscribe': subfieldfmt.format(listinfo, mlist.leave_address),
- 'List-Subscribe' : subfieldfmt.format(listinfo, mlist.join_address),
- })
- if msgdata.get('reduced_list_headers'):
- headers['X-List-Administrivia'] = 'yes'
- else:
- # List-Post: is controlled by a separate attribute
- if mlist.include_list_post_header:
- headers['List-Post'] = '<mailto:{0}>'.format(mlist.posting_address)
- # Add RFC 2369 and 5064 archiving headers, if archiving is enabled.
- if mlist.archive:
- for archiver in config.archivers:
- headers['List-Archive'] = '<{0}>'.format(
- archiver.list_url(mlist))
- permalink = archiver.permalink(mlist, msg)
- if permalink is not None:
- headers['Archived-At'] = permalink
- # XXX RFC 2369 also defines a List-Owner header which we are not currently
- # supporting, but should.
- for h, v in headers.items():
- # First we delete any pre-existing headers because the RFC permits
- # only one copy of each, and we want to be sure it's ours.
- del msg[h]
- # Wrap these lines if they are too long. 78 character width probably
- # shouldn't be hardcoded, but is at least text-MUA friendly. The
- # adding of 2 is for the colon-space separator.
- if len(h) + 2 + len(v) > 78:
- v = CONTINUATION.join(v.split(', '))
- msg[h] = v
def prefix_subject(mlist, msg, msgdata):
"""Maybe add a subject prefix.
-
+
Add the subject prefix unless the message is a digest or is being fast
tracked (e.g. internally crafted, delivered to a single user such as the
list admin).
diff --git a/src/mailman/pipeline/docs/cook-headers.rst b/src/mailman/pipeline/docs/cook-headers.rst
index cd2acaae2..e0313f53a 100644
--- a/src/mailman/pipeline/docs/cook-headers.rst
+++ b/src/mailman/pipeline/docs/cook-headers.rst
@@ -8,10 +8,8 @@ transformations. Some headers get added, others get changed. Some of these
changes depend on mailing list settings and others depend on how the message
is getting sent through the system. We'll take things one-by-one.
- >>> mlist = create_list('_xtest@example.com')
+ >>> mlist = create_list('test@example.com')
>>> mlist.subject_prefix = ''
- >>> mlist.include_list_post_header = False
- >>> mlist.archive = True
Saving the original sender
@@ -104,200 +102,6 @@ have one of them.
junk
-RFC 2919 and 2369 headers
-=========================
-
-This is a helper function for the following section.
-
- >>> def list_headers(msg):
- ... print '---start---'
- ... # Sort the List-* headers found in the message. We need to do
- ... # this because CookHeaders puts them in a dictionary which does
- ... # not have a guaranteed sort order.
- ... for header in sorted(msg.keys()):
- ... parts = header.lower().split('-')
- ... if 'list' not in parts:
- ... continue
- ... for value in msg.get_all(header):
- ... print '%s: %s' % (header, value)
- ... print '---end---'
-
-These RFCs define headers for mailing list actions. A mailing list should
-generally add these headers, but not for messages that aren't crafted for a
-specific list (e.g. password reminders in Mailman 2.x).
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ...
- ... """)
- >>> process(mlist, msg, dict(_nolist=True))
- >>> list_headers(msg)
- ---start---
- ---end---
-
-Some people don't like these headers because their mail readers aren't good
-about hiding them. A list owner can turn these headers off.
-
- >>> mlist.include_rfc2369_headers = False
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ...
- ... """)
- >>> process(mlist, msg, {})
- >>> list_headers(msg)
- ---start---
- ---end---
-
-But normally, a list will include these headers.
-
- >>> mlist.include_rfc2369_headers = True
- >>> mlist.include_list_post_header = True
- >>> mlist.preferred_language = 'en'
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... Message-ID: <12345>
- ...
- ... """)
- >>> process(mlist, msg, {})
- >>> list_headers(msg)
- ---start---
- List-Archive: <http://lists.example.com/archives/_xtest@example.com>
- List-Help: <mailto:_xtest-request@example.com?subject=help>
- List-Id: <_xtest.example.com>
- List-Post: <mailto:_xtest@example.com>
- List-Subscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-join@example.com>
- List-Unsubscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-leave@example.com>
- ---end---
-
-If the mailing list has a description, then it is included in the ``List-Id``
-header.
-
- >>> mlist.description = 'My test mailing list'
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ...
- ... """)
- >>> process(mlist, msg, {})
- >>> list_headers(msg)
- ---start---
- List-Archive: <http://lists.example.com/archives/_xtest@example.com>
- List-Help: <mailto:_xtest-request@example.com?subject=help>
- List-Id: My test mailing list <_xtest.example.com>
- List-Post: <mailto:_xtest@example.com>
- List-Subscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-join@example.com>
- List-Unsubscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-leave@example.com>
- ---end---
-
-There are some circumstances when the list administrator wants to explicitly
-set the ``List-ID`` header. Start by creating a new domain.
-::
-
- >>> from mailman.interfaces.domain import IDomainManager
- >>> from zope.component import getUtility
- >>> domain = getUtility(IDomainManager).add('mail.example.net')
- >>> mlist.mail_host = 'mail.example.net'
-
- >>> process(mlist, msg, {})
- >>> print msg['list-id']
- My test mailing list <_xtest.example.com>
-
- >>> mlist.list_id = '_xtest.mail.example.net'
- >>> process(mlist, msg, {})
- >>> print msg['list-id']
- My test mailing list <_xtest.mail.example.net>
-
- >>> mlist.mail_host = 'example.com'
- >>> mlist.list_id = '_xtest.example.com'
-
-Any existing ``List-ID`` headers are removed from the original message.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... List-ID: <123.456.789>
- ...
- ... """)
-
- >>> process(mlist, msg, {})
- >>> sorted(msg.get_all('list-id'))
- [u'My test mailing list <_xtest.example.com>']
-
-Administrative messages crafted by Mailman will have a reduced set of headers.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ...
- ... """)
- >>> process(mlist, msg, dict(reduced_list_headers=True))
- >>> list_headers(msg)
- ---start---
- List-Help: <mailto:_xtest-request@example.com?subject=help>
- List-Id: My test mailing list <_xtest.example.com>
- List-Subscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-join@example.com>
- List-Unsubscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-leave@example.com>
- X-List-Administrivia: yes
- ---end---
-
-With the normal set of ``List-*`` headers, it's still possible to suppress the
-``List-Post`` header, which is reasonable for an announce only mailing list.
-
- >>> mlist.include_list_post_header = False
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ...
- ... """)
- >>> process(mlist, msg, {})
- >>> list_headers(msg)
- ---start---
- List-Archive: <http://lists.example.com/archives/_xtest@example.com>
- List-Help: <mailto:_xtest-request@example.com?subject=help>
- List-Id: My test mailing list <_xtest.example.com>
- List-Subscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-join@example.com>
- List-Unsubscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-leave@example.com>
- ---end---
-
-And if the list isn't being archived, it makes no sense to add the
-``List-Archive`` header either.
-
- >>> mlist.include_list_post_header = True
- >>> mlist.archive = False
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ...
- ... """)
- >>> process(mlist, msg, {})
- >>> list_headers(msg)
- ---start---
- List-Help: <mailto:_xtest-request@example.com?subject=help>
- List-Id: My test mailing list <_xtest.example.com>
- List-Post: <mailto:_xtest@example.com>
- List-Subscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-join@example.com>
- List-Unsubscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-leave@example.com>
- ---end---
-
-
-Archived-At
-===========
-
-RFC 5064 (draft) defines a new ``Archived-At`` header which contains the url to
-the individual message in the archives. The stock Pipermail archiver doesn't
-support this because the url can't be calculated until after the message is
-archived. Because this is done by the archive runner, this information isn't
-available to us now.
-
- >>> print msg['archived-at']
- None
-
-
Personalization
===============
@@ -310,6 +114,7 @@ the recipient headers so that users will be able to reply back to the list.
... Personalization, ReplyToMunging)
>>> mlist.personalize = Personalization.full
>>> mlist.reply_goes_to_list = ReplyToMunging.no_munging
+ >>> mlist.description = 'My test mailing list'
>>> msg = message_from_string("""\
... From: aperson@example.com
...
@@ -317,16 +122,8 @@ the recipient headers so that users will be able to reply back to the list.
>>> process(mlist, msg, {})
>>> print msg.as_string()
From: aperson@example.com
- X-BeenThere: _xtest@example.com
X-Mailman-Version: ...
Precedence: list
- Cc: My test mailing list <_xtest@example.com>
- List-Id: My test mailing list <_xtest.example.com>
- List-Unsubscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-leave@example.com>
- List-Post: <mailto:_xtest@example.com>
- List-Help: <mailto:_xtest-request@example.com?subject=help>
- List-Subscribe: <http://lists.example.com/listinfo/_xtest@example.com>,
- <mailto:_xtest-join@example.com>
+ Cc: My test mailing list <test@example.com>
<BLANKLINE>
<BLANKLINE>
diff --git a/src/mailman/pipeline/docs/rfc-2369.rst b/src/mailman/pipeline/docs/rfc-2369.rst
new file mode 100644
index 000000000..473b1dcee
--- /dev/null
+++ b/src/mailman/pipeline/docs/rfc-2369.rst
@@ -0,0 +1,230 @@
+=========================
+RFC 2919 and 2369 headers
+=========================
+
+`RFC 2919`_ and `RFC 2369`_ define headers for mailing list actions. These
+headers generally start with the `List-` prefix.
+
+ >>> mlist = create_list('test@example.com')
+ >>> mlist.preferred_language = 'en'
+ >>> mlist.archive = False
+
+..
+This is a helper function for the following section.
+
+ >>> def list_headers(msg, only=None):
+ ... if isinstance(only, basestring):
+ ... only = (only.lower(),)
+ ... elif only is None:
+ ... only = set(header.lower() for header in msg.keys()
+ ... if header.lower().startswith('list-'))
+ ... only.add('archived-at')
+ ... else:
+ ... only = set(header.lower() for header in only)
+ ... print '---start---'
+ ... for header in sorted(only):
+ ... for value in sorted(msg.get_all(header, ())):
+ ... print '%s: %s' % (header, value)
+ ... print '---end---'
+
+The `rfc-2369` handler adds the `List-` headers. `List-Id` is always added.
+
+ >>> from mailman.pipeline.rfc_2369 import process
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg, 'list-id')
+ ---start---
+ list-id: <test.example.com>
+ ---end---
+
+
+Fewer headers
+=============
+
+Some people don't like these headers because their mail readers aren't good
+about hiding them. A list owner can turn these headers off.
+
+ >>> mlist.include_rfc2369_headers = False
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ ---end---
+
+Messages which Mailman generates itself, such as user or owner notifications,
+have a reduced set of `List-` headers. Specifically, there is no `List-Post`,
+`List-Archive` or `Archived-At` header.
+
+ >>> mlist.include_rfc2369_headers = True
+ >>> mlist.include_list_post_header = False
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, dict(reduced_list_headers=True))
+ >>> list_headers(msg)
+ ---start---
+ list-help: <mailto:test-request@example.com?subject=help>
+ list-id: <test.example.com>
+ list-subscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-join@example.com>
+ list-unsubscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-leave@example.com>
+ ---end---
+
+
+List-Post header
+================
+
+Discussion lists, to which any subscriber can post, also have a `List-Post`
+header which contains the `mailto:` URL used to send messages to the list.
+
+ >>> mlist.include_list_post_header = True
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ list-help: <mailto:test-request@example.com?subject=help>
+ list-id: <test.example.com>
+ list-post: <mailto:test@example.com>
+ list-subscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-join@example.com>
+ list-unsubscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-leave@example.com>
+ ---end---
+
+
+List-Id header
+==============
+
+If the mailing list has a description, then it is included in the ``List-Id``
+header.
+
+ >>> mlist.description = 'My test mailing list'
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ list-help: <mailto:test-request@example.com?subject=help>
+ list-id: My test mailing list <test.example.com>
+ list-post: <mailto:test@example.com>
+ list-subscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-join@example.com>
+ list-unsubscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-leave@example.com>
+ ---end---
+
+Any existing ``List-Id`` headers are removed from the original message.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... List-ID: <123.456.789>
+ ...
+ ... """)
+
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg, only='list-id')
+ ---start---
+ list-id: My test mailing list <test.example.com>
+ ---end---
+
+
+Archive headers
+===============
+
+When the mailing list is configured to enable archiving, a `List-Archive`
+header will be added.
+
+ >>> mlist.archive = True
+
+ >>> from mailman.config import config
+ >>> config.push('pipermail', """
+ ... [archiver.prototype]
+ ... enable: no
+ ... [archiver.mail_archive]
+ ... enable: no
+ ... [archiver.mhonarc]
+ ... enable: no
+ ... [archiver.pipermail]
+ ... enable: yes
+ ... """)
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg, only='list-archive')
+ ---start---
+ list-archive: <http://www.example.com/pipermail/test@example.com>
+ ---end---
+
+`RFC 5064`_ defines the `Archived-At` header which contains the url to the
+individual message in the archives. Archivers which don't support
+pre-calculation of the archive url cannot add the `Archived-At` header, as is
+the case with Pipermail (see above). However, other archivers can calculate
+the url, and do add this header.
+
+ >>> config.pop('pipermail')
+ >>> config.push('prototype', """
+ ... [archiver.prototype]
+ ... enable: yes
+ ... [archiver.mail_archive]
+ ... enable: no
+ ... [archiver.mhonarc]
+ ... enable: no
+ ... [archiver.pipermail]
+ ... enable: No
+ ... """)
+
+The *prototype* archiver can calculate this archive url given a `Message-ID`.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Message-ID: <first>
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg, only=('list-archive', 'archived-at'))
+ ---start---
+ archived-at: http://lists.example.com/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ list-archive: <http://lists.example.com>
+ ---end---
+
+If the mailing list isn't being archived, neither the `List-Archive` nor
+`Archived-At` headers will be added.
+
+ >>> config.pop('prototype')
+ >>> mlist.archive = False
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ list-help: <mailto:test-request@example.com?subject=help>
+ list-id: My test mailing list <test.example.com>
+ list-post: <mailto:test@example.com>
+ list-subscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-join@example.com>
+ list-unsubscribe: <http://lists.example.com/listinfo/test@example.com>,
+ <mailto:test-leave@example.com>
+ ---end---
+
+
+.. _`RFC 2919`: http://www.faqs.org/rfcs/rfc2919.html
+.. _`RFC 2369`: http://www.faqs.org/rfcs/rfc2369.html
+.. _`RFC 5064`: http://www.faqs.org/rfcs/rfc5064.html
diff --git a/src/mailman/pipeline/rfc_2369.py b/src/mailman/pipeline/rfc_2369.py
new file mode 100644
index 000000000..d29edcdbc
--- /dev/null
+++ b/src/mailman/pipeline/rfc_2369.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2011 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""RFC 2369 List-* and related headers."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'process',
+ ]
+
+
+from email.utils import formataddr
+
+from mailman.config import config
+from mailman.pipeline.cook_headers import uheader
+
+CONTINUATION = ',\n\t'
+
+
+
+def process(mlist, msg, msgdata):
+ """Add the RFC 2369 List-* and related headers."""
+ # Some people really hate the List-* headers. It seems that the free
+ # version of Eudora (possibly on for some platforms) does not hide these
+ # headers by default, pissing off their users. Too bad. Fix the MUAs.
+ if not mlist.include_rfc2369_headers:
+ return
+ list_id = '{0.list_name}.{0.mail_host}'.format(mlist)
+ if mlist.description:
+ # Don't wrap the header since here we just want to get it properly RFC
+ # 2047 encoded.
+ i18ndesc = uheader(mlist, mlist.description, 'List-Id', maxlinelen=998)
+ listid_h = formataddr((str(i18ndesc), list_id))
+ else:
+ # Without a description, we need to ensure the MUST brackets.
+ listid_h = '<{0}>'.format(list_id)
+ # No other agent should add a List-ID header except Mailman.
+ del msg['list-id']
+ msg['List-Id'] = listid_h
+ # For internally crafted messages, we also add a (nonstandard),
+ # "X-List-Administrivia: yes" header. For all others (i.e. those coming
+ # from list posts), we add a bunch of other RFC 2369 headers.
+ requestaddr = mlist.request_address
+ subfieldfmt = '<{0}>, <mailto:{1}>'
+ listinfo = mlist.script_url('listinfo')
+ headers = {}
+ # XXX reduced_list_headers used to suppress List-Help, List-Subject, and
+ # List-Unsubscribe from UserNotification. That doesn't seem to make sense
+ # any more, so always add those three headers (others will still be
+ # suppressed).
+ headers.update({
+ 'List-Help' : '<mailto:{0}?subject=help>'.format(requestaddr),
+ 'List-Unsubscribe': subfieldfmt.format(listinfo, mlist.leave_address),
+ 'List-Subscribe' : subfieldfmt.format(listinfo, mlist.join_address),
+ })
+ if not msgdata.get('reduced_list_headers'):
+ # List-Post: is controlled by a separate attribute
+ if mlist.include_list_post_header:
+ headers['List-Post'] = '<mailto:{0}>'.format(mlist.posting_address)
+ # Add RFC 2369 and 5064 archiving headers, if archiving is enabled.
+ if mlist.archive:
+ for archiver in config.archivers:
+ headers['List-Archive'] = '<{0}>'.format(
+ archiver.list_url(mlist))
+ permalink = archiver.permalink(mlist, msg)
+ if permalink is not None:
+ headers['Archived-At'] = permalink
+ # XXX RFC 2369 also defines a List-Owner header which we are not currently
+ # supporting, but should.
+ for h, v in headers.items():
+ # First we delete any pre-existing headers because the RFC permits
+ # only one copy of each, and we want to be sure it's ours.
+ del msg[h]
+ # Wrap these lines if they are too long. 78 character width probably
+ # shouldn't be hardcoded, but is at least text-MUA friendly. The
+ # adding of 2 is for the colon-space separator.
+ if len(h) + 2 + len(v) > 78:
+ v = CONTINUATION.join(v.split(', '))
+ msg[h] = v
diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py
index 0f08198ab..95b8a93f0 100644
--- a/src/mailman/rest/configuration.py
+++ b/src/mailman/rest/configuration.py
@@ -188,7 +188,6 @@ ATTRIBUTES = dict(
join_address=GetterSetter(None),
last_post_at=GetterSetter(None),
leave_address=GetterSetter(None),
- list_id=GetterSetter(None),
list_name=GetterSetter(None),
next_digest_number=GetterSetter(None),
no_reply_address=GetterSetter(None),
diff --git a/src/mailman/rest/docs/configuration.rst b/src/mailman/rest/docs/configuration.rst
index b21bb8e4c..55b732172 100644
--- a/src/mailman/rest/docs/configuration.rst
+++ b/src/mailman/rest/docs/configuration.rst
@@ -46,7 +46,6 @@ All readable attributes for a list are available on a sub-resource.
join_address: test-one-join@example.com
last_post_at: None
leave_address: test-one-leave@example.com
- list_id: test-one.example.com
list_name: test-one
mail_host: example.com
next_digest_number: 1
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index a7fc4e289..de35be8be 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -353,7 +353,7 @@ def reset_the_world():
"""Reset everything:
* Clear out the database
- * Remove all residual queue files
+ * Remove all residual queue and digest files
* Clear the message store
* Reset the global style manager
@@ -362,6 +362,11 @@ def reset_the_world():
"""
# Reset the database between tests.
config.db._reset()
+ # Remove any digest files.
+ for dirpath, dirnames, filenames in os.walk(config.LIST_DATA_DIR):
+ for filename in filenames:
+ if filename.endswith('.mmdf'):
+ os.remove(os.path.join(dirpath, filename))
# Remove all residual queue files.
for dirpath, dirnames, filenames in os.walk(config.QUEUE_DIR):
for filename in filenames: