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 | |
Convert the CookHeaders tests in test_handlers to using doctests, split up
into several sub-documents.
Defaults.py.in: Removed OLD_STYLE_PREFIXING. So-called 'new style' prefixing
is the default and only option now.
CookHeaders.py is updated to the new API and some (but not all) of the code
has been updated to more modern Python idioms.
reply_goes_to_list attribute has been changed from a strict integer to a
munepy enum called ReplyToMunging.
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.
| -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 |
