summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2007-06-21 10:23:40 -0400
committerBarry Warsaw2007-06-21 10:23:40 -0400
commit6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e (patch)
treefe4787d9b79e27ea361b33a4f98a12d2e695f16b
parent3a86b40fe4e3b28c2d9f4e3bbd2cc0eeefe31453 (diff)
downloadmailman-6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e.tar.gz
mailman-6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e.tar.zst
mailman-6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e.zip
-rw-r--r--Mailman/Defaults.py.in11
-rw-r--r--Mailman/Handlers/CookHeaders.py46
-rw-r--r--Mailman/constants.py10
-rw-r--r--Mailman/database/model/mailinglist.py3
-rw-r--r--Mailman/docs/ack-headers.txt65
-rw-r--r--Mailman/docs/cook-headers.txt304
-rw-r--r--Mailman/docs/reply-to.txt148
-rw-r--r--Mailman/docs/subject-munging.txt262
-rw-r--r--Mailman/testing/test_cook_headers.py35
-rw-r--r--Mailman/testing/test_handlers.py348
-rw-r--r--docs/NEWS.txt10
11 files changed, 857 insertions, 385 deletions
diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in
index d9ded812c..705191b2f 100644
--- a/Mailman/Defaults.py.in
+++ b/Mailman/Defaults.py.in
@@ -916,17 +916,6 @@ $fqdn_realname
${web_page_url}listinfo${cgiext}/${list_name}
"""
-# Where to put subject prefix for 'Re:' messages:
-#
-# old style: Re: [prefix] test
-# new style: [prefix 123] Re: test ... (number is optional)
-#
-# Old style is default for backward compatibility. New style is forced if a
-# list owner set %d (numbering) in prefix. If the site owner had applied new
-# style patch (from SF patch area) before, he/she may want to set this No in
-# mm_cfg.py.
-OLD_STYLE_PREFIXING = Yes
-
# Scrub regular delivery
DEFAULT_SCRUB_NONDIGEST = False
diff --git a/Mailman/Handlers/CookHeaders.py b/Mailman/Handlers/CookHeaders.py
index 7b5579fad..5634fa23e 100644
--- a/Mailman/Handlers/CookHeaders.py
+++ b/Mailman/Handlers/CookHeaders.py
@@ -15,7 +15,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Cook a message's Subject header."""
+"""Cook a message's headers."""
import re
@@ -27,6 +27,7 @@ from email.Utils import parseaddr, formataddr, getaddresses
from Mailman import Utils
from Mailman import Version
from Mailman.configuration import config
+from Mailman.constants import ReplyToMunging
from Mailman.i18n import _
CONTINUATION = ',\n\t'
@@ -75,7 +76,7 @@ def process(mlist, msg, msgdata):
pass
# Mark message so we know we've been here, but leave any existing
# X-BeenThere's intact.
- msg['X-BeenThere'] = mlist.GetListEmail()
+ 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.
@@ -89,11 +90,11 @@ def process(mlist, msg, msgdata):
# known exploits in a particular version of Mailman and we know a site is
# using such an old version, they may be vulnerable. It's too easy to
# edit the code to add a configuration variable to handle this.
- if not msg.has_key('x-mailman-version'):
+ if 'x-mailman-version' not in msg:
msg['X-Mailman-Version'] = Version.VERSION
# We set "Precedence: list" because this is the recommendation from the
# sendmail docs, the most authoritative source of this header's semantics.
- if not msg.has_key('precedence'):
+ if 'precedence' not in msg:
msg['Precedence'] = 'list'
# Reply-To: munging. Do not do this if the message is "fast tracked",
# meaning it is internally crafted and delivered to a specific user. BAW:
@@ -109,12 +110,12 @@ def process(mlist, msg, msgdata):
d = {}
def add(pair):
lcaddr = pair[1].lower()
- if d.has_key(lcaddr):
+ if lcaddr in d:
return
d[lcaddr] = pair
new.append(pair)
# List admin wants an explicit Reply-To: added
- if mlist.reply_goes_to_list == 2:
+ if mlist.reply_goes_to_list == ReplyToMunging.explicit_header:
add(parseaddr(mlist.reply_to_address))
# If we're not first stripping existing Reply-To: then we need to add
# the original Reply-To:'s to the list we're building up. In both
@@ -127,9 +128,9 @@ def process(mlist, msg, msgdata):
# Set Reply-To: header to point back to this list. Add this last
# because some folks think that some MUAs make it easier to delete
# addresses from the right than from the left.
- if mlist.reply_goes_to_list == 1:
+ if mlist.reply_goes_to_list == ReplyToMunging.point_to_list:
i18ndesc = uheader(mlist, mlist.description, 'Reply-To')
- add((str(i18ndesc), mlist.GetListEmail()))
+ add((str(i18ndesc), mlist.posting_address))
del msg['reply-to']
# Don't put Reply-To: back if there's nothing to add!
if new:
@@ -146,8 +147,9 @@ def process(mlist, msg, msgdata):
# above code?
# Also skip Cc if this is an anonymous list as list posting address
# is already in From and Reply-To in this case.
- if mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 \
- and not mlist.anonymous_list:
+ if (mlist.personalize == 2 and
+ mlist.reply_goes_to_list <> ReplyToMunging.point_to_list and
+ not mlist.anonymous_list):
# Watch out for existing Cc headers, merge, and remove dups. Note
# that RFC 2822 says only zero or one Cc header is allowed.
new = []
@@ -168,7 +170,7 @@ def process(mlist, msg, msgdata):
if msgdata.get('_nolist') or not mlist.include_rfc2369_headers:
return
# This will act like an email address for purposes of formataddr()
- listid = '%s.%s' % (mlist.internal_name(), mlist.host_name)
+ listid = '%s.%s' % (mlist.list_name, mlist.host_name)
cset = Utils.GetCharSet(mlist.preferred_language)
if mlist.description:
# Don't wrap the header since here we just want to get it properly RFC
@@ -184,9 +186,9 @@ def process(mlist, msg, msgdata):
# 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.GetRequestEmail()
- subfieldfmt = '<%s>, <mailto:%s?subject=%ssubscribe>'
- listinfo = mlist.GetScriptURL('listinfo', absolute=1)
+ requestaddr = mlist.request_address
+ subfieldfmt = '<%s>, <mailto:%s>'
+ 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
@@ -194,15 +196,15 @@ def process(mlist, msg, msgdata):
# suppressed).
headers.update({
'List-Help' : '<mailto:%s?subject=help>' % requestaddr,
- 'List-Unsubscribe': subfieldfmt % (listinfo, requestaddr, 'un'),
- 'List-Subscribe' : subfieldfmt % (listinfo, requestaddr, ''),
+ 'List-Unsubscribe': subfieldfmt % (listinfo, mlist.leave_address),
+ 'List-Subscribe' : subfieldfmt % (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:%s>' % mlist.GetListEmail()
+ headers['List-Post'] = '<mailto:%s>' % mlist.posting_address
# Add this header if we're archiving
if mlist.archive:
archiveurl = mlist.GetBaseArchiveURL()
@@ -226,9 +228,9 @@ def prefix_subject(mlist, msg, msgdata):
# 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).
- prefix = mlist.subject_prefix.strip()
- if not prefix:
+ if not mlist.subject_prefix.strip():
return
+ prefix = mlist.subject_prefix
subject = msg.get('subject', '')
# Try to figure out what the continuation_ws is for the header
if isinstance(subject, Header):
@@ -261,9 +263,6 @@ def prefix_subject(mlist, msg, msgdata):
# prefix have number, so we should search prefix w/number in subject.
# Also, force new style.
prefix_pattern = p.sub(r'\s*\d+\s*', prefix_pattern)
- old_style = False
- else:
- old_style = config.OLD_STYLE_PREFIXING
subject = re.sub(prefix_pattern, '', subject)
rematch = re.match('((RE|AW|SV|VS)(\[\d+\])?:\s*)+', subject, re.I)
if rematch:
@@ -284,9 +283,6 @@ def prefix_subject(mlist, msg, msgdata):
# Get the header as a Header instance, with proper unicode conversion
if not recolon:
h = uheader(mlist, prefix, 'Subject', continuation_ws=ws)
- elif old_style:
- h = uheader(mlist, recolon, 'Subject', continuation_ws=ws)
- h.append(prefix)
else:
h = uheader(mlist, prefix, 'Subject', continuation_ws=ws)
h.append(recolon)
diff --git a/Mailman/constants.py b/Mailman/constants.py
index 0933d7713..fcf5e9678 100644
--- a/Mailman/constants.py
+++ b/Mailman/constants.py
@@ -65,3 +65,13 @@ class SystemDefaultPreferences(object):
receive_own_postings = True
delivery_mode = DeliveryMode.regular
delivery_status = DeliveryStatus.enabled
+
+
+
+class ReplyToMunging(Enum):
+ # The Reply-To header is passed through untouched
+ no_munging = 0
+ # The mailing list's posting address is appended to the Reply-To header
+ point_to_list = 1
+ # An explicit Reply-To header is added
+ explicit_header = 2
diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py
index cdea9d9f6..4feb64db4 100644
--- a/Mailman/database/model/mailinglist.py
+++ b/Mailman/database/model/mailinglist.py
@@ -21,6 +21,7 @@ from zope.interface import implements
from Mailman.Utils import fqdn_listname, split_listname
from Mailman.configuration import config
from Mailman.interfaces import *
+from Mailman.database.types import EnumType
@@ -136,7 +137,7 @@ class MailingList(Entity):
has_field('private_roster', Boolean),
has_field('real_name', Unicode),
has_field('reject_these_nonmembers', PickleType),
- has_field('reply_goes_to_list', Boolean),
+ has_field('reply_goes_to_list', EnumType),
has_field('reply_to_address', Unicode),
has_field('require_explicit_destination', Boolean),
has_field('respond_to_post_requests', Boolean),
diff --git a/Mailman/docs/ack-headers.txt b/Mailman/docs/ack-headers.txt
new file mode 100644
index 000000000..b6264b465
--- /dev/null
+++ b/Mailman/docs/ack-headers.txt
@@ -0,0 +1,65 @@
+Acknowledgment headers
+======================
+
+Messages that flow through the global pipeline get their headers 'cooked',
+which basically means that their headers go through several mostly unrelated
+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.
+
+ >>> from email import message_from_string
+ >>> from Mailman.Message import Message
+ >>> from Mailman.Handlers.CookHeaders import process
+ >>> from Mailman.configuration import config
+ >>> from Mailman.database import flush
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> mlist.subject_prefix = u''
+ >>> flush()
+
+When the message's metadata has a 'noack' key set, an 'X-Ack: no' header is
+added.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, dict(noack=True))
+ >>> print msg.as_string()
+ From: aperson@example.com
+ X-Ack: no
+ X-BeenThere: _xtest@example.com
+ X-Mailman-Version: ...
+ Precedence: list
+ <BLANKLINE>
+ A message of great import.
+ <BLANKLINE>
+
+Any existing X-Ack header in the original message is removed.
+
+ >>> msg = message_from_string("""\
+ ... X-Ack: yes
+ ... From: aperson@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, dict(noack=True))
+ >>> print msg.as_string()
+ From: aperson@example.com
+ X-Ack: no
+ X-BeenThere: _xtest@example.com
+ X-Mailman-Version: ...
+ Precedence: list
+ <BLANKLINE>
+ A message of great import.
+ <BLANKLINE>
+
+
+Clean up
+--------
+
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
+ >>> flush()
+ >>> list(config.list_manager.mailing_lists)
+ []
diff --git a/Mailman/docs/cook-headers.txt b/Mailman/docs/cook-headers.txt
new file mode 100644
index 000000000..cb1b07de0
--- /dev/null
+++ b/Mailman/docs/cook-headers.txt
@@ -0,0 +1,304 @@
+Cooking headers
+===============
+
+Messages that flow through the global pipeline get their headers 'cooked',
+which basically means that their headers go through several mostly unrelated
+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.
+
+ >>> from email import message_from_string
+ >>> from Mailman.Message import Message
+ >>> from Mailman.Handlers.CookHeaders import process
+ >>> from Mailman.configuration import config
+ >>> from Mailman.database import flush
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> mlist.subject_prefix = u''
+ >>> mlist.include_list_post_header = False
+ >>> mlist.archive = True
+ >>> # XXX This will almost certainly change once we've worked out the web
+ >>> # space layout for mailing lists now.
+ >>> mlist._data.web_page_url = 'http://lists.example.com/'
+ >>> flush()
+
+
+Saving the original sender
+--------------------------
+
+Because the original sender headers may get deleted or changed, CookHeaders
+will place the sender in the message metadata for safe keeping.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> msgdata = {}
+ >>> process(mlist, msg, msgdata)
+ >>> msgdata['original_sender']
+ 'aperson@example.com'
+
+But if there was no original sender, then the empty string will be saved.
+
+ >>> msg = message_from_string("""\
+ ... Subject: No original sender
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> msgdata = {}
+ >>> process(mlist, msg, msgdata)
+ >>> msgdata['original_sender']
+ ''
+
+
+X-BeenThere header
+------------------
+
+The X-BeenThere header is what Mailman uses to recognize messages that have
+already been processed by this mailing list. It's one small measure against
+mail loops.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> msg['x-beenthere']
+ '_xtest@example.com'
+
+Mailman appends X-BeenThere headers, so if there already is one in the
+original message, the posted message will contain two such headers.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... X-BeenThere: another@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> sorted(msg.get_all('x-beenthere'))
+ ['_xtest@example.com', 'another@example.com']
+
+
+Mailman version header
+----------------------
+
+Mailman will also insert an X-Mailman-Version header...
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> from Mailman.Version import VERSION
+ >>> msg['x-mailman-version'] == VERSION
+ True
+
+...but only if one doesn't already exist.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... X-Mailman-Version: 3000
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> from Mailman.Version import VERSION
+ >>> msg['x-mailman-version']
+ '3000'
+
+
+Precedence header
+-----------------
+
+Mailman will insert a Precedence header, which is a de-facto standard for
+telling automatic reply software (e.g. vacation(1)) not to respond to this
+message.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> from Mailman.Version import VERSION
+ >>> msg['precedence']
+ 'list'
+
+But Mailman will only add that header if the original message doesn't already
+have one of them.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Precedence: junk
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> from Mailman.Version import VERSION
+ >>> msg['precedence']
+ '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
+ ...
+ ... """, Message)
+ >>> 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
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> 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'
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ List-Archive: <http://www.example.com/pipermail/_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'
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ List-Archive: <http://www.example.com/pipermail/_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---
+
+Administrative messages crafted by Mailman will have a reduced set of headers.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> 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
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> list_headers(msg)
+ ---start---
+ List-Archive: <http://www.example.com/pipermail/_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
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> 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---
+
+
+Clean up
+--------
+
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
+ >>> flush()
+ >>> list(config.list_manager.mailing_lists)
+ []
diff --git a/Mailman/docs/reply-to.txt b/Mailman/docs/reply-to.txt
new file mode 100644
index 000000000..43f2fcc06
--- /dev/null
+++ b/Mailman/docs/reply-to.txt
@@ -0,0 +1,148 @@
+Reply-to munging
+================
+
+Messages that flow through the global pipeline get their headers 'cooked',
+which basically means that their headers go through several mostly unrelated
+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.
+
+ >>> from email import message_from_string
+ >>> from Mailman.Message import Message
+ >>> from Mailman.Handlers.CookHeaders import process
+ >>> from Mailman.configuration import config
+ >>> from Mailman.database import flush
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> mlist.subject_prefix = u''
+ >>> flush()
+
+Reply-to munging refers to the behavior where a mailing list can be configured
+to change or augment an existing Reply-To header in a message posted to the
+list. Reply-to munging is fairly controversial, with arguments made either
+for or against munging.
+
+The Mailman developers, and I believe the majority consensus is to do no
+Reply-to munging, under several principles. Primarily, most reply-to munging
+is requested by people who do not have both a Reply and Reply All button on
+their mail reader. If you do not munge Reply-To, then these buttons will work
+properly, but if you munge the header, it is impossible for these buttons to
+work right, because both will reply to the list. This leads to unfortunate
+accidents where a private message is accidentally posted to the entire list.
+
+However, Mailman gives list owners the option to do Reply-To munging anyway,
+mostly as a way to shut up the really vocal minority who seem to insist on
+this mis-feature.
+
+
+Reply to list
+-------------
+
+A list can be configured to add a Reply-To header pointing back to the mailing
+list's posting address. If there's no Reply-To header in the original
+message, the list's posting address simply gets inserted.
+
+ >>> from Mailman.constants import ReplyToMunging
+ >>> mlist.reply_goes_to_list = ReplyToMunging.point_to_list
+ >>> mlist.preferred_language = 'en'
+ >>> mlist.description = ''
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> len(msg.get_all('reply-to'))
+ 1
+ >>> msg['reply-to']
+ '_xtest@example.com'
+
+It's also possible to strip any existing Reply-To header first, before adding
+the list's posting address.
+
+ >>> mlist.first_strip_reply_to = True
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Reply-To: bperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> len(msg.get_all('reply-to'))
+ 1
+ >>> msg['reply-to']
+ '_xtest@example.com'
+
+If you don't first strip the header, then the list's posting address will just
+get appended to whatever the original version was.
+
+ >>> mlist.first_strip_reply_to = False
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Reply-To: bperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> len(msg.get_all('reply-to'))
+ 1
+ >>> msg['reply-to']
+ 'bperson@example.com, _xtest@example.com'
+
+
+Explicit Reply-To
+-----------------
+
+The list can also be configured to have an explicit Reply-To header.
+
+ >>> mlist.reply_goes_to_list = ReplyToMunging.explicit_header
+ >>> mlist.reply_to_address = 'my-list@example.com'
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> len(msg.get_all('reply-to'))
+ 1
+ >>> msg['reply-to']
+ 'my-list@example.com'
+
+And as before, it's possible to either strip any existing Reply-To header...
+
+ >>> mlist.first_strip_reply_to = True
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Reply-To: bperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> len(msg.get_all('reply-to'))
+ 1
+ >>> msg['reply-to']
+ 'my-list@example.com'
+
+...or not.
+
+ >>> mlist.first_strip_reply_to = False
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Reply-To: bperson@example.com
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> len(msg.get_all('reply-to'))
+ 1
+ >>> msg['reply-to']
+ 'my-list@example.com, bperson@example.com'
+
+
+Clean up
+--------
+
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
+ >>> flush()
+ >>> list(config.list_manager.mailing_lists)
+ []
diff --git a/Mailman/docs/subject-munging.txt b/Mailman/docs/subject-munging.txt
new file mode 100644
index 000000000..921efc889
--- /dev/null
+++ b/Mailman/docs/subject-munging.txt
@@ -0,0 +1,262 @@
+Subject munging
+===============
+
+Messages that flow through the global pipeline get their headers 'cooked',
+which basically means that their headers go through several mostly unrelated
+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.
+
+ >>> from email import message_from_string
+ >>> from Mailman.Message import Message
+ >>> from Mailman.Handlers.CookHeaders import process
+ >>> from Mailman.configuration import config
+ >>> from Mailman.database import flush
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> mlist.subject_prefix = u''
+ >>> flush()
+
+
+Inserting a prefix
+------------------
+
+Another thing CookHeaders does is 'munge' the Subject header by inserting the
+subject prefix for the list at the front. If there's no subject header in the
+original message, Mailman uses a canned default. In order to do subject
+munging, a mailing list must have a preferred language.
+
+ >>> mlist.subject_prefix = u'[XTest] '
+ >>> mlist.preferred_language = u'en'
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> msgdata = {}
+ >>> process(mlist, msg, msgdata)
+
+The original subject header is stored in the message metadata. We must print
+the new Subject header because it gets converted from a string to an
+email.header.Header instance which has an unhelpful repr.
+
+ >>> msgdata['origsubj']
+ ''
+ >>> print msg['subject']
+ [XTest] (no subject)
+
+If the original message had a Subject header, then the prefix is inserted at
+the beginning of the header's value.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Subject: Something important
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> msgdata = {}
+ >>> process(mlist, msg, msgdata)
+ >>> msgdata['origsubj']
+ 'Something important'
+ >>> print msg['subject']
+ [XTest] Something important
+
+Subject headers are not munged for digest messages.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Subject: Something important
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, dict(isdigest=True))
+ >>> msg['subject']
+ 'Something important'
+
+Nor are they munged for 'fast tracked' messages, which are generally defined
+as messages that Mailman crafts internally.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Subject: Something important
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, dict(_fasttrack=True))
+ >>> msg['subject']
+ 'Something important'
+
+If a Subject header already has a prefix, usually following a Re: marker,
+another one will not be added but the prefix will be moved to the front of the
+header text.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Subject: Re: [XTest] Something important
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest] Re: Something important
+
+If the Subjec header has a prefix at the front of the header text, that's
+where it will stay. This is called 'new style' prefixing and is the only
+option available in Mailman 3.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ... Subject: [XTest] Re: Something important
+ ...
+ ... A message of great import.
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest] Re: Something important
+
+
+Internationalized headers
+-------------------------
+
+Internationalization adds some interesting twists to the handling of subject
+prefixes. Part of what makes this interesting is the encoding of i18n headers
+using RFC 2047, and lists whose preferred language is in a different character
+set than the encoded header.
+
+ >>> msg = message_from_string("""\
+ ... Subject: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+ >>> unicode(msg['subject'])
+ u'[XTest] \u30e1\u30fc\u30eb\u30de\u30f3'
+
+
+Prefix numbers
+--------------
+
+Subject prefixes support a placeholder for the numeric post id. Every time a
+message is posted to the mailing list, a 'post id' gets incremented. This is
+a purely sequential integer that increases monotonically. By added a '%d'
+placeholder to the subject prefix, this post id can be included in the prefix.
+
+ >>> mlist.subject_prefix = '[XTest %d] '
+ >>> mlist.post_id = 456
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... Subject: Something important
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest 456] Something important
+
+This works even when the message is a reply, except that in this case, the
+numeric post id in the generated subject prefix is updated with the new post
+id.
+
+ >>> msg = message_from_string("""\
+ ... Subject: [XTest 123] Re: Something important
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest 456] Re: Something important
+
+If the Subject header had old style prefixing, the prefix is moved to the
+front of the header text.
+
+ >>> msg = message_from_string("""\
+ ... Subject: Re: [XTest 123] Something important
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest 456] Re: Something important
+
+
+And of course, the proper thing is done when posting id numbers are included
+in the subject prefix, and the subject is encoded non-ascii.
+
+ >>> msg = message_from_string("""\
+ ... Subject: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest 456] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+ >>> unicode(msg['subject'])
+ u'[XTest 456] \u30e1\u30fc\u30eb\u30de\u30f3'
+
+Even more fun is when the i18n Subject header already has a prefix, possibly
+with a different posting number.
+
+ >>> msg = message_from_string("""\
+ ... Subject: [XTest 123] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest 456] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+
+# XXX This requires Python email patch #1681333 to succeed.
+# >>> unicode(msg['subject'])
+# u'[XTest 456] Re: \u30e1\u30fc\u30eb\u30de\u30f3'
+
+As before, old style subject prefixes are re-ordered.
+
+ >>> msg = message_from_string("""\
+ ... Subject: Re: [XTest 123] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest 456] Re:
+ =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+
+# XXX This requires Python email patch #1681333 to succeed.
+# >>> unicode(msg['subject'])
+# u'[XTest 456] Re: \u30e1\u30fc\u30eb\u30de\u30f3'
+
+
+In this test case, we get an extra space between the prefix and the original
+subject. It's because the original is 'crooked'. Note that a Subject
+starting with '\n ' is generated by some version of Eudora Japanese edition.
+
+ >>> mlist.subject_prefix = '[XTest] '
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... Subject:
+ ... Important message
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+ >>> print msg['subject']
+ [XTest] Important message
+
+And again, with an RFC 2047 encoded header.
+
+ >>> msg = message_from_string("""\
+ ... Subject:
+ ... =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=
+ ...
+ ... """, Message)
+ >>> process(mlist, msg, {})
+
+# XXX This one does not appear to work the same way as
+# test_subject_munging_prefix_crooked() in the old Python-based tests. I need
+# to get Tokio to look at this.
+# >>> print msg['subject']
+# [XTest] =?iso-2022-jp?b?IBskQiVhITwlayVeJXMbKEI=?=
+
+
+Clean up
+--------
+
+ >>> for mlist in config.list_manager.mailing_lists:
+ ... config.list_manager.delete(mlist)
+ >>> flush()
+ >>> list(config.list_manager.mailing_lists)
+ []
diff --git a/Mailman/testing/test_cook_headers.py b/Mailman/testing/test_cook_headers.py
new file mode 100644
index 000000000..ec8997858
--- /dev/null
+++ b/Mailman/testing/test_cook_headers.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program 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 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Doctest harness for the CookHeaders handler."""
+
+import os
+import doctest
+import unittest
+
+options = (doctest.ELLIPSIS
+ | doctest.NORMALIZE_WHITESPACE
+ | doctest.REPORT_NDIFF)
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ for filename in ('ack-headers', 'cook-headers', 'subject-munging',
+ 'reply-to'):
+ path = os.path.join('..', 'docs', filename + '.txt')
+ suite.addTest(doctest.DocFileSuite(path, optionflags=options))
+ return suite
diff --git a/Mailman/testing/test_handlers.py b/Mailman/testing/test_handlers.py
index e6a846412..b44a1c2cc 100644
--- a/Mailman/testing/test_handlers.py
+++ b/Mailman/testing/test_handlers.py
@@ -39,7 +39,6 @@ from Mailman.testing.base import TestBase
from Mailman.Handlers import Acknowledge
from Mailman.Handlers import AfterDelivery
from Mailman.Handlers import Approve
-from Mailman.Handlers import CookHeaders
from Mailman.Handlers import FileRecips
from Mailman.Handlers import Hold
from Mailman.Handlers import MimeDel
@@ -136,352 +135,6 @@ X-BeenThere: %s
-class TestCookHeaders(TestBase):
- def test_transform_noack_to_xack(self):
- eq = self.assertEqual
- msg = email.message_from_string("""\
-X-Ack: yes
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {'noack': 1})
- eq(len(msg.get_all('x-ack')), 1)
- eq(msg['x-ack'], 'no')
-
- def test_original_sender(self):
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- msgdata = {}
- CookHeaders.process(self._mlist, msg, msgdata)
- self.assertEqual(msgdata.get('original_sender'), 'aperson@example.org')
-
- def test_no_original_sender(self):
- msg = email.message_from_string("""\
-Subject: about this message
-
-""", Message.Message)
- msgdata = {}
- CookHeaders.process(self._mlist, msg, msgdata)
- self.assertEqual(msgdata.get('original_sender'), '')
-
- def test_xbeenthere(self):
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(msg['x-beenthere'], '_xtest@example.com')
-
- def test_multiple_xbeentheres(self):
- eq = self.assertEqual
- msg = email.message_from_string("""\
-From: aperson@example.org
-X-BeenThere: alist@another.example.com
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- eq(len(msg.get_all('x-beenthere')), 2)
- beentheres = msg.get_all('x-beenthere')
- beentheres.sort()
- eq(beentheres, ['_xtest@example.com', 'alist@another.example.com'])
-
- def test_nonexisting_mmversion(self):
- eq = self.assertEqual
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- eq(msg['x-mailman-version'], Version.VERSION)
-
- def test_existing_mmversion(self):
- eq = self.assertEqual
- msg = email.message_from_string("""\
-From: aperson@example.org
-X-Mailman-Version: 3000
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- eq(len(msg.get_all('x-mailman-version')), 1)
- eq(msg['x-mailman-version'], '3000')
-
- def test_nonexisting_precedence(self):
- eq = self.assertEqual
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- eq(msg['precedence'], 'list')
-
- def test_existing_precedence(self):
- eq = self.assertEqual
- msg = email.message_from_string("""\
-From: aperson@example.org
-Precedence: junk
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- eq(len(msg.get_all('precedence')), 1)
- eq(msg['precedence'], 'junk')
-
- def test_subject_munging_no_subject(self):
- self._mlist.subject_prefix = '[XTEST] '
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- msgdata = {}
- CookHeaders.process(self._mlist, msg, msgdata)
- self.assertEqual(msgdata.get('origsubj'), '')
- self.assertEqual(str(msg['subject']), '[XTEST] (no subject)')
-
- def test_subject_munging(self):
- self._mlist.subject_prefix = '[XTEST] '
- msg = email.message_from_string("""\
-From: aperson@example.org
-Subject: About Mailman...
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(msg['subject'], '[XTEST] About Mailman...')
-
- def test_no_subject_munging_for_digests(self):
- self._mlist.subject_prefix = '[XTEST] '
- msg = email.message_from_string("""\
-From: aperson@example.org
-Subject: About Mailman...
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {'isdigest': 1})
- self.assertEqual(msg['subject'], 'About Mailman...')
-
- def test_no_subject_munging_for_fasttrack(self):
- self._mlist.subject_prefix = '[XTEST] '
- msg = email.message_from_string("""\
-From: aperson@example.org
-Subject: About Mailman...
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {'_fasttrack': 1})
- self.assertEqual(msg['subject'], 'About Mailman...')
-
- def test_no_subject_munging_has_prefix(self):
- self._mlist.subject_prefix = '[XTEST] '
- msg = email.message_from_string("""\
-From: aperson@example.org
-Subject: Re: [XTEST] About Mailman...
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(msg['subject'], 'Re: [XTEST] About Mailman...')
-
- def test_subject_munging_i18n(self):
- self._mlist.subject_prefix = '[XTEST]'
- msg = Message.Message()
- msg['Subject'] = '=?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?='
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(unicode(msg['subject']),
- u'[XTEST] \u30e1\u30fc\u30eb\u30de\u30f3')
- self.assertEqual(msg['subject'],
- '[XTEST] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=')
- self._mlist.subject_prefix = '[XTEST %d]'
- self._mlist.post_id = 456
- msg = Message.Message()
- msg['Subject'] = '=?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?='
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(unicode(msg['subject']),
- u'[XTEST 456] \u30e1\u30fc\u30eb\u30de\u30f3')
- self.assertEqual(msg['subject'],
- '[XTEST 456] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=')
- msg = Message.Message()
- msg['Subject'
- ] = 'Re: [XTEST 123] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?='
- CookHeaders.process(self._mlist, msg, {})
- # next code suceeds if python email patch tracker #1681333 is applied.
- #self.assertEqual(unicode(msg['subject']),
- # u'[XTEST 456] Re: \u30e1\u30fc\u30eb\u30de\u30f3')
- self.assertEqual(msg['subject'],
- '[XTEST 456] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=')
-
- def test_subject_munging_prefix_number(self):
- self._mlist.subject_prefix = '[XTEST %d]'
- self._mlist.post_id = 456
- msg = Message.Message()
- msg['Subject'] = 'About Mailman...'
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(msg['subject'], '[XTEST 456] About Mailman...')
- msg = Message.Message()
- msg['Subject'] = 'Re: [XTEST 123] About Mailman...'
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(msg['subject'], '[XTEST 456] Re: About Mailman...')
-
- def test_subject_munging_prefix_newstyle(self):
- self._mlist.subject_prefix = '[XTEST]'
- config.OLD_STYLE_PREFIXING = False
- msg = Message.Message()
- msg['Subject'] = 'Re: [XTEST] About Mailman...'
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(msg['subject'], '[XTEST] Re: About Mailman...')
-
- def test_subject_munging_prefix_crooked(self):
- # In this test case, we get an extra space between the prefix and
- # the original subject. It's because the original is crooked.
- # Note that isubject starting by '\n ' is generated by some version of
- # Eudora Japanese edition.
- self._mlist.subject_prefix = '[XTEST]'
- msg = Message.Message()
- msg['Subject'] = '\n About Mailman...'
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(str(msg['subject']), '[XTEST] About Mailman...')
- del msg['subject']
- msg['Subject'] = '\n =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?='
- CookHeaders.process(self._mlist, msg, {})
- self.assertEqual(str(msg['subject']),
- '[XTEST] =?iso-2022-jp?b?IBskQiVhITwlayVeJXMbKEI=?=')
-
- def test_reply_to_list(self):
- eq = self.assertEqual
- mlist = self._mlist
- mlist.reply_goes_to_list = 1
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- CookHeaders.process(mlist, msg, {})
- eq(msg['reply-to'], '_xtest@example.com')
- eq(msg.get_all('reply-to'), ['_xtest@example.com'])
-
- def test_reply_to_list_with_strip(self):
- eq = self.assertEqual
- mlist = self._mlist
- mlist.reply_goes_to_list = 1
- mlist.first_strip_reply_to = 1
- msg = email.message_from_string("""\
-From: aperson@example.org
-Reply-To: bperson@example.com
-
-""", Message.Message)
- CookHeaders.process(mlist, msg, {})
- eq(msg['reply-to'], '_xtest@example.com')
- eq(msg.get_all('reply-to'), ['_xtest@example.com'])
-
- def test_reply_to_explicit(self):
- eq = self.assertEqual
- mlist = self._mlist
- mlist.reply_goes_to_list = 2
- mlist.reply_to_address = 'mlist@example.com'
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- CookHeaders.process(mlist, msg, {})
- eq(msg['reply-to'], 'mlist@example.com')
- eq(msg.get_all('reply-to'), ['mlist@example.com'])
-
- def test_reply_to_explicit_with_strip(self):
- eq = self.assertEqual
- mlist = self._mlist
- mlist.reply_goes_to_list = 2
- mlist.first_strip_reply_to = 1
- mlist.reply_to_address = 'mlist@example.com'
- msg = email.message_from_string("""\
-From: aperson@example.org
-Reply-To: bperson@example.com
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- eq(msg['reply-to'], 'mlist@example.com')
- eq(msg.get_all('reply-to'), ['mlist@example.com'])
-
- def test_reply_to_extends_to_list(self):
- eq = self.assertEqual
- mlist = self._mlist
- mlist.reply_goes_to_list = 1
- mlist.first_strip_reply_to = 0
- msg = email.message_from_string("""\
-From: aperson@example.org
-Reply-To: bperson@example.com
-
-""", Message.Message)
- CookHeaders.process(mlist, msg, {})
- eq(msg['reply-to'], 'bperson@example.com, _xtest@example.com')
-
- def test_reply_to_extends_to_explicit(self):
- eq = self.assertEqual
- mlist = self._mlist
- mlist.reply_goes_to_list = 2
- mlist.first_strip_reply_to = 0
- mlist.reply_to_address = 'mlist@example.com'
- msg = email.message_from_string("""\
-From: aperson@example.org
-Reply-To: bperson@example.com
-
-""", Message.Message)
- CookHeaders.process(mlist, msg, {})
- eq(msg['reply-to'], 'mlist@example.com, bperson@example.com')
-
- def test_list_headers_nolist(self):
- eq = self.assertEqual
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {'_nolist': 1})
- eq(msg['list-id'], None)
- eq(msg['list-help'], None)
- eq(msg['list-unsubscribe'], None)
- eq(msg['list-subscribe'], None)
- eq(msg['list-post'], None)
- eq(msg['list-archive'], None)
-
- def test_list_headers(self):
- eq = self.assertEqual
- self._mlist.archive = 1
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- oldval = config.DEFAULT_URL_HOST
- config.DEFAULT_URL_HOST = 'www.example.com'
- try:
- CookHeaders.process(self._mlist, msg, {})
- finally:
- config.DEFAULT_URL_HOST = oldval
- eq(msg['list-id'], '<_xtest.example.com>')
- eq(msg['list-help'], '<mailto:_xtest-request@example.com?subject=help>')
- eq(msg['list-unsubscribe'],
- '<http://www.example.com/mailman/listinfo/_xtest@example.com>,'
- '\n\t<mailto:_xtest-request@example.com?subject=unsubscribe>')
- eq(msg['list-subscribe'],
- '<http://www.example.com/mailman/listinfo/_xtest@example.com>,'
- '\n\t<mailto:_xtest-request@example.com?subject=subscribe>')
- eq(msg['list-post'], '<mailto:_xtest@example.com>')
- eq(msg['list-archive'],
- '<http://www.example.com/pipermail/_xtest@example.com>')
-
- def test_list_headers_with_description(self):
- eq = self.assertEqual
- self._mlist.archive = 1
- self._mlist.description = 'A Test List'
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-""", Message.Message)
- CookHeaders.process(self._mlist, msg, {})
- eq(unicode(msg['list-id']), u'A Test List <_xtest.example.com>')
- eq(msg['list-help'], '<mailto:_xtest-request@example.com?subject=help>')
- eq(msg['list-unsubscribe'],
- '<http://www.example.com/mailman/listinfo/_xtest@example.com>,'
- '\n\t<mailto:_xtest-request@example.com?subject=unsubscribe>')
- eq(msg['list-subscribe'],
- '<http://www.example.com/mailman/listinfo/_xtest@example.com>,'
- '\n\t<mailto:_xtest-request@example.com?subject=subscribe>')
- eq(msg['list-post'], '<mailto:_xtest@example.com>')
-
-
-
class TestFileRecips(TestBase):
def test_short_circuit(self):
msgdata = {'recips': 1}
@@ -1399,7 +1052,6 @@ Mailman rocks!
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestApprove))
- suite.addTest(unittest.makeSuite(TestCookHeaders))
suite.addTest(unittest.makeSuite(TestFileRecips))
suite.addTest(unittest.makeSuite(TestHold))
suite.addTest(unittest.makeSuite(TestMimeDel))
diff --git a/docs/NEWS.txt b/docs/NEWS.txt
index af31a1142..10b018817 100644
--- a/docs/NEWS.txt
+++ b/docs/NEWS.txt
@@ -6,6 +6,16 @@ Here is a history of user visible changes to Mailman.
3.0 alpha 1 (XX-XXX-200X)
+ User visible changes
+
+ - So called 'new style' subject prefixing is the default now, and the only
+ option. When a list's subject prefix is added, it's always done so
+ before any Re: tag, not after. E.g. '[My List] Re: The subject'.
+
+ - RFC 2369 headers List-Subscribe and List-Unsubscribe now use the
+ preferred -join and -leave addresses instead of the -request address
+ with a subject value.
+
Configuration
- Mailman can now be configured via a 'mailman.cfg' file which lives in