diff options
| author | Barry Warsaw | 2015-01-04 20:20:33 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2015-01-04 20:20:33 -0500 |
| commit | 4a612db8e89afed74173b93f3b64fa567b8417a3 (patch) | |
| tree | 81a687d113079a25f93279f35c7eee2aa2572510 /src/mailman/handlers | |
| parent | 84af79988a4e916604cba31843778206efb7d1b8 (diff) | |
| parent | de181c1a40965a3a7deedd56a034a946f45b6984 (diff) | |
| download | mailman-4a612db8e89afed74173b93f3b64fa567b8417a3.tar.gz mailman-4a612db8e89afed74173b93f3b64fa567b8417a3.tar.zst mailman-4a612db8e89afed74173b93f3b64fa567b8417a3.zip | |
Merge the Python 3 branch.
Diffstat (limited to 'src/mailman/handlers')
37 files changed, 575 insertions, 401 deletions
diff --git a/src/mailman/handlers/acknowledge.py b/src/mailman/handlers/acknowledge.py index c3af9ab27..c10043981 100644 --- a/src/mailman/handlers/acknowledge.py +++ b/src/mailman/handlers/acknowledge.py @@ -20,23 +20,19 @@ This only happens if the sender has set their AcknowledgePosts attribute. """ -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'Acknowledge', ] -from zope.component import getUtility -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.email.message import UserNotification from mailman.interfaces.handler import IHandler from mailman.interfaces.languages import ILanguageManager from mailman.utilities.i18n import make from mailman.utilities.string import oneline +from zope.component import getUtility +from zope.interface import implementer @@ -67,14 +63,13 @@ class Acknowledge: language = (language_manager[msgdata['lang']] if 'lang' in msgdata else member.preferred_language) - charset = language_manager[language.code].charset # Now get the acknowledgement template. display_name = mlist.display_name text = make('postack.txt', mailing_list=mlist, language=language.code, wrap=False, - subject=oneline(original_subject, charset), + subject=oneline(original_subject, in_unicode=True), list_name=mlist.list_name, display_name=display_name, listinfo_url=mlist.script_url('listinfo'), diff --git a/src/mailman/handlers/after_delivery.py b/src/mailman/handlers/after_delivery.py index 7fa7a4554..464fafd8c 100644 --- a/src/mailman/handlers/after_delivery.py +++ b/src/mailman/handlers/after_delivery.py @@ -17,19 +17,15 @@ """Perform some bookkeeping after a successful post.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'AfterDelivery', ] -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler from mailman.utilities.datetime import now +from zope.interface import implementer diff --git a/src/mailman/handlers/avoid_duplicates.py b/src/mailman/handlers/avoid_duplicates.py index 529a99f68..636a9f24d 100644 --- a/src/mailman/handlers/avoid_duplicates.py +++ b/src/mailman/handlers/avoid_duplicates.py @@ -23,19 +23,15 @@ has already received a copy, we either drop the message, add a duplicate warning header, or pass it through, depending on the user's preferences. """ -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'AvoidDuplicates', ] from email.utils import getaddresses, formataddr -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler +from zope.interface import implementer COMMASPACE = ', ' diff --git a/src/mailman/handlers/cleanse.py b/src/mailman/handlers/cleanse.py index 6b653bb34..0dad3077e 100644 --- a/src/mailman/handlers/cleanse.py +++ b/src/mailman/handlers/cleanse.py @@ -17,9 +17,6 @@ """Cleanse certain headers from all messages.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'Cleanse', ] @@ -28,11 +25,10 @@ __all__ = [ import logging from email.utils import formataddr -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.handlers.cook_headers import uheader from mailman.interfaces.handler import IHandler +from zope.interface import implementer log = logging.getLogger('mailman.post') diff --git a/src/mailman/handlers/cleanse_dkim.py b/src/mailman/handlers/cleanse_dkim.py index 225666bf1..a4c16d31e 100644 --- a/src/mailman/handlers/cleanse_dkim.py +++ b/src/mailman/handlers/cleanse_dkim.py @@ -25,20 +25,16 @@ and it will also give the MTA the opportunity to regenerate valid keys originating at the Mailman server for the outgoing message. """ -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'CleanseDKIM', ] from lazr.config import as_boolean -from zope.interface import implementer - from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler +from zope.interface import implementer diff --git a/src/mailman/handlers/cook_headers.py b/src/mailman/handlers/cook_headers.py index d5d096448..44ef02e36 100644 --- a/src/mailman/handlers/cook_headers.py +++ b/src/mailman/handlers/cook_headers.py @@ -17,9 +17,6 @@ """Cook a message's headers.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'CookHeaders', ] @@ -27,21 +24,18 @@ __all__ = [ import re -from email.errors import HeaderParseError -from email.header import Header, decode_header, make_header +from email.header import Header from email.utils import parseaddr, formataddr, getaddresses -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler from mailman.interfaces.mailinglist import Personalization, ReplyToMunging from mailman.version import VERSION +from zope.interface import implementer COMMASPACE = ', ' MAXLINELEN = 78 - -nonascii = re.compile('[^\s!-~]') +NONASCII = re.compile('[^\s!-~]') @@ -54,12 +48,12 @@ def uheader(mlist, s, header_name=None, continuation_ws='\t', maxlinelen=None): specified. """ charset = mlist.preferred_language.charset - if nonascii.search(s): + if NONASCII.search(s): # use list charset but ... if charset == 'us-ascii': charset = 'iso-8859-1' else: - # there is no nonascii so ... + # there is no non-ascii so ... charset = 'us-ascii' return Header(s, charset, maxlinelen, header_name, continuation_ws) @@ -78,13 +72,6 @@ def process(mlist, msg, msgdata): msgdata['original_sender'] = msg.sender # VirginRunner sets _fasttrack for internally crafted messages. fasttrack = msgdata.get('_fasttrack') - if not msgdata.get('isdigest') and not fasttrack: - try: - prefix_subject(mlist, msg, msgdata) - except (UnicodeError, ValueError): - # TK: Sometimes subject header is not MIME encoded for 8bit - # simply abort prefixing. - pass # 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. @@ -171,114 +158,6 @@ def process(mlist, msg, msgdata): -def prefix_subject(mlist, msg, msgdata): - """Maybe add a subject prefix. - - Add the subject prefix unless the message is a digest or is being fast - tracked (e.g. internally crafted, delivered to a single user such as the - list admin). - """ - 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): - lines = str(subject).splitlines() - else: - lines = subject.splitlines() - ws = '\t' - if len(lines) > 1 and lines[1] and lines[1][0] in ' \t': - ws = lines[1][0] - msgdata['original_subject'] = subject - # The subject may be multilingual but we take the first charset as major - # one and try to decode. If it is decodable, returned subject is in one - # line and cset is properly set. If fail, subject is mime-encoded and - # cset is set as us-ascii. See detail for ch_oneline() (CookHeaders one - # line function). - subject, cset = ch_oneline(subject) - # TK: Python interpreter has evolved to be strict on ascii charset code - # range. It is safe to use unicode string when manupilating header - # contents with re module. It would be best to return unicode in - # ch_oneline() but here is temporary solution. - subject = unicode(subject, cset) - # If the subject_prefix contains '%d', it is replaced with the - # mailing list sequential number. Sequential number format allows - # '%d' or '%05d' like pattern. - prefix_pattern = re.escape(prefix) - # unescape '%' :-< - prefix_pattern = '%'.join(prefix_pattern.split(r'\%')) - p = re.compile('%\d*d') - if p.search(prefix, 1): - # 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) - subject = re.sub(prefix_pattern, '', subject) - rematch = re.match('((RE|AW|SV|VS)(\[\d+\])?:\s*)+', subject, re.I) - if rematch: - subject = subject[rematch.end():] - recolon = 'Re:' - else: - recolon = '' - # At this point, subject may become null if someone post mail with - # subject: [subject prefix] - if subject.strip() == '': - subject = _('(no subject)') - cset = mlist.preferred_language.charset - # and substitute %d in prefix with post_id - try: - prefix = prefix % mlist.post_id - except TypeError: - pass - # Get the header as a Header instance, with proper unicode conversion - if not recolon: - h = uheader(mlist, prefix, 'Subject', continuation_ws=ws) - else: - h = uheader(mlist, prefix, 'Subject', continuation_ws=ws) - h.append(recolon) - # TK: Subject is concatenated and unicode string. - subject = subject.encode(cset, 'replace') - h.append(subject, cset) - del msg['subject'] - msg['Subject'] = h - ss = uheader(mlist, recolon, 'Subject', continuation_ws=ws) - ss.append(subject, cset) - msgdata['stripped_subject'] = ss - - - -def ch_oneline(headerstr): - # Decode header string in one line and convert into single charset. - # Return (string, cset) tuple as check for failure. - try: - d = decode_header(headerstr) - # At this point, we should rstrip() every string because some - # MUA deliberately add trailing spaces when composing return - # message. - d = [(s.rstrip(), c) for (s, c) in d] - # Find all charsets in the original header. We use 'utf-8' rather - # than using the first charset (in mailman 2.1.x) if multiple - # charsets are used. - csets = [] - for (s, c) in d: - if c and c not in csets: - csets.append(c) - if len(csets) == 0: - cset = 'us-ascii' - elif len(csets) == 1: - cset = csets[0] - else: - cset = 'utf-8' - h = make_header(d) - ustr = unicode(h) - oneline = ''.join(ustr.splitlines()) - return oneline.encode(cset, 'replace'), cset - except (LookupError, UnicodeError, ValueError, HeaderParseError): - # possibly charset problem. return with undecoded string in one line. - return ''.join(headerstr.splitlines()), 'us-ascii' - - - @implementer(IHandler) class CookHeaders: """Modify message headers.""" diff --git a/src/mailman/handlers/decorate.py b/src/mailman/handlers/decorate.py index bf8454232..78fafb3ca 100644 --- a/src/mailman/handlers/decorate.py +++ b/src/mailman/handlers/decorate.py @@ -17,9 +17,6 @@ """Decorate a message by sticking the header and footer around it.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'Decorate', 'decorate', @@ -31,15 +28,14 @@ import re import logging from email.mime.text import MIMEText -from urllib2 import URLError -from zope.component import getUtility -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.email.message import Message from mailman.interfaces.handler import IHandler from mailman.interfaces.templates import ITemplateLoader from mailman.utilities.string import expand +from six.moves.urllib_error import URLError +from zope.component import getUtility +from zope.interface import implementer log = logging.getLogger('mailman.error') diff --git a/src/mailman/handlers/docs/acknowledge.rst b/src/mailman/handlers/docs/acknowledge.rst index e91f94f62..42cab04a0 100644 --- a/src/mailman/handlers/docs/acknowledge.rst +++ b/src/mailman/handlers/docs/acknowledge.rst @@ -113,9 +113,9 @@ The receipt will include the original message's subject in the response body, 1 >>> dump_msgdata(messages[0].msgdata) _parsemsg : False - listname : test@example.com + listid : test.example.com nodecorate : True - recipients : set([u'aperson@example.com']) + recipients : {'aperson@example.com'} reduced_list_headers: True ... >>> print(messages[0].msg.as_string()) @@ -150,9 +150,9 @@ If there is no subject, then the receipt will use a generic message. 1 >>> dump_msgdata(messages[0].msgdata) _parsemsg : False - listname : test@example.com + listid : test.example.com nodecorate : True - recipients : set([u'aperson@example.com']) + recipients : {'aperson@example.com'} reduced_list_headers: True ... >>> print(messages[0].msg.as_string()) diff --git a/src/mailman/handlers/docs/avoid-duplicates.rst b/src/mailman/handlers/docs/avoid-duplicates.rst index 612634941..19a41bf85 100644 --- a/src/mailman/handlers/docs/avoid-duplicates.rst +++ b/src/mailman/handlers/docs/avoid-duplicates.rst @@ -71,7 +71,7 @@ or ``Resent-CC``), then they will get a list copy. >>> msgdata = recips.copy() >>> handler.process(mlist, msg, msgdata) >>> sorted(msgdata['recipients']) - [u'aperson@example.com', u'bperson@example.com'] + ['aperson@example.com', 'bperson@example.com'] >>> print(msg.as_string()) From: Claire Person <cperson@example.com> <BLANKLINE> @@ -89,7 +89,7 @@ If they're mentioned on the ``CC`` line, they won't get a list copy. >>> msgdata = recips.copy() >>> handler.process(mlist, msg, msgdata) >>> sorted(msgdata['recipients']) - [u'bperson@example.com'] + ['bperson@example.com'] >>> print(msg.as_string()) From: Claire Person <cperson@example.com> CC: aperson@example.com @@ -109,7 +109,7 @@ to ``True`` (the default), then they still get a list copy. >>> msgdata = recips.copy() >>> handler.process(mlist, msg, msgdata) >>> sorted(msgdata['recipients']) - [u'aperson@example.com', u'bperson@example.com'] + ['aperson@example.com', 'bperson@example.com'] >>> print(msg.as_string()) From: Claire Person <cperson@example.com> CC: bperson@example.com @@ -128,7 +128,7 @@ Other headers checked for recipients include the ``To``... >>> msgdata = recips.copy() >>> handler.process(mlist, msg, msgdata) >>> sorted(msgdata['recipients']) - [u'bperson@example.com'] + ['bperson@example.com'] >>> print(msg.as_string()) From: Claire Person <cperson@example.com> To: aperson@example.com @@ -147,7 +147,7 @@ Other headers checked for recipients include the ``To``... >>> msgdata = recips.copy() >>> handler.process(mlist, msg, msgdata) >>> sorted(msgdata['recipients']) - [u'bperson@example.com'] + ['bperson@example.com'] >>> print(msg.as_string()) From: Claire Person <cperson@example.com> Resent-To: aperson@example.com @@ -166,7 +166,7 @@ Other headers checked for recipients include the ``To``... >>> msgdata = recips.copy() >>> handler.process(mlist, msg, msgdata) >>> sorted(msgdata['recipients']) - [u'bperson@example.com'] + ['bperson@example.com'] >>> print(msg.as_string()) From: Claire Person <cperson@example.com> Resent-Cc: aperson@example.com diff --git a/src/mailman/handlers/docs/digests.rst b/src/mailman/handlers/docs/digests.rst index ac6ea33d6..c3fc62ebf 100644 --- a/src/mailman/handlers/docs/digests.rst +++ b/src/mailman/handlers/docs/digests.rst @@ -82,11 +82,13 @@ actually crafted by the handler. >>> mlist.digest_size_threshold = 1 >>> mlist.volume = 2 >>> mlist.next_digest_number = 10 + >>> digest_path = os.path.join(mlist.data_path, 'digest.mmdf') >>> size = 0 >>> for msg in message_factory: ... process(mlist, msg, {}) - ... size += len(str(msg)) - ... if size >= mlist.digest_size_threshold * 1024: + ... # When the digest reaches the proper size, it is renamed. So we + ... # can break out of this list when the file disappears. + ... if not os.path.exists(digest_path): ... break >>> sum(1 for msg in digest_mbox(mlist)) diff --git a/src/mailman/handlers/docs/file-recips.rst b/src/mailman/handlers/docs/file-recips.rst index 58af6f480..73b47adb1 100644 --- a/src/mailman/handlers/docs/file-recips.rst +++ b/src/mailman/handlers/docs/file-recips.rst @@ -34,26 +34,6 @@ returns. recipients: 7 -Missing file -============ - -The include file must live inside the list's data directory, under the name -``members.txt``. If the file doesn't exist, the list of recipients will be -empty. - - >>> import os - >>> file_path = os.path.join(mlist.data_path, 'members.txt') - >>> open(file_path) - Traceback (most recent call last): - ... - IOError: [Errno ...] - No such file or directory: u'.../_xtest@example.com/members.txt' - >>> msgdata = {} - >>> handler.process(mlist, msg, msgdata) - >>> dump_list(msgdata['recipients']) - *Empty* - - Existing file ============= @@ -61,16 +41,15 @@ If the file exists, it contains a list of addresses, one per line. These addresses are returned as the set of recipients. :: - >>> fp = open(file_path, 'w') - >>> try: + >>> import os + >>> file_path = os.path.join(mlist.data_path, 'members.txt') + >>> with open(file_path, 'w', encoding='utf-8') as fp: ... print('bperson@example.com', file=fp) ... print('cperson@example.com', file=fp) ... print('dperson@example.com', file=fp) ... print('eperson@example.com', file=fp) ... print('fperson@example.com', file=fp) ... print('gperson@example.com', file=fp) - ... finally: - ... fp.close() >>> msgdata = {} >>> handler.process(mlist, msg, msgdata) diff --git a/src/mailman/handlers/docs/filtering.rst b/src/mailman/handlers/docs/filtering.rst index 6c3735f1b..582211d54 100644 --- a/src/mailman/handlers/docs/filtering.rst +++ b/src/mailman/handlers/docs/filtering.rst @@ -26,6 +26,8 @@ Filtering the outer content type A simple filtering setting will just search the content types of the messages parts, discarding all parts with a matching MIME type. If the message's outer content type matches the filter, the entire message will be discarded. +However, if we turn off content filtering altogether, then the handler +short-circuits. :: >>> from mailman.interfaces.mime import FilterAction @@ -42,14 +44,6 @@ content type matches the filter, the entire message will be discarded. ... """) >>> process = config.handlers['mime-delete'].process - >>> process(mlist, msg, {}) - Traceback (most recent call last): - ... - DiscardMessage: The message's content type was explicitly disallowed - -However, if we turn off content filtering altogether, then the handler -short-circuits. - >>> mlist.filter_content = False >>> msgdata = {} >>> process(mlist, msg, msgdata) @@ -74,15 +68,15 @@ crafted internally by Mailman. MIME-Version: 1.0 <BLANKLINE> xxxxx - >>> msgdata - {u'isdigest': True} + >>> dump_msgdata(msgdata) + isdigest: True Simple multipart filtering ========================== -If one of the subparts in a multipart message matches the filter type, then -just that subpart will be stripped. +If one of the subparts in a ``multipart`` message matches the filter type, +then just that subpart will be stripped. :: >>> msg = message_from_string("""\ @@ -241,8 +235,8 @@ name of the file containing the message payload to filter. >>> try: ... print("""\ ... import sys - ... print 'Converted text/html to text/plain' - ... print 'Filename:', sys.argv[1] + ... print('Converted text/html to text/plain') + ... print('Filename:', sys.argv[1]) ... """, file=fp) ... finally: ... fp.close() diff --git a/src/mailman/handlers/docs/nntp.rst b/src/mailman/handlers/docs/nntp.rst index 2dfc95ce1..72bcb35f0 100644 --- a/src/mailman/handlers/docs/nntp.rst +++ b/src/mailman/handlers/docs/nntp.rst @@ -63,5 +63,5 @@ messages are gated to. >>> dump_msgdata(messages[0].msgdata) _parsemsg: False - listname : test@example.com + listid : test.example.com version : 3 diff --git a/src/mailman/handlers/docs/replybot.rst b/src/mailman/handlers/docs/replybot.rst index 638c2fdc8..9e18ce911 100644 --- a/src/mailman/handlers/docs/replybot.rst +++ b/src/mailman/handlers/docs/replybot.rst @@ -49,9 +49,9 @@ response. >>> dump_msgdata(messages[0].msgdata) _parsemsg : False - listname : _xtest@example.com + listid : _xtest.example.com nodecorate : True - recipients : set([u'aperson@example.com']) + recipients : {'aperson@example.com'} reduced_list_headers: True version : 3 @@ -141,9 +141,9 @@ Unless the ``X-Ack:`` header has a value of ``yes``, in which case, the >>> dump_msgdata(messages[0].msgdata) _parsemsg : False - listname : _xtest@example.com + listid : _xtest.example.com nodecorate : True - recipients : set([u'asystem@example.com']) + recipients : {'asystem@example.com'} reduced_list_headers: True version : 3 diff --git a/src/mailman/handlers/docs/rfc-2369.rst b/src/mailman/handlers/docs/rfc-2369.rst index 8180b0635..b5a783edc 100644 --- a/src/mailman/handlers/docs/rfc-2369.rst +++ b/src/mailman/handlers/docs/rfc-2369.rst @@ -13,7 +13,7 @@ headers generally start with the `List-` prefix. .. This is a helper function for the following section. >>> def list_headers(msg, only=None): - ... if isinstance(only, basestring): + ... if isinstance(only, str): ... only = (only.lower(),) ... elif only is None: ... only = set(header.lower() for header in msg.keys() diff --git a/src/mailman/handlers/docs/subject-munging.rst b/src/mailman/handlers/docs/subject-munging.rst index 538ad99c7..de22a928c 100644 --- a/src/mailman/handlers/docs/subject-munging.rst +++ b/src/mailman/handlers/docs/subject-munging.rst @@ -1,44 +1,42 @@ -=============== -Subject munging -=============== +================ +Subject prefixes +================ -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. +Mailing lists can define a *subject prefix* which gets added to the front of +any ``Subject`` text. This can be used to quickly identify which mailing list +the message was posted to. >>> mlist = create_list('test@example.com') +The default list style gives the mailing list a default prefix. -Inserting a prefix -================== + >>> print(mlist.subject_prefix) + [Test] -Another thing header cooking 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. -:: +This can be changed to anything, but typically ends with a trailing space. >>> mlist.subject_prefix = '[XTest] ' - >>> mlist.preferred_language = 'en' + >>> process = config.handlers['subject-prefix'].process + + +No Subject +========== + +If the original message has no ``Subject``, then a canned one is used. + >>> msg = message_from_string("""\ ... From: aperson@example.com ... ... A message of great import. ... """) - >>> msgdata = {} - - >>> from mailman.handlers.cook_headers import process - >>> process(mlist, msg, msgdata) - -The original subject header is stored in the message metadata. - - >>> msgdata['original_subject'] - u'' + >>> process(mlist, msg, {}) >>> print(msg['subject']) [XTest] (no subject) + +Inserting a prefix +================== + If the original message had a ``Subject`` header, then the prefix is inserted at the beginning of the header's value. @@ -50,34 +48,12 @@ at the beginning of the header's value. ... """) >>> msgdata = {} >>> process(mlist, msg, msgdata) - >>> print(msgdata['original_subject']) - 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. - ... """) - >>> process(mlist, msg, dict(isdigest=True)) - >>> print(msg['subject']) - Something important - -Nor are they munged for *fast tracked* messages, which are generally defined -as messages that Mailman crafts internally. +The original ``Subject`` is available in the metadata. - >>> msg = message_from_string("""\ - ... From: aperson@example.com - ... Subject: Something important - ... - ... A message of great import. - ... """) - >>> process(mlist, msg, dict(_fasttrack=True)) - >>> print(msg['subject']) + >>> print(msgdata['original_subject']) Something important If a ``Subject`` header already has a prefix, usually following a ``Re:`` @@ -95,8 +71,7 @@ front of the header text. [XTest] Re: Something important If the ``Subject`` 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. +where it will stay. >>> msg = message_from_string("""\ ... From: aperson@example.com @@ -122,10 +97,10 @@ set than the encoded header. ... ... """) >>> process(mlist, msg, {}) - >>> print(msg['subject']) + >>> print(msg['subject'].encode()) [XTest] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?= - >>> unicode(msg['subject']) - u'[XTest] \u30e1\u30fc\u30eb\u30de\u30f3' + >>> print(str(msg['subject'])) + [XTest] メールマン Prefix numbers @@ -178,10 +153,10 @@ in the subject prefix, and the subject is encoded non-ASCII. ... ... """) >>> process(mlist, msg, {}) - >>> print(msg['subject']) + >>> print(msg['subject'].encode()) [XTest 456] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?= - >>> unicode(msg['subject']) - u'[XTest 456] \u30e1\u30fc\u30eb\u30de\u30f3' + >>> print(msg['subject']) + [XTest 456] メールマン Even more fun is when the internationalized ``Subject`` header already has a prefix, possibly with a different posting number. @@ -191,13 +166,10 @@ prefix, possibly with a different posting number. ... ... """) >>> process(mlist, msg, {}) - >>> print(msg['subject']) + >>> print(msg['subject'].encode()) [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' + >>> print(msg['subject']) + [XTest 456] Re: メールマン As before, old style subject prefixes are re-ordered. @@ -206,14 +178,11 @@ As before, old style subject prefixes are re-ordered. ... ... """) >>> process(mlist, msg, {}) - >>> print(msg['subject']) + >>> print(msg['subject'].encode()) [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' + >>> print(msg['subject']) + [XTest 456] Re: メールマン In this test case, we get an extra space between the prefix and the original diff --git a/src/mailman/handlers/docs/tagger.rst b/src/mailman/handlers/docs/tagger.rst index f3303b7ef..fcefdb01c 100644 --- a/src/mailman/handlers/docs/tagger.rst +++ b/src/mailman/handlers/docs/tagger.rst @@ -55,7 +55,7 @@ and the message metadata gets a key with a list of matching topic names. <BLANKLINE> <BLANKLINE> >>> msgdata['topichits'] - [u'bar fight'] + ['bar fight'] Scanning body lines @@ -114,7 +114,7 @@ found. Keywords: barbaz <BLANKLINE> >>> msgdata['topichits'] - [u'bar fight'] + ['bar fight'] However, scanning stops at the first body line that doesn't look like a header. @@ -161,7 +161,7 @@ When set to a negative number, all body lines will be scanned. >>> print(msg['x-topics']) bar fight >>> msgdata['topichits'] - [u'bar fight'] + ['bar fight'] Scanning sub-parts @@ -175,14 +175,14 @@ text payload. ... Subject: Was ... Keywords: Raw ... Content-Type: multipart/alternative; boundary="BOUNDARY" - ... + ... ... --BOUNDARY ... From: sabo ... To: obas - ... + ... ... Subject: farbaw ... Keywords: barbaz - ... + ... ... --BOUNDARY-- ... """) >>> msgdata = {} @@ -203,7 +203,7 @@ text payload. --BOUNDARY-- <BLANKLINE> >>> msgdata['topichits'] - [u'bar fight'] + ['bar fight'] But the tagger will not descend into non-text parts. @@ -211,23 +211,23 @@ But the tagger will not descend into non-text parts. ... Subject: Was ... Keywords: Raw ... Content-Type: multipart/alternative; boundary=BOUNDARY - ... + ... ... --BOUNDARY ... From: sabo ... To: obas ... Content-Type: message/rfc822 - ... + ... ... Subject: farbaw ... Keywords: barbaz - ... + ... ... --BOUNDARY ... From: sabo ... To: obas ... Content-Type: message/rfc822 - ... + ... ... Subject: farbaw ... Keywords: barbaz - ... + ... ... --BOUNDARY-- ... """) >>> msgdata = {} diff --git a/src/mailman/handlers/docs/to-outgoing.rst b/src/mailman/handlers/docs/to-outgoing.rst index e87fd4f26..90ea137a5 100644 --- a/src/mailman/handlers/docs/to-outgoing.rst +++ b/src/mailman/handlers/docs/to-outgoing.rst @@ -37,6 +37,6 @@ additional key set: the mailing list name. _parsemsg: False bar : 2 foo : 1 - listname : test@example.com + listid : test.example.com verp : True version : 3 diff --git a/src/mailman/handlers/file_recipients.py b/src/mailman/handlers/file_recipients.py index ec8868649..4b115bb53 100644 --- a/src/mailman/handlers/file_recipients.py +++ b/src/mailman/handlers/file_recipients.py @@ -17,9 +17,6 @@ """Get the normal delivery recipients from a Sendmail style :include: file.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'FileRecipients', ] @@ -28,10 +25,9 @@ __all__ = [ import os import errno -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler +from zope.interface import implementer diff --git a/src/mailman/handlers/member_recipients.py b/src/mailman/handlers/member_recipients.py index 0f99bf709..7497746eb 100644 --- a/src/mailman/handlers/member_recipients.py +++ b/src/mailman/handlers/member_recipients.py @@ -23,22 +23,18 @@ on the `recipients' attribute of the message. This attribute is used by the SendmailDeliver and BulkDeliver modules. """ -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'MemberRecipients', ] -from zope.interface import implementer - from mailman.config import config from mailman.core import errors from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler from mailman.interfaces.member import DeliveryStatus from mailman.utilities.string import wrap +from zope.interface import implementer diff --git a/src/mailman/handlers/mime_delete.py b/src/mailman/handlers/mime_delete.py index 98c1de3f9..1d107522d 100644 --- a/src/mailman/handlers/mime_delete.py +++ b/src/mailman/handlers/mime_delete.py @@ -24,9 +24,6 @@ wrapping only single sections after other processing are replaced by their contents. """ -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'MIMEDelete', ] @@ -41,9 +38,6 @@ from email.iterators import typed_subpart_iterator from email.mime.message import MIMEMessage from email.mime.text import MIMEText from lazr.config import as_boolean -from os.path import splitext -from zope.interface import implementer - from mailman.config import config from mailman.core import errors from mailman.core.i18n import _ @@ -52,6 +46,8 @@ from mailman.interfaces.action import FilterAction from mailman.interfaces.handler import IHandler from mailman.utilities.string import oneline from mailman.version import VERSION +from os.path import splitext +from zope.interface import implementer log = logging.getLogger('mailman.error') @@ -245,7 +241,7 @@ def to_plaintext(msg): filename = tempfile.mktemp('.html') fp = open(filename, 'w') try: - fp.write(subpart.get_payload(decode=True)) + fp.write(subpart.get_payload()) fp.close() cmd = os.popen(config.HTML_TO_PLAIN_TEXT_COMMAND % {'filename': filename}) diff --git a/src/mailman/handlers/owner_recipients.py b/src/mailman/handlers/owner_recipients.py index 5a1d0bd2e..dbb203728 100644 --- a/src/mailman/handlers/owner_recipients.py +++ b/src/mailman/handlers/owner_recipients.py @@ -17,20 +17,16 @@ """Calculate the list owner recipients (includes moderators).""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'OwnerRecipients', ] -from zope.interface import implementer - from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler from mailman.interfaces.member import DeliveryStatus +from zope.interface import implementer diff --git a/src/mailman/handlers/replybot.py b/src/mailman/handlers/replybot.py index 63f3ca4cf..44df2344e 100644 --- a/src/mailman/handlers/replybot.py +++ b/src/mailman/handlers/replybot.py @@ -17,9 +17,6 @@ """Handler for automatic responses.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'Replybot', ] @@ -27,9 +24,6 @@ __all__ = [ import logging -from zope.component import getUtility -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.email.message import UserNotification from mailman.interfaces.autorespond import ( @@ -38,6 +32,8 @@ from mailman.interfaces.handler import IHandler from mailman.interfaces.usermanager import IUserManager from mailman.utilities.datetime import today from mailman.utilities.string import expand, wrap +from zope.component import getUtility +from zope.interface import implementer log = logging.getLogger('mailman.error') diff --git a/src/mailman/handlers/rfc_2369.py b/src/mailman/handlers/rfc_2369.py index ea909f41b..c835f2a67 100644 --- a/src/mailman/handlers/rfc_2369.py +++ b/src/mailman/handlers/rfc_2369.py @@ -17,22 +17,18 @@ """RFC 2369 List-* and related headers.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'RFC2369', ] from email.utils import formataddr -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.handlers.cook_headers import uheader from mailman.interfaces.archiver import ArchivePolicy from mailman.interfaces.mailinglist import IListArchiverSet from mailman.interfaces.handler import IHandler +from zope.interface import implementer CONTINUATION = ',\n\t' diff --git a/src/mailman/handlers/subject_prefix.py b/src/mailman/handlers/subject_prefix.py new file mode 100644 index 000000000..20abd1036 --- /dev/null +++ b/src/mailman/handlers/subject_prefix.py @@ -0,0 +1,184 @@ +# Copyright (C) 2014 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Subject header prefix munging.""" + +__all__ = [ + 'SubjectPrefix', + ] + + +import re + +from email.header import Header, make_header, decode_header +from mailman.core.i18n import _ +from mailman.interfaces.handler import IHandler +from zope.interface import implementer + + +RE_PATTERN = '((RE|AW|SV|VS)(\[\d+\])?:\s*)+' +ASCII_CHARSETS = (None, 'ascii', 'us-ascii') +EMPTYSTRING = '' + + + +def ascii_header(mlist, msgdata, subject, prefix, prefix_pattern, ws): + if mlist.preferred_language.charset not in ASCII_CHARSETS: + return None + for chunk, charset in decode_header(subject.encode()): + if charset not in ASCII_CHARSETS: + return None + subject_text = EMPTYSTRING.join(str(subject).splitlines()) + rematch = re.match(RE_PATTERN, subject_text, re.I) + if rematch: + subject_text = subject_text[rematch.end():] + recolon = 'Re: ' + else: + recolon = '' + # At this point, the subject may become null if someone posted mail + # with "Subject: [subject prefix]". + if subject_text.strip() == '': + with _.using(mlist.preferred_language.code): + subject_text = _('(no subject)') + else: + subject_text = re.sub(prefix_pattern, '', subject_text) + msgdata['stripped_subject'] = subject_text + lines = subject_text.splitlines() + first_line = [lines[0]] + if recolon: + first_line.insert(0, recolon) + if prefix: + first_line.insert(0, prefix) + subject_text = EMPTYSTRING.join(first_line) + return Header(subject_text, continuation_ws=ws) + + +def all_same_charset(mlist, msgdata, subject, prefix, prefix_pattern, ws): + list_charset = mlist.preferred_language.charset + chunks = [] + for chunk, charset in decode_header(subject.encode()): + if charset is None: + charset = 'us-ascii' + chunks.append(chunk.decode(charset)) + if charset != list_charset: + return None + subject_text = EMPTYSTRING.join(chunks) + rematch = re.match(RE_PATTERN, subject_text, re.I) + if rematch: + subject_text = subject_text[rematch.end():] + recolon = 'Re: ' + else: + recolon = '' + # At this point, the subject may become null if someone posted mail + # with "Subject: [subject prefix]". + if subject_text.strip() == '': + with _.push(mlist.preferred_language.code): + subject_text = _('(no subject)') + else: + subject_text = re.sub(prefix_pattern, '', subject_text) + msgdata['stripped_subject'] = subject_text + lines = subject_text.splitlines() + first_line = [lines[0]] + if recolon: + first_line.insert(0, recolon) + if prefix: + first_line.insert(0, prefix) + subject_text = EMPTYSTRING.join(first_line) + return Header(subject_text, charset=list_charset, continuation_ws=ws) + + +def mixed_charsets(mlist, msgdata, subject, prefix, prefix_pattern, ws): + list_charset = mlist.preferred_language.charset + chunks = decode_header(subject.encode()) + if len(chunks) == 0: + with _.push(mlist.preferred_language.code): + subject_text = _('(no subject)') + chunks = [(prefix, list_charset), + (subject_text, list_charset), + ] + return make_header(chunks, continuation_ws=ws) + # Only search the first chunk for Re and existing prefix. + chunk_text, chunk_charset = chunks[0] + if chunk_charset is None: + chunk_charset = 'us-ascii' + first_text = chunk_text.decode(chunk_charset) + first_text = re.sub(prefix_pattern, '', first_text).lstrip() + rematch = re.match(RE_PATTERN, first_text, re.I) + if rematch: + first_text = 'Re: ' + first_text[rematch.end():] + chunks[0] = (first_text, chunk_charset) + # The subject text stripped of the prefix, for use in the NNTP gateway. + msgdata['stripped_subject'] = str(make_header(chunks, continuation_ws=ws)) + chunks.insert(0, (prefix, list_charset)) + return make_header(chunks, continuation_ws=ws) + + + +@implementer(IHandler) +class SubjectPrefix: + """Add a list-specific prefix to the Subject header value.""" + + name = 'subject-prefix' + description = _('Add a list-specific prefix to the Subject header value.') + + def process(self, mlist, msg, msgdata): + """See `IHandler`.""" + if msgdata.get('isdigest') or msgdata.get('_fasttrack'): + return + prefix = mlist.subject_prefix + if not prefix.strip(): + return + subject = msg.get('subject', '') + # Turn the value into a Header instance and try to figure out what + # continuation whitespace is being used. + # Save the original Subject. + msgdata['original_subject'] = subject + if isinstance(subject, Header): + subject_text = str(subject) + else: + subject = make_header(decode_header(subject)) + subject_text = str(subject) + lines = subject_text.splitlines() + ws = '\t' + if len(lines) > 1 and lines[1] and lines[1][0] in ' \t': + ws = lines[1][0] + # If the subject_prefix contains '%d', it is replaced with the mailing + # list's sequence number. The sequential number format allows '%d' or + # '%05d' like pattern. + prefix_pattern = re.escape(prefix) + # Unescape '%'. + prefix_pattern = '%'.join(prefix_pattern.split(r'\%')) + p = re.compile('%\d*d') + if p.search(prefix, 1): + # The prefix has number, so we should search prefix w/number in + # subject. Also, force new style. + prefix_pattern = p.sub(r'\s*\d+\s*', prefix_pattern) + # Substitute %d in prefix with post_id + try: + prefix = prefix % mlist.post_id + except TypeError: + pass + for handler in (ascii_header, + all_same_charset, + mixed_charsets, + ): + new_subject = handler( + mlist, msgdata, subject, prefix, prefix_pattern, ws) + if new_subject is not None: + del msg['subject'] + msg['Subject'] = new_subject + return diff --git a/src/mailman/handlers/tagger.py b/src/mailman/handlers/tagger.py index 803cc6d11..199c5907f 100644 --- a/src/mailman/handlers/tagger.py +++ b/src/mailman/handlers/tagger.py @@ -17,9 +17,6 @@ """Extract topics from the original mail message.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'Tagger', ] @@ -29,15 +26,14 @@ import re import email.iterators import email.parser -from zope.interface import implementer - from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler +from zope.interface import implementer OR = '|' CRNL = '\r\n' -EMPTYBYTES = b'' +EMPTYSTRING = '' NLTAB = '\n\t' @@ -104,7 +100,7 @@ def scanbody(msg, numlines=None): reader = list(email.iterators.body_line_iterator(msg)) while numlines is None or lineno < numlines: try: - line = bytes(reader.pop(0)) + line = reader.pop(0) except IndexError: break # Blank lines don't count @@ -115,7 +111,7 @@ def scanbody(msg, numlines=None): # Concatenate those body text lines with newlines, and then create a new # message object from those lines. p = _ForgivingParser() - msg = p.parsestr(EMPTYBYTES.join(lines)) + msg = p.parsestr(EMPTYSTRING.join(lines)) return msg.get_all('subject', []) + msg.get_all('keywords', []) diff --git a/src/mailman/handlers/tests/test_cook_headers.py b/src/mailman/handlers/tests/test_cook_headers.py index d83a44f20..385f402c5 100644 --- a/src/mailman/handlers/tests/test_cook_headers.py +++ b/src/mailman/handlers/tests/test_cook_headers.py @@ -17,9 +17,6 @@ """Test the cook_headers handler.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'TestCookHeaders', ] @@ -50,6 +47,6 @@ class TestCookHeaders(unittest.TestCase): for msg in messages: try: cook_headers.process(self._mlist, msg, {}) - except AttributeError as e: + except AttributeError as error: # LP: #1130696 would raise an AttributeError on .sender - self.fail(e) + self.fail(error) diff --git a/src/mailman/handlers/tests/test_file_recips.py b/src/mailman/handlers/tests/test_file_recips.py new file mode 100644 index 000000000..906530762 --- /dev/null +++ b/src/mailman/handlers/tests/test_file_recips.py @@ -0,0 +1,73 @@ +# Copyright (C) 2014 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test file-recips handler.""" + +__all__ = [ + 'TestFileRecips', + ] + + +import os +import unittest + +from mailman.app.lifecycle import create_list +from mailman.config import config +from mailman.testing.helpers import specialized_message_from_string as mfs +from mailman.testing.layers import ConfigLayer + + + +class TestFileRecips(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('test@example.com') + self._handler = config.handlers['file-recipients'].process + self._msg = mfs("""\ +From: aperson@example.com + +A message. +""") + + def test_file_is_missing(self): + # It is not an error for the list's the members.txt file to be + # missing. The missing file is just ignored. + msgdata = {} + self._handler(self._mlist, self._msg, msgdata) + self.assertEqual(msgdata['recipients'], set()) + + def test_file_exists(self): + # Like above, but the file exists and contains recipients. + path = os.path.join(self._mlist.data_path, 'members.txt') + with open(path, 'w', encoding='utf-8') as fp: + print('bperson@example.com', file=fp) + print('cperson@example.com', file=fp) + print('dperson@example.com', file=fp) + print('eperson@example.com', file=fp) + print('fperson@example.com', file=fp) + print('gperson@example.com', file=fp) + msgdata = {} + self._handler(self._mlist, self._msg, msgdata) + self.assertEqual(msgdata['recipients'], set(( + 'bperson@example.com', + 'cperson@example.com', + 'dperson@example.com', + 'eperson@example.com', + 'fperson@example.com', + 'gperson@example.com', + ))) diff --git a/src/mailman/handlers/tests/test_filter.py b/src/mailman/handlers/tests/test_filter.py new file mode 100644 index 000000000..b81744008 --- /dev/null +++ b/src/mailman/handlers/tests/test_filter.py @@ -0,0 +1,57 @@ +# Copyright (C) 2014 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test the filter handler.""" + +__all__ = [ + 'TestFilters', + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.config import config +from mailman.core.errors import DiscardMessage +from mailman.interfaces.mime import FilterAction +from mailman.testing.helpers import specialized_message_from_string as mfs +from mailman.testing.layers import ConfigLayer + + + +class TestFilters(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('test@example.com') + + def test_discard_when_outer_type_matches(self): + # When the outer MIME type of the message matches a filter type, the + # entire message is discarded. + self._mlist.filter_content = True + self._mlist.filter_types = ['image/jpeg'] + self._mlist.filter_action = FilterAction.discard + msg = mfs("""\ +From: aperson@example.com +Content-Type: image/jpeg +MIME-Version: 1.0 + +xxxxx +""") + self.assertRaises(DiscardMessage, + config.handlers['mime-delete'].process, + self._mlist, msg, {}) diff --git a/src/mailman/handlers/tests/test_mimedel.py b/src/mailman/handlers/tests/test_mimedel.py index c7c37152f..02cb275e0 100644 --- a/src/mailman/handlers/tests/test_mimedel.py +++ b/src/mailman/handlers/tests/test_mimedel.py @@ -17,9 +17,6 @@ """Test the mime_delete handler.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'TestDispose', ] @@ -27,8 +24,6 @@ __all__ = [ import unittest -from zope.component import getUtility - from mailman.app.lifecycle import create_list from mailman.config import config from mailman.core import errors @@ -40,6 +35,7 @@ from mailman.testing.helpers import ( LogFileMark, configuration, get_queue_messages, specialized_message_from_string as mfs) from mailman.testing.layers import ConfigLayer +from zope.component import getUtility diff --git a/src/mailman/handlers/tests/test_recipients.py b/src/mailman/handlers/tests/test_recipients.py index afe533a7e..688dcce04 100644 --- a/src/mailman/handlers/tests/test_recipients.py +++ b/src/mailman/handlers/tests/test_recipients.py @@ -17,9 +17,6 @@ """Testing various recipients stuff.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'TestMemberRecipients', 'TestOwnerRecipients', @@ -28,13 +25,14 @@ __all__ = [ import unittest -from zope.component import getUtility from mailman.app.lifecycle import create_list from mailman.config import config from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole from mailman.interfaces.usermanager import IUserManager -from mailman.testing.helpers import specialized_message_from_string as mfs +from mailman.testing.helpers import ( + configuration, specialized_message_from_string as mfs) from mailman.testing.layers import ConfigLayer +from zope.component import getUtility @@ -199,23 +197,14 @@ To: test-owner@example.com self._process(self._mlist, self._msg, msgdata) self.assertEqual(msgdata['recipients'], set(('noreply@example.com',))) - def test_site_admin_unicode(self): - # Since the config file is read as bytes, the site_owner is also a - # bytes and must be converted to unicode when used as a fallback. + @configuration('mailman', site_owner='siteadmin@example.com') + def test_no_owners_site_owner_fallback(self): + # The list has no owners or moderators, but there is a non-default + # site owner defined. That owner gets the message. self._cris.unsubscribe() self._dave.unsubscribe() self.assertEqual(self._mlist.administrators.member_count, 0) msgdata = {} - # In order to properly mimic the testing environment, use - # config.push()/config.pop() directly instead of using the - # configuration() context manager. - config.push('test_site_admin_unicode', b"""\ -[mailman] -site_owner: siteadmin@example.com -""") - try: - self._process(self._mlist, self._msg, msgdata) - finally: - config.pop('test_site_admin_unicode') - self.assertEqual(len(msgdata['recipients']), 1) - self.assertIsInstance(list(msgdata['recipients'])[0], unicode) + self._process(self._mlist, self._msg, msgdata) + self.assertEqual(msgdata['recipients'], + set(('siteadmin@example.com',))) diff --git a/src/mailman/handlers/tests/test_subject_prefix.py b/src/mailman/handlers/tests/test_subject_prefix.py new file mode 100644 index 000000000..f4fd8c113 --- /dev/null +++ b/src/mailman/handlers/tests/test_subject_prefix.py @@ -0,0 +1,129 @@ +# Copyright (C) 2014 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test the Subject header prefix munging..""" + +__all__ = [ + 'TestSubjectPrefix', + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.config import config +from mailman.email.message import Message +from mailman.testing.layers import ConfigLayer + + + +class TestSubjectPrefix(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('test@example.com') + self._process = config.handlers['subject-prefix'].process + + def test_isdigest(self): + # If the message is destined for the digest, the Subject header does + # not get touched. + msg = Message() + msg['Subject'] = 'A test message' + self._process(self._mlist, msg, dict(isdigest=True)) + self.assertEqual(str(msg['subject']), 'A test message') + + def test_fasttrack(self): + # Messages internally crafted are 'fast tracked' and don't get their + # Subjects prefixed either. + msg = Message() + msg['Subject'] = 'A test message' + self._process(self._mlist, msg, dict(_fasttrack=True)) + self.assertEqual(str(msg['subject']), 'A test message') + + def test_whitespace_only_prefix(self): + # If the Subject prefix only contains whitespace, ignore it. + self._mlist.subject_prefix = ' ' + msg = Message() + msg['Subject'] = 'A test message' + self._process(self._mlist, msg, dict(_fasttrack=True)) + self.assertEqual(str(msg['subject']), 'A test message') + + def test_save_original_subject(self): + # When the Subject gets prefixed, the original is saved in the message + # metadata. + msgdata = {} + msg = Message() + msg['Subject'] = 'A test message' + self._process(self._mlist, msg, msgdata) + self.assertEqual(msgdata['original_subject'], 'A test message') + + def test_prefix(self): + # The Subject gets prefixed. The prefix gets automatically set by the + # list style when the list gets created. + msg = Message() + msg['Subject'] = 'A test message' + self._process(self._mlist, msg, {}) + self.assertEqual(str(msg['subject']), '[Test] A test message') + + def test_no_double_prefix(self): + # Don't add a prefix if the subject already contains one. + msg = Message() + msg['Subject'] = '[Test] A test message' + self._process(self._mlist, msg, {}) + self.assertEqual(str(msg['subject']), '[Test] A test message') + + def test_re_prefix(self): + # The subject has a Re: prefix. Make sure that gets preserved, but + # after the list prefix. + msg = Message() + msg['Subject'] = 'Re: [Test] A test message' + self._process(self._mlist, msg, {}) + self.assertEqual(str(msg['subject']), '[Test] Re: A test message') + + def test_multiline_subject(self): + # The subject appears on multiple lines. + msg = Message() + msg['Subject'] = '\n A test message' + self._process(self._mlist, msg, {}) + self.assertEqual(str(msg['subject']), '[Test] A test message') + + def test_i18n_prefix(self): + # The Subject header is encoded, but the prefix is still added. + msg = Message() + msg['Subject'] = '=?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=' + self._process(self._mlist, msg, {}) + subject = msg['subject'] + self.assertEqual(subject.encode(), + '[Test] =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=') + self.assertEqual(str(subject), '[Test] メールマン') + + def test_i18n_subject_with_sequential_prefix_and_re(self): + # The mailing list defines a sequential prefix, and the original + # Subject has a prefix with a different sequence number, *and* it also + # contains a Re: prefix. Make sure the sequence gets updated and all + # the bits get put back together in the right order. + self._mlist.subject_prefix = '[Test %d]' + self._mlist.post_id = 456 + msg = Message() + msg['Subject'] = \ + '[Test 123] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=' + self._process(self._mlist, msg, {}) + subject = msg['subject'] + self.assertEqual( + subject.encode(), + '[Test 456] Re: =?iso-2022-jp?b?GyRCJWEhPCVrJV4lcxsoQg==?=') + self.assertEqual(str(subject), '[Test 456] Re: メールマン') diff --git a/src/mailman/handlers/tests/test_to_digest.py b/src/mailman/handlers/tests/test_to_digest.py index 451ebf9a5..8562c3fd7 100644 --- a/src/mailman/handlers/tests/test_to_digest.py +++ b/src/mailman/handlers/tests/test_to_digest.py @@ -17,9 +17,6 @@ """Test the to_digest handler.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'TestToDigest', ] diff --git a/src/mailman/handlers/to_archive.py b/src/mailman/handlers/to_archive.py index d18742f3c..d8c61bc7d 100644 --- a/src/mailman/handlers/to_archive.py +++ b/src/mailman/handlers/to_archive.py @@ -17,20 +17,16 @@ """Add the message to the archives.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'ToArchive', ] -from zope.interface import implementer - from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.archiver import ArchivePolicy from mailman.interfaces.handler import IHandler +from zope.interface import implementer diff --git a/src/mailman/handlers/to_digest.py b/src/mailman/handlers/to_digest.py index e915bbfa3..70aeb0dcc 100644 --- a/src/mailman/handlers/to_digest.py +++ b/src/mailman/handlers/to_digest.py @@ -17,9 +17,6 @@ """Add the message to the list's current digest.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'ToDigest', ] @@ -27,8 +24,6 @@ __all__ = [ import os -from zope.interface import implementer - from mailman.config import config from mailman.core.i18n import _ from mailman.email.message import Message @@ -36,6 +31,7 @@ from mailman.interfaces.digests import DigestFrequency from mailman.interfaces.handler import IHandler from mailman.utilities.datetime import now as right_now from mailman.utilities.mailbox import Mailbox +from zope.interface import implementer @@ -55,7 +51,7 @@ class ToDigest: mailbox_path = os.path.join(mlist.data_path, 'digest.mmdf') # Lock the mailbox and append the message. with Mailbox(mailbox_path, create=True) as mbox: - mbox.add(msg.as_string()) + mbox.add(msg) # Calculate the current size of the mailbox file. This will not tell # us exactly how big the resulting MIME and rfc1153 digest will # actually be, but it's the most easily available metric to decide @@ -75,7 +71,7 @@ class ToDigest: os.rename(mailbox_path, mailbox_dest) config.switchboards['digest'].enqueue( Message(), - listname=mlist.fqdn_listname, + listid=mlist.list_id, digest_path=mailbox_dest, volume=volume, digest_number=digest_number) diff --git a/src/mailman/handlers/to_outgoing.py b/src/mailman/handlers/to_outgoing.py index 6dfbe88c0..95686d9c7 100644 --- a/src/mailman/handlers/to_outgoing.py +++ b/src/mailman/handlers/to_outgoing.py @@ -22,19 +22,15 @@ posted to the list membership. Anything else that needs to go out to some recipient should just be placed in the out queue directly. """ -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'ToOutgoing', ] -from zope.interface import implementer - from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler +from zope.interface import implementer @@ -47,5 +43,4 @@ class ToOutgoing: def process(self, mlist, msg, msgdata): """See `IHandler`.""" - config.switchboards['out'].enqueue( - msg, msgdata, listname=mlist.fqdn_listname) + config.switchboards['out'].enqueue(msg, msgdata, listid=mlist.list_id) diff --git a/src/mailman/handlers/to_usenet.py b/src/mailman/handlers/to_usenet.py index d5a946644..8d86ea86e 100644 --- a/src/mailman/handlers/to_usenet.py +++ b/src/mailman/handlers/to_usenet.py @@ -17,9 +17,6 @@ """Move the message to the mail->news queue.""" -from __future__ import absolute_import, print_function, unicode_literals - -__metaclass__ = type __all__ = [ 'ToUsenet', ] @@ -27,14 +24,13 @@ __all__ = [ import logging -from zope.interface import implementer - from mailman.config import config from mailman.core.i18n import _ from mailman.interfaces.handler import IHandler +from zope.interface import implementer -COMMASPACE = ', ' +COMMASPACE = ', ' log = logging.getLogger('mailman.error') @@ -65,5 +61,4 @@ class ToUsenet: COMMASPACE.join(error)) return # Put the message in the news runner's queue. - config.switchboards['nntp'].enqueue( - msg, msgdata, listname=mlist.fqdn_listname) + config.switchboards['nntp'].enqueue(msg, msgdata, listid=mlist.list_id) |
