summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrea Crotti2012-03-14 16:49:59 +0000
committerAndrea Crotti2012-03-14 16:49:59 +0000
commit52a7e534427f6a62db0f7d857dc4fc2a19137f0a (patch)
tree5ac1e40555fc391dae241d0741d1fafbaa338396
parentf66b5879531dc37509402cc17c1e7bfe817d964c (diff)
parentbcc42e2201c7172848185e5675a7b79e3d28aa0f (diff)
downloadmailman-52a7e534427f6a62db0f7d857dc4fc2a19137f0a.tar.gz
mailman-52a7e534427f6a62db0f7d857dc4fc2a19137f0a.tar.zst
mailman-52a7e534427f6a62db0f7d857dc4fc2a19137f0a.zip
merge
-rw-r--r--README.rst1
-rw-r--r--src/mailman/app/docs/pipelines.rst31
-rw-r--r--src/mailman/app/inject.py14
-rw-r--r--src/mailman/app/tests/test_inject.py77
-rw-r--r--src/mailman/app/tests/test_moderation.py7
-rw-r--r--src/mailman/archiving/docs/common.rst25
-rw-r--r--src/mailman/archiving/mailarchive.py22
-rw-r--r--src/mailman/archiving/mhonarc.py20
-rw-r--r--src/mailman/archiving/prototype.py21
-rw-r--r--src/mailman/bin/mailman.py6
-rw-r--r--src/mailman/commands/cli_qfile.py2
-rw-r--r--src/mailman/commands/cli_withlist.py11
-rw-r--r--src/mailman/commands/docs/inject.rst54
-rw-r--r--src/mailman/commands/docs/withlist.rst19
-rw-r--r--src/mailman/config/mailman.cfg5
-rw-r--r--src/mailman/config/schema.cfg12
-rw-r--r--src/mailman/docs/NEWS.rst16
-rw-r--r--src/mailman/model/tests/test_user.py3
-rw-r--r--src/mailman/pipeline/decorate.py3
-rw-r--r--src/mailman/pipeline/docs/rfc-2369.rst1
-rw-r--r--src/mailman/runners/docs/incoming.rst1
-rw-r--r--src/mailman/runners/docs/lmtp.rst27
-rw-r--r--src/mailman/runners/lmtp.py17
-rw-r--r--src/mailman/runners/maildir.py195
-rw-r--r--src/mailman/runners/tests/test_confirm.py6
-rw-r--r--src/mailman/runners/tests/test_lmtp.py98
-rw-r--r--src/mailman/styles/docs/__init__.py0
-rw-r--r--src/mailman/styles/docs/styles.rst (renamed from src/mailman/app/docs/styles.rst)105
-rw-r--r--src/mailman/styles/tests/__init__.py0
-rw-r--r--src/mailman/styles/tests/test_styles.py90
-rw-r--r--src/mailman/testing/helpers.py5
-rw-r--r--src/mailman/testing/layers.py35
-rw-r--r--src/mailman/testing/testing.cfg3
-rw-r--r--src/mailman/utilities/email.py33
-rw-r--r--src/mailman/utilities/interact.py (renamed from src/mailman/interact.py)1
-rw-r--r--src/mailman/utilities/tests/test_email.py75
-rw-r--r--src/mailman/version.py12
37 files changed, 630 insertions, 423 deletions
diff --git a/README.rst b/README.rst
index e761e28d7..9c9b026c6 100644
--- a/README.rst
+++ b/README.rst
@@ -43,6 +43,7 @@ Table of Contents
src/mailman/model/docs/*
src/mailman/core/docs/*
src/mailman/app/docs/*
+ src/mailman/styles/docs/*
src/mailman/runners/docs/*
src/mailman/pipeline/docs/*
src/mailman/rest/docs/*
diff --git a/src/mailman/app/docs/pipelines.rst b/src/mailman/app/docs/pipelines.rst
index fbd405f68..56fdeb5c6 100644
--- a/src/mailman/app/docs/pipelines.rst
+++ b/src/mailman/app/docs/pipelines.rst
@@ -24,6 +24,7 @@ Messages hit the pipeline after they've been accepted for posting.
... To: test@example.com
... Subject: My first post
... Message-ID: <first>
+ ... X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
...
... First post!
... """)
@@ -37,15 +38,15 @@ etc.
From: aperson@example.com
To: test@example.com
Message-ID: <first>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
List-Id: <test.example.com>
- X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
List-Post: <mailto:test@example.com>
List-Subscribe: <http://lists.example.com/listinfo/test@example.com>,
<mailto:test-join@example.com>
- Archived-At: http://lists.example.com/archives/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ Archived-At: http://lists.example.com/.../4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
List-Unsubscribe: <http://lists.example.com/listinfo/test@example.com>,
<mailto:test-leave@example.com>
List-Archive: <http://lists.example.com/archives/test@example.com>
@@ -65,15 +66,18 @@ However there are currently no recipients for this message.
After pipeline processing, 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: test@example.com
Message-ID: <first>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
@@ -82,6 +86,7 @@ processing queues.
<BLANKLINE>
First post!
<BLANKLINE>
+
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
original_sender : aperson@example.com
@@ -97,15 +102,19 @@ the outgoing nntp queue.
>>> len(messages)
0
-This is the message that will actually get delivered to end recipients.
+The outgoing queue will hold the copy of 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: test@example.com
Message-ID: <first>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
@@ -114,6 +123,7 @@ This is the message that will actually get delivered to end recipients.
<BLANKLINE>
First post!
<BLANKLINE>
+
>>> dump_msgdata(messages[0].msgdata)
_parsemsg : False
listname : test@example.com
@@ -124,15 +134,18 @@ This is the message that will actually get delivered to end recipients.
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: test@example.com
Message-ID: <first>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
Subject: [Test] My first post
X-Mailman-Version: ...
Precedence: list
@@ -143,10 +156,8 @@ There's now one message in the digest mailbox, getting ready to be sent.
<BLANKLINE>
-Clean up the digests
-====================
-
- >>> digest.clear()
- >>> digest.flush()
- >>> sum(1 for msg in digest_mbox(mlist))
- 0
+.. Clean up the digests
+ >>> digest.clear()
+ >>> digest.flush()
+ >>> sum(1 for msg in digest_mbox(mlist))
+ 0
diff --git a/src/mailman/app/inject.py b/src/mailman/app/inject.py
index 8bf907af0..5dcaf5584 100644
--- a/src/mailman/app/inject.py
+++ b/src/mailman/app/inject.py
@@ -17,7 +17,7 @@
"""Inject a message into a queue."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -31,12 +31,16 @@ from email.utils import formatdate, make_msgid
from mailman.config import config
from mailman.email.message import Message
+from mailman.utilities.email import add_message_hash
def inject_message(mlist, msg, recipients=None, switchboard=None, **kws):
"""Inject a message into a queue.
+ If the message does not have a Message-ID header, one is added. An
+ X-Message-Id-Hash header is also always added.
+
:param mlist: The mailing list this message is destined for.
:type mlist: IMailingList
:param msg: The Message object to inject.
@@ -56,12 +60,14 @@ def inject_message(mlist, msg, recipients=None, switchboard=None, **kws):
# message has a Message-ID.
if 'message-id' not in msg:
msg['Message-ID'] = make_msgid()
+ add_message_hash(msg)
# Ditto for Date: as required by RFC 2822.
if 'date' not in msg:
msg['Date'] = formatdate(localtime=True)
+ msg.original_size = len(msg.as_string())
msgdata = dict(
listname=mlist.fqdn_listname,
- original_size=getattr(msg, 'original_size', len(msg.as_string())),
+ original_size=msg.original_size,
)
msgdata.update(kws)
if recipients is not None:
@@ -73,6 +79,9 @@ def inject_message(mlist, msg, recipients=None, switchboard=None, **kws):
def inject_text(mlist, text, recipients=None, switchboard=None, **kws):
"""Turn text into a message and inject that into a queue.
+ If the text does not have a Message-ID header, one is added. An
+ X-Message-Id-Hash header is also always added.
+
:param mlist: The mailing list this message is destined for.
:type mlist: IMailingList
:param text: The text of the message to inject. This will be parsed into
@@ -88,5 +97,4 @@ def inject_text(mlist, text, recipients=None, switchboard=None, **kws):
:type kws: dictionary
"""
message = message_from_string(text, Message)
- message.original_size = len(text)
inject_message(mlist, message, recipients, switchboard, **kws)
diff --git a/src/mailman/app/tests/test_inject.py b/src/mailman/app/tests/test_inject.py
index de7e4bd29..4e08ff0a7 100644
--- a/src/mailman/app/tests/test_inject.py
+++ b/src/mailman/app/tests/test_inject.py
@@ -17,7 +17,7 @@
"""Testing app.inject functions."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -108,14 +108,6 @@ Nothing.
self.assertTrue('date' in items[0].msg)
self.assertEqual(items[0].msg['date'], self.msg['date'])
- def test_inject_message_trusts_original_size_attribute(self):
- # If the message object has an `original_size` attribute, this is
- # copied into the metadata.
- self.msg.original_size = 3
- inject_message(self.mlist, self.msg)
- items = get_queue_messages('in')
- self.assertEqual(items[0].msgdata['original_size'], 3)
-
def test_inject_message_with_keywords(self):
# Keyword arguments are copied into the metadata.
inject_message(self.mlist, self.msg, foo='yes', bar='no')
@@ -123,6 +115,26 @@ Nothing.
self.assertEqual(items[0].msgdata['foo'], 'yes')
self.assertEqual(items[0].msgdata['bar'], 'no')
+ def test_inject_message_id_hash(self):
+ # When the injected message has a Message-ID header, the injected
+ # message will also get an X-Message-ID-Hash header.
+ inject_message(self.mlist, self.msg)
+ items = get_queue_messages('in')
+ self.assertEqual(items[0].msg['x-message-id-hash'],
+ '4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB')
+
+ def test_inject_message_id_hash_without_message_id(self):
+ # When the injected message does not have a Message-ID header, a
+ # Message-ID header will be added, and the injected message will also
+ # get an X-Message-ID-Hash header.
+ del self.msg['message-id']
+ self.assertFalse('message-id' in self.msg)
+ self.assertFalse('x-message-id-hash' in self.msg)
+ inject_message(self.mlist, self.msg)
+ items = get_queue_messages('in')
+ self.assertTrue('message-id' in items[0].msg)
+ self.assertTrue('x-message-id-hash' in items[0].msg)
+
class TestInjectText(unittest.TestCase):
@@ -155,9 +167,18 @@ Nothing.
items = get_queue_messages('in')
self.assertEqual(len(items), 1)
self.assertTrue(isinstance(items[0].msg, Message))
+ self.assertEqual(items[0].msg['x-message-id-hash'],
+ 'GUXXQKNCHBFQAHGBFMGCME6HKZCUUH3K')
+ # Delete that header because it is not in the original text.
+ del items[0].msg['x-message-id-hash']
self.eq(items[0].msg.as_string(), self.text)
self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
- self.assertEqual(items[0].msgdata['original_size'], len(self.text))
+ self.assertEqual(items[0].msgdata['original_size'],
+ # Add back the X-Message-ID-Header which was in the
+ # message contributing to the original_size, but
+ # wasn't in the original text. Don't forget the
+ # newline!
+ len(self.text) + 52)
def test_inject_text_with_recipients(self):
# Explicit recipients end up in the metadata.
@@ -173,9 +194,16 @@ Nothing.
self.assertEqual(len(items), 0)
items = get_queue_messages('virgin')
self.assertEqual(len(items), 1)
+ # Remove the X-Message-ID-Hash header which isn't in the original text.
+ del items[0].msg['x-message-id-hash']
self.eq(items[0].msg.as_string(), self.text)
self.assertEqual(items[0].msgdata['listname'], 'test@example.com')
- self.assertEqual(items[0].msgdata['original_size'], len(self.text))
+ self.assertEqual(items[0].msgdata['original_size'],
+ # Add back the X-Message-ID-Header which was in the
+ # message contributing to the original_size, but
+ # wasn't in the original text. Don't forget the
+ # newline!
+ len(self.text) + 52)
def test_inject_text_without_message_id(self):
# inject_text() adds a Message-ID header if it's missing.
@@ -198,7 +226,12 @@ Nothing.
# the injected text.
inject_text(self.mlist, self.text)
items = get_queue_messages('in')
- self.assertEqual(items[0].msgdata['original_size'], len(self.text))
+ self.assertEqual(items[0].msgdata['original_size'],
+ # Add back the X-Message-ID-Header which was in the
+ # message contributing to the original_size, but
+ # wasn't in the original text. Don't forget the
+ # newline!
+ len(self.text) + 52)
def test_inject_text_with_keywords(self):
# Keyword arguments are copied into the metadata.
@@ -206,3 +239,23 @@ Nothing.
items = get_queue_messages('in')
self.assertEqual(items[0].msgdata['foo'], 'yes')
self.assertEqual(items[0].msgdata['bar'], 'no')
+
+ def test_inject_message_id_hash(self):
+ # When the injected message has a Message-ID header, the injected
+ # message will also get an X-Message-ID-Hash header.
+ inject_text(self.mlist, self.text)
+ items = get_queue_messages('in')
+ self.assertEqual(items[0].msg['x-message-id-hash'],
+ 'GUXXQKNCHBFQAHGBFMGCME6HKZCUUH3K')
+
+ def test_inject_message_id_hash_without_message_id(self):
+ # When the injected message does not have a Message-ID header, a
+ # Message-ID header will be added, and the injected message will also
+ # get an X-Message-ID-Hash header.
+ filtered = self._remove_line('message-id')
+ self.assertFalse('Message-ID' in filtered)
+ self.assertFalse('X-Message-ID-Hash' in filtered)
+ inject_text(self.mlist, filtered)
+ items = get_queue_messages('in')
+ self.assertTrue('message-id' in items[0].msg)
+ self.assertTrue('x-message-id-hash' in items[0].msg)
diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py
index dea2f5918..bc324faea 100644
--- a/src/mailman/app/tests/test_moderation.py
+++ b/src/mailman/app/tests/test_moderation.py
@@ -17,10 +17,11 @@
"""Moderation tests."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestModeration',
]
@@ -79,8 +80,8 @@ Message-ID: <alpha>
self.assertTrue('x-peer' in message)
# The X-Mailman-Approved-At header has local timezone information in
# it, so test that separately.
- self.assertEqual(message['x-mailman-approved-at'][:-4],
- 'Mon, 01 Aug 2005 07:49:23 -')
+ self.assertEqual(message['x-mailman-approved-at'][:-5],
+ 'Mon, 01 Aug 2005 07:49:23 ')
del message['x-mailman-approved-at']
# The Message-ID matches the original.
self.assertEqual(message['message-id'], '<alpha>')
diff --git a/src/mailman/archiving/docs/common.rst b/src/mailman/archiving/docs/common.rst
index 330c8e307..45ec8f194 100644
--- a/src/mailman/archiving/docs/common.rst
+++ b/src/mailman/archiving/docs/common.rst
@@ -11,6 +11,7 @@ archivers.
... To: test@example.com
... Subject: An archived message
... Message-ID: <12345>
+ ... X-Message-ID-Hash: RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
...
... Here is an archived message.
... """)
@@ -33,7 +34,7 @@ interoperate.
... archivers[archiver.name] = archiver
mail-archive
http://go.mail-archive.dev/test%40example.com
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
+ http://go.mail-archive.dev/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
mhonarc
http://lists.example.com/.../test@example.com
http://lists.example.com/.../RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
@@ -80,7 +81,7 @@ messages to public lists will get sent there automatically.
>>> print archiver.list_url(mlist)
http://go.mail-archive.dev/test%40example.com
>>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
+ http://go.mail-archive.dev/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
To archive the message, the archiver actually mails the message to a special
address at The Mail Archive. The message gets no header or footer decoration.
@@ -103,7 +104,7 @@ address at The Mail Archive. The message gets no header or footer decoration.
To: test@example.com
Subject: An archived message
Message-ID: <12345>
- X-Message-ID-Hash: ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
+ X-Message-ID-Hash: RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
X-Peer: 127.0.0.1:...
X-MailFrom: test-bounces@example.com
X-RcptTo: archive@mail-archive.dev
@@ -127,26 +128,36 @@ at this service.
Additionally, this archiver can handle malformed ``Message-IDs``.
::
+ >>> from mailman.utilities.email import add_message_hash
>>> mlist.archive_private = False
>>> del msg['message-id']
+ >>> del msg['x-message-id-hash']
>>> msg['Message-ID'] = '12345>'
+ >>> add_message_hash(msg)
>>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8=
+ http://go.mail-archive.dev/YJIGBYRWZFG5LZEBQ7NR25B5HBR2BVD6
>>> del msg['message-id']
+ >>> del msg['x-message-id-hash']
>>> msg['Message-ID'] = '<12345'
+ >>> add_message_hash(msg)
>>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY=
+ http://go.mail-archive.dev/XUFFJNJ2P2WC4NDPQRZFDJMV24POP64B
>>> del msg['message-id']
+ >>> del msg['x-message-id-hash']
>>> msg['Message-ID'] = '12345'
+ >>> add_message_hash(msg)
>>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
+ http://go.mail-archive.dev/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
>>> del msg['message-id']
+ >>> del msg['x-message-id-hash']
+ >>> add_message_hash(msg)
>>> msg['Message-ID'] = ' 12345 '
+ >>> add_message_hash(msg)
>>> print archiver.permalink(mlist, msg)
- http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
+ http://go.mail-archive.dev/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
MHonArc
diff --git a/src/mailman/archiving/mailarchive.py b/src/mailman/archiving/mailarchive.py
index 29c0e22c2..c72cde11c 100644
--- a/src/mailman/archiving/mailarchive.py
+++ b/src/mailman/archiving/mailarchive.py
@@ -25,9 +25,6 @@ __all__ = [
]
-import hashlib
-
-from base64 import urlsafe_b64encode
from urllib import quote
from urlparse import urljoin
from zope.interface import implements
@@ -60,21 +57,12 @@ class MailArchive:
"""See `IArchiver`."""
if mlist.archive_private:
return None
- message_id = msg.get('message-id')
- # It is not the archiver's job to ensure the message has a Message-ID.
- # If no Message-ID is available, there is no permalink.
- if message_id is None:
+ # It is the LMTP server's responsibility to ensure that the message
+ # has a X-Message-ID-Hash header. If it doesn't then there's no
+ # permalink.
+ message_id_hash = msg.get('x-message-id-hash')
+ if message_id_hash is None:
return None
- # 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]
- else:
- message_id = message_id.strip()
- sha = hashlib.sha1(message_id)
- sha.update(str(mlist.posting_address))
- message_id_hash = urlsafe_b64encode(sha.digest())
- del msg['x-message-id-hash']
- msg['X-Message-ID-Hash'] = message_id_hash
return urljoin(config.archiver.mail_archive.base_url, message_id_hash)
@staticmethod
diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py
index db0b12c3d..0beeed73e 100644
--- a/src/mailman/archiving/mhonarc.py
+++ b/src/mailman/archiving/mhonarc.py
@@ -25,11 +25,9 @@ __all__ = [
]
-import hashlib
import logging
import subprocess
-from base64 import b32encode
from urlparse import urljoin
from zope.interface import implements
@@ -63,20 +61,12 @@ class MHonArc:
def permalink(mlist, msg):
"""See `IArchiver`."""
# XXX What about private MHonArc archives?
- message_id = msg.get('message-id')
- # It is not the archiver's job to ensure the message has a Message-ID.
- # If no Message-ID is available, there is no permalink.
- if message_id is None:
+ # It is the LMTP server's responsibility to ensure that the message
+ # has a X-Message-ID-Hash header. If it doesn't then there's no
+ # permalink.
+ message_id_hash = msg.get('x-message-id-hash')
+ if message_id_hash is None:
return None
- # 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]
- else:
- message_id = message_id.strip()
- sha = hashlib.sha1(message_id)
- message_id_hash = b32encode(sha.digest())
- del msg['x-message-id-hash']
- msg['X-Message-ID-Hash'] = message_id_hash
return urljoin(MHonArc.list_url(mlist), message_id_hash)
@staticmethod
diff --git a/src/mailman/archiving/prototype.py b/src/mailman/archiving/prototype.py
index 0a857fd77..55d78074e 100644
--- a/src/mailman/archiving/prototype.py
+++ b/src/mailman/archiving/prototype.py
@@ -25,9 +25,6 @@ __all__ = [
]
-import hashlib
-
-from base64 import b32encode
from urlparse import urljoin
from zope.interface import implements
@@ -54,20 +51,12 @@ class Prototype:
@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.
- # If this header is missing, there is no permalink.
- if message_id is None:
+ # It is the LMTP server's responsibility to ensure that the message
+ # has a X-Message-ID-Hash header. If it doesn't then there's no
+ # permalink.
+ message_id_hash = msg.get('x-message-id-hash')
+ if message_id_hash is None:
return None
- # 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]
- else:
- message_id = message_id.strip()
- 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
diff --git a/src/mailman/bin/mailman.py b/src/mailman/bin/mailman.py
index 2a230b49f..94de65255 100644
--- a/src/mailman/bin/mailman.py
+++ b/src/mailman/bin/mailman.py
@@ -17,7 +17,7 @@
"""The 'mailman' command dispatcher."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -94,7 +94,9 @@ def main():
# Mailman system, and in particular, we must read the configuration file.
config_file = os.getenv('MAILMAN_CONFIG_FILE')
if config_file is None:
- config_file = args.config
+ if args.config is not None:
+ config_file = os.path.abspath(os.path.expanduser(args.config))
+
initialize(config_file)
# Perform the subcommand option.
args.func(args)
diff --git a/src/mailman/commands/cli_qfile.py b/src/mailman/commands/cli_qfile.py
index 5851388de..78156f08c 100644
--- a/src/mailman/commands/cli_qfile.py
+++ b/src/mailman/commands/cli_qfile.py
@@ -31,8 +31,8 @@ from pprint import PrettyPrinter
from zope.interface import implements
from mailman.core.i18n import _
-from mailman.interact import interact
from mailman.interfaces.command import ICLISubCommand
+from mailman.utilities.interact import interact
m = []
diff --git a/src/mailman/commands/cli_withlist.py b/src/mailman/commands/cli_withlist.py
index dd1596227..3b1b36b1a 100644
--- a/src/mailman/commands/cli_withlist.py
+++ b/src/mailman/commands/cli_withlist.py
@@ -35,9 +35,9 @@ from zope.interface import implements
from mailman.config import config
from mailman.core.i18n import _
-from mailman.interact import DEFAULT_BANNER, interact
from mailman.interfaces.command import ICLISubCommand
from mailman.interfaces.listmanager import IListManager
+from mailman.utilities.interact import DEFAULT_BANNER, interact
from mailman.utilities.modules import call_name
# Global holding onto the open mailing list.
@@ -151,7 +151,6 @@ class Withlist:
abort=config.db.abort,
config=config,
)
-
banner = config.shell.banner + '\n' + banner
if as_boolean(config.shell.use_ipython):
self._start_ipython(overrides, banner)
@@ -167,14 +166,14 @@ class Withlist:
print _('ipython is not available, set use_ipython to no')
def _start_python(self, overrides, banner):
- # set the tab completion
+ # Set the tab completion.
try:
import readline, rlcompleter
readline.parse_and_bind('tab: complete')
except ImportError:
pass
else:
- sys.ps1 = config.shell.ps1 + ' '
+ sys.ps1 = config.shell.prompt + ' '
interact(upframe=False, banner=banner, overrides=overrides)
def _details(self):
@@ -240,9 +239,9 @@ and run this from the command line:
% bin/mailman withlist -r change mylist@example.com 'My List'""")
-
+
class Shell(Withlist):
"""An alias for `withlist`."""
-
+
name = 'shell'
diff --git a/src/mailman/commands/docs/inject.rst b/src/mailman/commands/docs/inject.rst
index 1c0843ff3..7150beac7 100644
--- a/src/mailman/commands/docs/inject.rst
+++ b/src/mailman/commands/docs/inject.rst
@@ -35,7 +35,6 @@ It's easy to find out which queues are available.
digest
in
lmtp
- maildir
news
out
pipeline
@@ -55,6 +54,7 @@ Usually, the text of the message to inject is in a file.
... From: aperson@example.com
... To: test@example.com
... Subject: testing
+ ... Message-ID: <aardvark>
...
... This is a test message.
... """
@@ -70,22 +70,20 @@ Let's provide a list name and try again.
>>> mlist = create_list('test@example.com')
>>> transaction.commit()
+ >>> from mailman.testing.helpers import get_queue_messages
- >>> in_queue = config.switchboards['in']
- >>> len(in_queue.files)
- 0
+ >>> get_queue_messages('in')
+ []
>>> args.listname = ['test@example.com']
>>> command.process(args)
By default, the incoming queue is used.
::
- >>> len(in_queue.files)
+ >>> items = get_queue_messages('in')
+ >>> len(items)
1
-
- >>> from mailman.testing.helpers import get_queue_messages
- >>> item = get_queue_messages('in')[0]
- >>> print item.msg.as_string()
+ >>> print items[0].msg.as_string()
From: aperson@example.com
To: test@example.com
Subject: testing
@@ -96,10 +94,10 @@ By default, the incoming queue is used.
<BLANKLINE>
<BLANKLINE>
- >>> dump_msgdata(item.msgdata)
+ >>> dump_msgdata(items[0].msgdata)
_parsemsg : False
listname : test@example.com
- original_size: 90
+ original_size: 203
version : 3
But a different queue can be specified on the command line.
@@ -108,13 +106,12 @@ But a different queue can be specified on the command line.
>>> args.queue = 'virgin'
>>> command.process(args)
- >>> len(in_queue.files)
- 0
- >>> virgin_queue = config.switchboards['virgin']
- >>> len(virgin_queue.files)
+ >>> get_queue_messages('in')
+ []
+ >>> items = get_queue_messages('virgin')
+ >>> len(items)
1
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
+ >>> print items[0].msg.as_string()
From: aperson@example.com
To: test@example.com
Subject: testing
@@ -125,10 +122,10 @@ But a different queue can be specified on the command line.
<BLANKLINE>
<BLANKLINE>
- >>> dump_msgdata(item.msgdata)
+ >>> dump_msgdata(items[0].msgdata)
_parsemsg : False
listname : test@example.com
- original_size: 90
+ original_size: 203
version : 3
@@ -145,6 +142,7 @@ The message text can also be provided on standard input.
... From: bperson@example.com
... To: test@example.com
... Subject: another test
+ ... Message-ID: <badger>
...
... This is another test message.
... """))
@@ -155,10 +153,10 @@ The message text can also be provided on standard input.
>>> args.queue = None
>>> command.process(args)
- >>> len(in_queue.files)
+ >>> items = get_queue_messages('in')
+ >>> len(items)
1
- >>> item = get_queue_messages('in')[0]
- >>> print item.msg.as_string()
+ >>> print items[0].msg.as_string()
From: bperson@example.com
To: test@example.com
Subject: another test
@@ -169,15 +167,15 @@ The message text can also be provided on standard input.
<BLANKLINE>
<BLANKLINE>
- >>> dump_msgdata(item.msgdata)
+ >>> dump_msgdata(items[0].msgdata)
_parsemsg : False
listname : test@example.com
- original_size: 100
+ original_size: 211
version : 3
- # Clean up.
- >>> sys.stdin = sys.__stdin__
- >>> args.filename = filename
+.. Clean up.
+ >>> sys.stdin = sys.__stdin__
+ >>> args.filename = filename
Metadata
@@ -200,7 +198,7 @@ injected.
bar : two
foo : one
listname : test@example.com
- original_size: 90
+ original_size: 203
version : 3
diff --git a/src/mailman/commands/docs/withlist.rst b/src/mailman/commands/docs/withlist.rst
index 7632c726a..f00208490 100644
--- a/src/mailman/commands/docs/withlist.rst
+++ b/src/mailman/commands/docs/withlist.rst
@@ -119,7 +119,20 @@ You also get an error if no mailing list is named.
--run requires a mailing list name
-Clean up
-========
+IPython
+=======
- >>> sys.path = old_path
+You can use `IPython`_ as the interactive shell by changing certain
+configuration variables in the `[shell]` section of your `mailman.cfg` file.
+Set `use_ipython` to "yes" to switch to IPython, which must be installed on
+your system.
+
+Other configuration variables in the `[shell]` section can be used to
+configure other aspects of the interactive shell. You can change both the
+prompt and the banner.
+
+
+.. Clean up
+ >>> sys.path = old_path
+
+.. _`IPython`: http://ipython.org/
diff --git a/src/mailman/config/mailman.cfg b/src/mailman/config/mailman.cfg
index 22c3a129b..0d37ceed9 100644
--- a/src/mailman/config/mailman.cfg
+++ b/src/mailman/config/mailman.cfg
@@ -61,11 +61,6 @@ class: mailman.runners.incoming.IncomingRunner
[runner.lmtp]
class: mailman.runners.lmtp.LMTPRunner
-[runner.maildir]
-class: mailman.runners.maildir.MaildirRunner
-# This is still experimental.
-start: no
-
[runner.news]
class: mailman.runners.news.NewsRunner
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index d0a8e9e20..e662633e6 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -63,14 +63,18 @@ post_hook:
layout: dev
[shell]
+# `bin/mailman shell` (also `withlist`) gives you an interactive prompt that
+# you can use to interact with an initialized and configured Mailman system.
+# Use --help for more information. This section allows you to configure
+# certain aspects of this interactive shell.
-# customize the interpreter prompt
-ps1: << MM >>
+# Customize the interpreter prompt.
+prompt: >>>
# Banner to show on startup.
-banner: welcome to the GNU Mailman shell
+banner: Welcome to the GNU Mailman shell
-# If IPython is found use it as the shell
+# Use IPython as the shell, which must be found on the system.
use_ipython: no
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 7993795a1..6869e2889 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -36,6 +36,16 @@ Architecture
`owners_chain`. The default `built-in` chain is renamed to
`default-posting-chain` while the `built-in` pipeline is renamed
`default-posting-pipeline`.
+ * The experimental `maildir` runner is removed. Use LMTP.
+ * The LMTP server now requires that the incoming message have a `Message-ID`,
+ otherwise it rejects the message with a 550 error. Also, the LMTP server
+ adds the `X-Message-ID-Hash` header automatically. The `inject` cli
+ command will also add the `X-Message-ID-Hash` header, but it will craft a
+ `Message-ID` header first if one is missing from the injected text. Also,
+ `inject` will always set the correct value for the `original_size`
+ attribute on the message object, instead of trusting a possibly incorrect
+ value if it's already set. The individual `IArchiver` implementations no
+ longer set the `X-Message-ID-Hash` header.
Database
--------
@@ -77,6 +87,8 @@ Interfaces
Commands
--------
+ * IPython support in `bin/mailman shell` contributed by Andrea Crotti.
+ (LP: #949926).
* The `mailman.cfg` configuration file will now automatically be detected if
it exists in an `etc` directory which is a sibling of argv0.
* `bin/mailman shell` is an alias for `withlist`.
@@ -88,6 +100,8 @@ Commands
* Added a `help` email command.
* A welcome message is sent when the user confirms their subscription via
email.
+ * Global ``-C`` option now accepts an absolute path to the configuration
+ file. Given by Andrea Crotti. (LP: #953707)
Bug fixes
---------
@@ -95,6 +109,8 @@ Bug fixes
(LP: #872391)
* Fixed bogus use of `bounce_processing` attribute (should have been
`process_bounces`, with thanks to Vincent Fretin. (LP: #876774)
+ * Fix `test_moderation` for timezones east of UTC+0000, given by blacktav.
+ (LP: #890675)
3.0 alpha 8 -- "Where's My Thing?"
diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py
index c4c07ed22..2f04b7c3a 100644
--- a/src/mailman/model/tests/test_user.py
+++ b/src/mailman/model/tests/test_user.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestUser',
]
@@ -36,6 +37,8 @@ from mailman.utilities.datetime import now
class TestUser(unittest.TestCase):
+ """Test users."""
+
layer = ConfigLayer
def setUp(self):
diff --git a/src/mailman/pipeline/decorate.py b/src/mailman/pipeline/decorate.py
index b785e5ecf..1291a2668 100644
--- a/src/mailman/pipeline/decorate.py
+++ b/src/mailman/pipeline/decorate.py
@@ -17,7 +17,7 @@
"""Decorate a message by sticking the header and footer around it."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
@@ -37,7 +37,6 @@ from mailman.core.i18n import _
from mailman.email.message import Message
from mailman.interfaces.handler import IHandler
from mailman.interfaces.templates import ITemplateLoader
-from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.string import expand
diff --git a/src/mailman/pipeline/docs/rfc-2369.rst b/src/mailman/pipeline/docs/rfc-2369.rst
index bb9276157..a1ba6c746 100644
--- a/src/mailman/pipeline/docs/rfc-2369.rst
+++ b/src/mailman/pipeline/docs/rfc-2369.rst
@@ -193,6 +193,7 @@ The *prototype* archiver can calculate this archive url given a `Message-ID`.
>>> msg = message_from_string("""\
... From: aperson@example.com
... Message-ID: <first>
+ ... X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
...
... """)
>>> process(mlist, msg, {})
diff --git a/src/mailman/runners/docs/incoming.rst b/src/mailman/runners/docs/incoming.rst
index e5f544a85..4a9db778e 100644
--- a/src/mailman/runners/docs/incoming.rst
+++ b/src/mailman/runners/docs/incoming.rst
@@ -121,6 +121,7 @@ Now the message is in the pipeline queue.
To: test@example.com
Subject: My first post
Message-ID: <first>
+ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
Date: ...
X-Mailman-Rule-Misses: approved; emergency; loop; member-moderation;
administrivia; implicit-dest; max-recipients; max-size;
diff --git a/src/mailman/runners/docs/lmtp.rst b/src/mailman/runners/docs/lmtp.rst
index 1ab42410b..2b6c4b42b 100644
--- a/src/mailman/runners/docs/lmtp.rst
+++ b/src/mailman/runners/docs/lmtp.rst
@@ -7,7 +7,9 @@ support LMTP local delivery, so this is a very portable way to connect Mailman
with your mail server.
Our LMTP server is fairly simple though; all it does is make sure that the
-message is destined for a valid endpoint, e.g. ``mylist-join@example.com``.
+message is destined for a valid endpoint, e.g. ``mylist-join@example.com``,
+that the message bytes can be parsed into a message object, and that the
+message has a `Message-ID` header.
Let's start a testable LMTP runner.
@@ -62,6 +64,9 @@ Once the mailing list is created, the posting address is valid.
... """)
{}
+Since the message itself is valid, it gets parsed and lands in the incoming
+queue.
+
>>> from mailman.testing.helpers import get_queue_messages
>>> messages = get_queue_messages('in')
>>> len(messages)
@@ -71,6 +76,7 @@ Once the mailing list is created, the posting address is valid.
To: mylist@example.com
Subject: An interesting message
Message-ID: <badger>
+ X-Message-ID-Hash: JYMZWSQ4IC2JPKK7ZUONRFRVC4ZYJGKJ
X-MailFrom: anne.person@example.com
<BLANKLINE>
This is an interesting message.
@@ -85,8 +91,8 @@ Once the mailing list is created, the posting address is valid.
Sub-addresses
=============
-The LMTP server understands each of the list's sub-addreses, such as -join,
--leave, -request and so on. If the message is posted to an invalid
+The LMTP server understands each of the list's sub-addreses, such as `-join`,
+`-leave`, `-request` and so on. If the message is posted to an invalid
sub-address though, it is rejected.
>>> lmtp.sendmail(
@@ -122,8 +128,8 @@ Request subaddress
------------------
Depending on the subaddress, there is a message in the appropriate queue for
-later processing. For example, all -request messages are put into the command
-queue for processing.
+later processing. For example, all `-request` messages are put into the
+command queue for processing.
>>> messages = get_queue_messages('command')
>>> len(messages)
@@ -133,6 +139,7 @@ queue for processing.
To: mylist-request@example.com
Subject: Help
Message-ID: <dog>
+ X-Message-ID-Hash: 4SKREUSPI62BHDMFBSOZ3BMXFETSQHNA
X-MailFrom: anne.person@example.com
<BLANKLINE>
Please help me.
@@ -147,7 +154,7 @@ queue for processing.
Bounce processor
----------------
-A message to the -bounces address goes to the bounce processor.
+A message to the `-bounces` address goes to the bounce processor.
>>> lmtp.sendmail(
... 'mail-daemon@example.com',
@@ -283,7 +290,7 @@ Confirmation messages go to the command processor...
Incoming processor
------------------
-Messages to the -owner address go to the incoming processor.
+Messages to the `-owner` address also go to the incoming processor.
>>> lmtp.sendmail(
... 'anne.person@example.com',
@@ -307,7 +314,5 @@ Messages to the -owner address go to the incoming processor.
version : ...
-Clean up
-========
-
- >>> master.stop()
+.. Clean up
+ >>> master.stop()
diff --git a/src/mailman/runners/lmtp.py b/src/mailman/runners/lmtp.py
index 6824491eb..bee111ad1 100644
--- a/src/mailman/runners/lmtp.py
+++ b/src/mailman/runners/lmtp.py
@@ -44,6 +44,7 @@ from mailman.core.runner import Runner
from mailman.database.transaction import txn
from mailman.email.message import Message
from mailman.interfaces.listmanager import IListManager
+from mailman.utilities.email import add_message_hash
elog = logging.getLogger('mailman.error')
qlog = logging.getLogger('mailman.runner')
@@ -82,6 +83,7 @@ ERR_451 = '451 Requested action aborted: error in processing'
ERR_501 = '501 Message has defects'
ERR_502 = '502 Error: command HELO not implemented'
ERR_550 = '550 Requested action not taken: mailbox unavailable'
+ERR_550_MID = '550 No Message-ID header provided'
# XXX Blech
smtpd.__version__ = 'Python LMTP runner 1.0'
@@ -159,15 +161,20 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
# Parse the message data. If there are any defects in the
# message, reject it right away; it's probably spam.
msg = email.message_from_string(data, Message)
- msg.original_size = len(data)
- if msg.defects:
- return ERR_501
- msg['X-MailFrom'] = mailfrom
- message_id = msg['message-id']
except Exception:
elog.exception('LMTP message parsing')
config.db.abort()
return CRLF.join(ERR_451 for to in rcpttos)
+ # Do basic post-processing of the message, checking it for defects or
+ # other missing information.
+ message_id = msg.get('message-id')
+ if message_id is None:
+ return ERR_550_MID
+ if msg.defects:
+ return ERR_501
+ msg.original_size = len(data)
+ add_message_hash(msg)
+ msg['X-MailFrom'] = mailfrom
# RFC 2033 requires us to return a status code for every recipient.
status = []
# Now for each address in the recipients, parse the address to first
diff --git a/src/mailman/runners/maildir.py b/src/mailman/runners/maildir.py
deleted file mode 100644
index 2d3a49285..000000000
--- a/src/mailman/runners/maildir.py
+++ /dev/null
@@ -1,195 +0,0 @@
-# Copyright (C) 2002-2012 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/>.
-
-"""Maildir runner.
-
-Most MTAs can be configured to deliver messages to a `Maildir'[1]. This
-runner will read messages from a maildir's new/ directory and inject them into
-Mailman's qfiles/in directory for processing in the normal pipeline. This
-delivery mechanism contrasts with mail program delivery, where incoming
-messages end up in qfiles/in via the MTA executing the scripts/post script
-(and likewise for the other -aliases for each mailing list).
-
-The advantage to Maildir delivery is that it is more efficient; there's no
-need to fork an intervening program just to take the message from the MTA's
-standard output, to the qfiles/in directory.
-
-[1] http://cr.yp.to/proto/maildir.html
-
-We're going to use the :info flag == 1, experimental status flag for our own
-purposes. The :1 can be followed by one of these letters:
-
-- P means that MaildirRunner's in the process of parsing and enqueuing the
- message. If successful, it will delete the file.
-
-- X means something failed during the parse/enqueue phase. An error message
- will be logged to log/error and the file will be renamed <filename>:1,X.
- MaildirRunner will never automatically return to this file, but once the
- problem is fixed, you can manually move the file back to the new/ directory
- and MaildirRunner will attempt to re-process it. At some point we may do
- this automatically.
-
-See the variable USE_MAILDIR in Defaults.py.in for enabling this delivery
-mechanism.
-"""
-
-# NOTE: Maildir delivery is experimental in Mailman 2.1, and untested in
-# Mailman 3. Instead, use LMTP delivery for Mailman 3.
-
-import os
-import errno
-import logging
-
-from email.parser import Parser
-from email.utils import parseaddr
-
-from mailman.config import config
-from mailman.core.runner import Runner
-from mailman.core.switchboard import Switchboard
-from mailman.message import Message
-
-
-log = logging.getLogger('mailman.error')
-
-# We only care about the listname and the subq as in listname@ or
-# listname-request@
-subqnames = ('admin', 'bounces', 'confirm', 'join', 'leave',
- 'owner', 'request', 'subscribe', 'unsubscribe')
-
-
-def getlistq(address):
- localpart, domain = address.split('@', 1)
- # TK: FIXME I only know configs of Postfix.
- if config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
- p = localpart.split(config.POSTFIX_VIRTUAL_SEPARATOR, 1)
- if len(p) == 2:
- localpart, domain = p
- l = localpart.split('-')
- if l[-1] in subqnames:
- listname = '-'.join(l[:-1])
- subq = l[-1]
- else:
- listname = localpart
- subq = None
- return listname, subq, domain
-
-
-
-class MaildirRunner(Runner):
- # This class is much different than most runners because it pulls files
- # of a different format than what scripts/post and friends leaves. The
- # files this runner reads are just single message files as dropped into
- # the directory by the MTA. This runner will read the file, and enqueue
- # it in the expected qfiles directory for normal processing.
- def __init__(self, slice=None, numslices=1):
- # Don't call the base class constructor, but build enough of the
- # underlying attributes to use the base class's implementation.
- self._stop = 0
- self._dir = os.path.join(config.MAILDIR_DIR, 'new')
- self._cur = os.path.join(config.MAILDIR_DIR, 'cur')
- self._parser = Parser(Message)
-
- def _one_iteration(self):
- # Refresh this each time through the list.
- listnames = list(config.list_manager.names)
- # Cruise through all the files currently in the new/ directory
- try:
- files = os.listdir(self._dir)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- # Nothing's been delivered yet
- return 0
- for file in files:
- srcname = os.path.join(self._dir, file)
- dstname = os.path.join(self._cur, file + ':1,P')
- xdstname = os.path.join(self._cur, file + ':1,X')
- try:
- os.rename(srcname, dstname)
- except OSError, e:
- if e.errno == errno.ENOENT:
- # Some other MaildirRunner beat us to it
- continue
- log.error('Could not rename maildir file: %s', srcname)
- raise
- # Now open, read, parse, and enqueue this message
- try:
- fp = open(dstname)
- try:
- msg = self._parser.parse(fp)
- finally:
- fp.close()
- # Now we need to figure out which queue of which list this
- # message was destined for. See get_verp() in
- # mailman.app.bounces for why we do things this way.
- vals = []
- for header in ('delivered-to', 'envelope-to', 'apparently-to'):
- vals.extend(msg.get_all(header, []))
- for field in vals:
- to = parseaddr(field)[1].lower()
- if not to:
- continue
- listname, subq, domain = getlistq(to)
- listname = listname + '@' + domain
- if listname in listnames:
- break
- else:
- # As far as we can tell, this message isn't destined for
- # any list on the system. What to do?
- log.error('Message apparently not for any list: %s',
- xdstname)
- os.rename(dstname, xdstname)
- continue
- # BAW: blech, hardcoded
- msgdata = {'listname': listname}
- # -admin is deprecated
- if subq in ('bounces', 'admin'):
- queue = Switchboard('bounces', config.BOUNCEQUEUE_DIR)
- elif subq == 'confirm':
- msgdata['toconfirm'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- elif subq in ('join', 'subscribe'):
- msgdata['tojoin'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- elif subq in ('leave', 'unsubscribe'):
- msgdata['toleave'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- elif subq == 'owner':
- msgdata.update({
- 'toowner': True,
- 'envsender': config.SITE_OWNER_ADDRESS,
- 'pipeline': config.OWNER_PIPELINE,
- })
- queue = Switchboard('in', config.INQUEUE_DIR)
- elif subq is None:
- msgdata['tolist'] = 1
- queue = Switchboard('in', config.INQUEUE_DIR)
- elif subq == 'request':
- msgdata['torequest'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- else:
- log.error('Unknown sub-queue: %s', subq)
- os.rename(dstname, xdstname)
- continue
- queue.enqueue(msg, msgdata)
- os.unlink(dstname)
- except Exception, e:
- os.rename(dstname, xdstname)
- log.error('%s', e)
-
- def _clean_up(self):
- pass
diff --git a/src/mailman/runners/tests/test_confirm.py b/src/mailman/runners/tests/test_confirm.py
index 34a2af523..ea972c17f 100644
--- a/src/mailman/runners/tests/test_confirm.py
+++ b/src/mailman/runners/tests/test_confirm.py
@@ -155,7 +155,7 @@ Franziskanerstra=C3=9Fe
self.assertEqual(address.email, 'anne@example.org')
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
- self.assertEqual(messages[0].msgdata['recipients'],
+ self.assertEqual(messages[0].msgdata['recipients'],
set(['anne@example.org']))
def test_confirm_with_no_command_in_utf8_body(self):
@@ -188,7 +188,7 @@ Franziskanerstra=C3=9Fe
self.assertEqual(address.email, 'anne@example.org')
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
- self.assertEqual(messages[0].msgdata['recipients'],
+ self.assertEqual(messages[0].msgdata['recipients'],
set(['anne@example.org']))
def test_double_confirmation(self):
@@ -259,5 +259,5 @@ From: Anne Person <anne@example.org>
messages = get_queue_messages('virgin', sort_on='subject')
self.assertEqual(len(messages), 2)
message = messages[1].msg
- self.assertEqual(str(message['subject']),
+ self.assertEqual(str(message['subject']),
'Welcome to the "Test" mailing list')
diff --git a/src/mailman/runners/tests/test_lmtp.py b/src/mailman/runners/tests/test_lmtp.py
new file mode 100644
index 000000000..2c4defe59
--- /dev/null
+++ b/src/mailman/runners/tests/test_lmtp.py
@@ -0,0 +1,98 @@
+# Copyright (C) 2012 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/>.
+
+"""Tests for the LMTP server."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestLMTP',
+ ]
+
+
+import smtplib
+import unittest
+
+from mailman.app.lifecycle import create_list
+from mailman.config import config
+from mailman.testing.helpers import get_lmtp_client, get_queue_messages
+from mailman.testing.layers import LMTPLayer
+
+
+
+class TestLMTP(unittest.TestCase):
+ """Test various aspects of the LMTP server."""
+
+ layer = LMTPLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ config.db.commit()
+ self._lmtp = get_lmtp_client(quiet=True)
+ self._lmtp.lhlo('remote.example.org')
+
+ def tearDown(self):
+ self._lmtp.close()
+
+ def test_message_id_required(self):
+ # The message is rejected if it does not have a Message-ID header.
+ try:
+ self._lmtp.sendmail('anne@example.com', ['test@example.com'], """\
+From: anne@example.com
+To: test@example.com
+Subject: This has no Message-ID header
+
+""")
+ except smtplib.SMTPDataError as error:
+ pass
+ else:
+ raise AssertionError('SMTPDataError expected')
+ # LMTP returns a 550: Requested action not taken: mailbox unavailable
+ # (e.g., mailbox not found, no access, or command rejected for policy
+ # reasons)
+ self.assertEqual(error.smtp_code, 550)
+ self.assertEqual(error.smtp_error, 'No Message-ID header provided')
+
+ def test_message_id_hash_is_added(self):
+ self._lmtp.sendmail('anne@example.com', ['test@example.com'], """\
+From: anne@example.com
+To: test@example.com
+Message-ID: <ant>
+Subject: This has a Message-ID but no X-Message-ID-Hash
+
+""")
+ messages = get_queue_messages('in')
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(messages[0].msg['x-message-id-hash'],
+ 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
+
+ def test_original_message_id_hash_is_overwritten(self):
+ self._lmtp.sendmail('anne@example.com', ['test@example.com'], """\
+From: anne@example.com
+To: test@example.com
+Message-ID: <ant>
+X-Message-ID-Hash: IGNOREME
+Subject: This has a Message-ID but no X-Message-ID-Hash
+
+""")
+ messages = get_queue_messages('in')
+ self.assertEqual(len(messages), 1)
+ all_headers = messages[0].msg.get_all('x-message-id-hash')
+ self.assertEqual(len(all_headers), 1)
+ self.assertEqual(messages[0].msg['x-message-id-hash'],
+ 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
diff --git a/src/mailman/styles/docs/__init__.py b/src/mailman/styles/docs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/styles/docs/__init__.py
diff --git a/src/mailman/app/docs/styles.rst b/src/mailman/styles/docs/styles.rst
index 63ec999bf..90a02227b 100644
--- a/src/mailman/app/docs/styles.rst
+++ b/src/mailman/styles/docs/styles.rst
@@ -2,9 +2,10 @@
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.
+List styles are a way to name and apply a template of attribute settings to
+new mailing lists. 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
@@ -22,26 +23,26 @@ Let's start with a vanilla mailing list and a default style manager.
>>> from mailman.styles.manager import StyleManager
>>> style_manager = StyleManager()
>>> style_manager.populate()
- >>> sorted(style.name for style in style_manager.styles)
- ['default']
+ >>> styles = sorted(style.name for style in style_manager.styles)
+ >>> len(styles)
+ 1
+ >>> print styles[0]
+ 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.
+There is a default style which implements a legacy style roughly corresponding
+to discussion mailing lists. This style matches 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'
+ >>> print 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
@@ -49,8 +50,11 @@ 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']
+ >>> matched_styles = [style.name for style in style_manager.lookup(mlist)]
+ >>> len(matched_styles)
+ 1
+ >>> print matched_styles[0]
+ default
Registering styles
@@ -60,13 +64,13 @@ New styles must implement the ``IStyle`` interface.
>>> from zope.interface import implements
>>> from mailman.interfaces.styles import IStyle
- >>> class TestStyle(object):
+ >>> class TestStyle:
... implements(IStyle)
... name = 'test'
... priority = 10
... def apply(self, mailing_list):
... # Just does something very simple.
- ... mailing_list.msg_footer = 'test footer'
+ ... mailing_list.style_thing = 'thing 1'
... def match(self, mailing_list, styles):
... # Applies to any test list
... if 'test' in mailing_list.fqdn_listname:
@@ -76,16 +80,20 @@ 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
+And now if you look up 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']
+ >>> matched_styles = sorted(
+ ... style.name for style in style_manager.lookup(mlist))
+ >>> len(matched_styles)
+ 1
+ >>> print matched_styles[0]
+ test
>>> for style in style_manager.lookup(mlist):
... style.apply(mlist)
- >>> print mlist.msg_footer
- test footer
+ >>> print mlist.style_thing
+ thing 1
Style priority
@@ -101,16 +109,16 @@ applied last.
... priority = 5
... # Use the base class's match() method.
... def apply(self, mailing_list):
- ... mailing_list.msg_footer = 'another footer'
+ ... mailing_list.style_thing = 'thing 2'
- >>> mlist.msg_footer = ''
- >>> mlist.msg_footer
- u''
+ >>> mlist.style_thing = 'thing 0'
+ >>> print mlist.style_thing
+ thing 0
>>> style_manager.register(AnotherTestStyle())
>>> for style in style_manager.lookup(mlist):
... style.apply(mlist)
- >>> print mlist.msg_footer
- another footer
+ >>> print mlist.style_thing
+ thing 2
You can change the priority of a style, and if you reapply the styles, they
will take effect in the new priority order.
@@ -121,8 +129,8 @@ will take effect in the new priority order.
>>> style_2.priority = 10
>>> for style in style_manager.lookup(mlist):
... style.apply(mlist)
- >>> print mlist.msg_footer
- test footer
+ >>> print mlist.style_thing
+ thing 1
Unregistering styles
@@ -131,32 +139,9 @@ 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'
+ >>> matched_styles = sorted(
+ ... style.name for style in style_manager.lookup(mlist))
+ >>> len(matched_styles)
+ 1
+ >>> print matched_styles[0]
+ test
diff --git a/src/mailman/styles/tests/__init__.py b/src/mailman/styles/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/styles/tests/__init__.py
diff --git a/src/mailman/styles/tests/test_styles.py b/src/mailman/styles/tests/test_styles.py
new file mode 100644
index 000000000..ce8b5064d
--- /dev/null
+++ b/src/mailman/styles/tests/test_styles.py
@@ -0,0 +1,90 @@
+# Copyright (C) 2012 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/>.
+
+"""Test styles."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestStyle',
+ ]
+
+
+import unittest
+
+from zope.component import getUtility
+from zope.interface import implements
+from zope.interface.exceptions import DoesNotImplement
+
+from mailman.interfaces.styles import (
+ DuplicateStyleError, IStyle, IStyleManager)
+from mailman.testing.layers import ConfigLayer
+
+
+
+class DummyStyle:
+ implements(IStyle)
+
+ name = 'dummy'
+ priority = 1
+
+ def apply(self, mlist):
+ pass
+
+ def match(self, mlist, styles):
+ styles.append(self)
+
+
+
+class TestStyle(unittest.TestCase):
+ """Test styles."""
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self.manager = getUtility(IStyleManager)
+
+ def test_register_style_again(self):
+ # Registering a style with the same name as a previous style raises an
+ # exception.
+ self.manager.register(DummyStyle())
+ try:
+ self.manager.register(DummyStyle())
+ except DuplicateStyleError:
+ pass
+ else:
+ raise AssertionError('DuplicateStyleError exception expected')
+
+ def test_register_a_non_style(self):
+ # You can't register something that doesn't implement the IStyle
+ # interface.
+ try:
+ self.manager.register(object())
+ except DoesNotImplement:
+ pass
+ else:
+ raise AssertionError('DoesNotImplement exception expected')
+
+ def test_unregister_a_non_registered_style(self):
+ # You cannot unregister a style that hasn't yet been registered.
+ try:
+ self.manager.unregister(DummyStyle())
+ except KeyError:
+ pass
+ else:
+ raise AssertionError('KeyError expected')
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index e61aca605..58c72d6d9 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -223,7 +223,7 @@ class LMTP(smtplib.SMTP):
return code, msg
-def get_lmtp_client():
+def get_lmtp_client(quiet=False):
"""Return a connected LMTP client."""
# It's possible the process has started but is not yet accepting
# connections. Wait a little while.
@@ -233,7 +233,8 @@ def get_lmtp_client():
try:
response = lmtp.connect(
config.mta.lmtp_host, int(config.mta.lmtp_port))
- print response
+ if not quiet:
+ print response
return lmtp
except socket.error as error:
if error[0] == errno.ECONNREFUSED:
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 5cee8c8d2..04ab8f91f 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'ConfigLayer',
+ 'LMTPLayer',
'MockAndMonkeyLayer',
'RESTLayer',
'SMTPLayer',
@@ -48,7 +49,8 @@ from mailman.core import initialize
from mailman.core.initialize import INHIBIT_CONFIG_FILE
from mailman.core.logging import get_handler
from mailman.interfaces.domain import IDomainManager
-from mailman.testing.helpers import TestableMaster, reset_the_world
+from mailman.testing.helpers import (
+ TestableMaster, get_lmtp_client, reset_the_world)
from mailman.testing.mta import ConnectionCountingController
from mailman.utilities.string import expand
@@ -250,6 +252,8 @@ class SMTPLayer(ConfigLayer):
@classmethod
def testSetUp(cls):
+ # Make sure we don't call our superclass's testSetUp(), otherwise the
+ # example.com domain will get added twice.
pass
@classmethod
@@ -259,6 +263,35 @@ class SMTPLayer(ConfigLayer):
+class LMTPLayer(ConfigLayer):
+ """Layer for starting, stopping, and accessing a test LMTP server."""
+
+ lmtpd = None
+
+ @staticmethod
+ def _wait_for_lmtp_server():
+ get_lmtp_client(quiet=True)
+
+ @classmethod
+ def setUp(cls):
+ assert cls.lmtpd is None, 'Layer already set up'
+ cls.lmtpd = TestableMaster(cls._wait_for_lmtp_server)
+ cls.lmtpd.start('lmtp')
+
+ @classmethod
+ def tearDown(cls):
+ assert cls.lmtpd is not None, 'Layer not set up'
+ cls.lmtpd.stop()
+ cls.lmtpd = None
+
+ @classmethod
+ def testSetUp(cls):
+ # Make sure we don't call our superclass's testSetUp(), otherwise the
+ # example.com domain will get added twice.
+ pass
+
+
+
class RESTLayer(SMTPLayer):
"""Layer for starting, stopping, and accessing the test REST layer."""
diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg
index 89d986a7c..526093572 100644
--- a/src/mailman/testing/testing.cfg
+++ b/src/mailman/testing/testing.cfg
@@ -45,9 +45,6 @@ max_restarts: 1
[runner.lmtp]
max_restarts: 1
-[runner.maildir]
-max_restarts: 1
-
[runner.news]
max_restarts: 1
diff --git a/src/mailman/utilities/email.py b/src/mailman/utilities/email.py
index 4b1023ce1..38c9980a2 100644
--- a/src/mailman/utilities/email.py
+++ b/src/mailman/utilities/email.py
@@ -21,10 +21,17 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
+ 'add_message_hash',
'split_email',
]
+from base64 import b32encode
+from hashlib import sha1
+
+
+
+
def split_email(address):
"""Split an email address into a user name and domain.
@@ -39,3 +46,29 @@ def split_email(address):
# There was no at-sign in the email address.
return local_part, None
return local_part, domain.split('.')
+
+
+def add_message_hash(msg):
+ """Add a X-Message-ID-Hash header derived from Message-ID.
+
+ This function works by side-effect; the original message is mutated. Any
+ existing X-Message-ID-Headers are deleted if a Message-ID header is
+ found. If no Message-ID header is found, the original message is not
+ modified.
+
+ :param msg: An email message
+ :type msg: `email.message.Message` or derived
+ """
+ message_id = msg.get('message-id')
+ if message_id is None:
+ return
+ # The angle brackets are not part of the Message-ID. See RFC 2822
+ # and http://wiki.list.org/display/DEV/Stable+URLs
+ if message_id.startswith('<') and message_id.endswith('>'):
+ message_id = message_id[1:-1]
+ else:
+ message_id = message_id.strip()
+ digest = sha1(message_id).digest()
+ message_id_hash = b32encode(digest)
+ del msg['x-message-id-hash']
+ msg['X-Message-ID-Hash'] = message_id_hash
diff --git a/src/mailman/interact.py b/src/mailman/utilities/interact.py
index 25dcfbc85..dd6bac9f0 100644
--- a/src/mailman/interact.py
+++ b/src/mailman/utilities/interact.py
@@ -24,6 +24,7 @@ __all__ = [
'interact',
]
+
import os
import sys
import code
diff --git a/src/mailman/utilities/tests/test_email.py b/src/mailman/utilities/tests/test_email.py
index 833d631b5..478322aaf 100644
--- a/src/mailman/utilities/tests/test_email.py
+++ b/src/mailman/utilities/tests/test_email.py
@@ -15,22 +15,27 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""Testing app.bounces functions."""
+"""Testing functions in the email utilities."""
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestEmail',
]
import unittest
-from mailman.utilities.email import split_email
+from mailman.testing.helpers import (
+ specialized_message_from_string as mfs)
+from mailman.utilities.email import add_message_hash, split_email
class TestEmail(unittest.TestCase):
+ """Testing functions in the email utilities."""
+
def test_normal_split(self):
self.assertEqual(split_email('anne@example.com'),
('anne', ['example', 'com']))
@@ -39,3 +44,67 @@ class TestEmail(unittest.TestCase):
def test_no_at_split(self):
self.assertEqual(split_email('anne'), ('anne', None))
+
+ def test_adding_the_message_hash(self):
+ # When the message has a Message-ID header, this will add the
+ # X-Mailman-Hash-ID header.
+ msg = mfs("""\
+Message-ID: <aardvark>
+
+""")
+ add_message_hash(msg)
+ self.assertEqual(msg['x-message-id-hash'],
+ '75E2XSUXAFQGWANWEROVQ7JGYMNWHJBT')
+
+ def test_remove_hash_headers_first(self):
+ # Any existing X-Mailman-Hash-ID header is removed first.
+ msg = mfs("""\
+Message-ID: <aardvark>
+X-Message-ID-Hash: abc
+
+""")
+ add_message_hash(msg)
+ headers = msg.get_all('x-message-id-hash')
+ self.assertEqual(len(headers), 1)
+ self.assertEqual(headers[0], '75E2XSUXAFQGWANWEROVQ7JGYMNWHJBT')
+
+ def test_hash_header_left_alone_if_no_message_id(self):
+ # If the original message has no Message-ID header, then any existing
+ # X-Message-ID-Hash headers are left intact.
+ msg = mfs("""\
+X-Message-ID-Hash: abc
+
+""")
+ add_message_hash(msg)
+ headers = msg.get_all('x-message-id-hash')
+ self.assertEqual(len(headers), 1)
+ self.assertEqual(headers[0], 'abc')
+
+ def test_angle_brackets_dont_contribute_to_hash(self):
+ # According to RFC 5322, the [matching] angle brackets do not
+ # contribute to the hash.
+ msg = mfs("""\
+Message-ID: aardvark
+
+""")
+ add_message_hash(msg)
+ self.assertEqual(msg['x-message-id-hash'],
+ '75E2XSUXAFQGWANWEROVQ7JGYMNWHJBT')
+
+ def test_mismatched_angle_brackets_do_contribute_to_hash(self):
+ # According to RFC 5322, the [matching] angle brackets do not
+ # contribute to the hash.
+ msg = mfs("""\
+Message-ID: <aardvark
+
+""")
+ add_message_hash(msg)
+ self.assertEqual(msg['x-message-id-hash'],
+ 'AOJ545GHRYD2Y3RUFG2EWMPHUABTG4SM')
+ msg = mfs("""\
+Message-ID: aardvark>
+
+""")
+ add_message_hash(msg)
+ self.assertEqual(msg['x-message-id-hash'],
+ '5KH3RA7ZM4VM6XOZXA7AST2XN2X4S3WY')
diff --git a/src/mailman/version.py b/src/mailman/version.py
index 00c5b6c7a..8f13f8f51 100644
--- a/src/mailman/version.py
+++ b/src/mailman/version.py
@@ -17,15 +17,15 @@
"""Mailman version strings."""
-# Mailman version
+# Mailman version.
VERSION = '3.0.0a8+'
CODENAME = "The Twilight Zone"
-# And as a hex number in the manner of PY_VERSION_HEX
+# And as a hex number in the manner of PY_VERSION_HEX.
ALPHA = 0xa
BETA = 0xb
GAMMA = 0xc
-# release candidates
+# Release candidates.
RC = GAMMA
FINAL = 0xf
@@ -33,16 +33,16 @@ MAJOR_REV = 3
MINOR_REV = 0
MICRO_REV = 0
REL_LEVEL = BETA
-# at most 15 beta releases!
+# At most 15 beta releases!
REL_SERIAL = 1
HEX_VERSION = ((MAJOR_REV << 24) | (MINOR_REV << 16) | (MICRO_REV << 8) |
(REL_LEVEL << 4) | (REL_SERIAL << 0))
-# qfile/*.db schema version number
+# queue/*.pck schema version number.
QFILE_SCHEMA_VERSION = 3
-# Printable version string used by command line scripts
+# Printable version string used by command line scripts.
MAILMAN_VERSION = 'GNU Mailman ' + VERSION
MAILMAN_VERSION_FULL = MAILMAN_VERSION + ' (' + CODENAME + ')'