summaryrefslogtreecommitdiff
path: root/src/mailman/handlers
diff options
context:
space:
mode:
authorBarry Warsaw2015-01-04 20:20:33 -0500
committerBarry Warsaw2015-01-04 20:20:33 -0500
commit4a612db8e89afed74173b93f3b64fa567b8417a3 (patch)
tree81a687d113079a25f93279f35c7eee2aa2572510 /src/mailman/handlers
parent84af79988a4e916604cba31843778206efb7d1b8 (diff)
parentde181c1a40965a3a7deedd56a034a946f45b6984 (diff)
downloadmailman-4a612db8e89afed74173b93f3b64fa567b8417a3.tar.gz
mailman-4a612db8e89afed74173b93f3b64fa567b8417a3.tar.zst
mailman-4a612db8e89afed74173b93f3b64fa567b8417a3.zip
Merge the Python 3 branch.
Diffstat (limited to 'src/mailman/handlers')
-rw-r--r--src/mailman/handlers/acknowledge.py11
-rw-r--r--src/mailman/handlers/after_delivery.py6
-rw-r--r--src/mailman/handlers/avoid_duplicates.py6
-rw-r--r--src/mailman/handlers/cleanse.py6
-rw-r--r--src/mailman/handlers/cleanse_dkim.py6
-rw-r--r--src/mailman/handlers/cook_headers.py131
-rw-r--r--src/mailman/handlers/decorate.py10
-rw-r--r--src/mailman/handlers/docs/acknowledge.rst8
-rw-r--r--src/mailman/handlers/docs/avoid-duplicates.rst12
-rw-r--r--src/mailman/handlers/docs/digests.rst6
-rw-r--r--src/mailman/handlers/docs/file-recips.rst27
-rw-r--r--src/mailman/handlers/docs/filtering.rst22
-rw-r--r--src/mailman/handlers/docs/nntp.rst2
-rw-r--r--src/mailman/handlers/docs/replybot.rst8
-rw-r--r--src/mailman/handlers/docs/rfc-2369.rst2
-rw-r--r--src/mailman/handlers/docs/subject-munging.rst107
-rw-r--r--src/mailman/handlers/docs/tagger.rst24
-rw-r--r--src/mailman/handlers/docs/to-outgoing.rst2
-rw-r--r--src/mailman/handlers/file_recipients.py6
-rw-r--r--src/mailman/handlers/member_recipients.py6
-rw-r--r--src/mailman/handlers/mime_delete.py10
-rw-r--r--src/mailman/handlers/owner_recipients.py6
-rw-r--r--src/mailman/handlers/replybot.py8
-rw-r--r--src/mailman/handlers/rfc_2369.py6
-rw-r--r--src/mailman/handlers/subject_prefix.py184
-rw-r--r--src/mailman/handlers/tagger.py12
-rw-r--r--src/mailman/handlers/tests/test_cook_headers.py7
-rw-r--r--src/mailman/handlers/tests/test_file_recips.py73
-rw-r--r--src/mailman/handlers/tests/test_filter.py57
-rw-r--r--src/mailman/handlers/tests/test_mimedel.py6
-rw-r--r--src/mailman/handlers/tests/test_recipients.py31
-rw-r--r--src/mailman/handlers/tests/test_subject_prefix.py129
-rw-r--r--src/mailman/handlers/tests/test_to_digest.py3
-rw-r--r--src/mailman/handlers/to_archive.py6
-rw-r--r--src/mailman/handlers/to_digest.py10
-rw-r--r--src/mailman/handlers/to_outgoing.py9
-rw-r--r--src/mailman/handlers/to_usenet.py11
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)