diff options
| author | Barry Warsaw | 2007-06-21 10:23:40 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2007-06-21 10:23:40 -0400 |
| commit | 6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e (patch) | |
| tree | fe4787d9b79e27ea361b33a4f98a12d2e695f16b | |
| parent | 3a86b40fe4e3b28c2d9f4e3bbd2cc0eeefe31453 (diff) | |
| download | mailman-6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e.tar.gz mailman-6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e.tar.zst mailman-6c1ccc236bc24d8b3bb04807ba4b24e8a7a0d18e.zip | |
| -rw-r--r-- | Mailman/Defaults.py.in | 11 | ||||
| -rw-r--r-- | Mailman/Handlers/CookHeaders.py | 46 | ||||
| -rw-r--r-- | Mailman/constants.py | 10 | ||||
| -rw-r--r-- | Mailman/database/model/mailinglist.py | 3 | ||||
| -rw-r--r-- | Mailman/docs/ack-headers.txt | 65 | ||||
| -rw-r--r-- | Mailman/docs/cook-headers.txt | 304 | ||||
| -rw-r--r-- | Mailman/docs/reply-to.txt | 148 | ||||
| -rw-r--r-- | Mailman/docs/subject-munging.txt | 262 | ||||
| -rw-r--r-- | Mailman/testing/test_cook_headers.py | 35 | ||||
| -rw-r--r-- | Mailman/testing/test_handlers.py | 348 | ||||
| -rw-r--r-- | docs/NEWS.txt | 10 |
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 |
