summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mailman/Utils.py13
-rw-r--r--mailman/app/bounces.py3
-rw-r--r--mailman/app/commands.py6
-rw-r--r--mailman/app/lifecycle.py4
-rw-r--r--mailman/app/membership.py4
-rw-r--r--mailman/app/moderator.py8
-rw-r--r--mailman/app/notifications.py2
-rw-r--r--mailman/app/registrar.py2
-rw-r--r--mailman/app/replybot.py3
-rw-r--r--mailman/archiving/mailarchive.py2
-rw-r--r--mailman/archiving/mhonarc.py17
-rw-r--r--mailman/archiving/pipermail.py15
-rw-r--r--mailman/archiving/prototype.py2
-rw-r--r--mailman/chains/accept.py6
-rw-r--r--mailman/chains/base.py5
-rw-r--r--mailman/chains/builtin.py8
-rw-r--r--mailman/chains/discard.py6
-rw-r--r--mailman/chains/headers.py8
-rw-r--r--mailman/chains/hold.py20
-rw-r--r--mailman/chains/reject.py6
-rw-r--r--mailman/config/__init__.py4
-rw-r--r--mailman/config/config.py2
-rw-r--r--mailman/constants.py5
-rw-r--r--mailman/core/chains.py9
-rw-r--r--mailman/core/errors.py28
-rw-r--r--mailman/core/initialize.py11
-rw-r--r--mailman/core/logging.py7
-rw-r--r--mailman/core/pipelines.py6
-rw-r--r--mailman/core/plugins.py15
-rw-r--r--mailman/core/rules.py5
-rw-r--r--mailman/database/__init__.py8
-rw-r--r--mailman/database/address.py8
-rw-r--r--mailman/database/language.py10
-rw-r--r--mailman/database/listmanager.py8
-rw-r--r--mailman/database/mailinglist.py39
-rw-r--r--mailman/database/member.py6
-rw-r--r--mailman/database/message.py5
-rw-r--r--mailman/database/messagestore.py14
-rw-r--r--mailman/database/model.py2
-rw-r--r--mailman/database/pending.py8
-rw-r--r--mailman/database/preferences.py12
-rw-r--r--mailman/database/requests.py2
-rw-r--r--mailman/database/roster.py14
-rw-r--r--mailman/database/transaction.py2
-rw-r--r--mailman/database/types.py13
-rw-r--r--mailman/database/user.py8
-rw-r--r--mailman/database/usermanager.py6
-rw-r--r--mailman/database/version.py4
-rw-r--r--mailman/docs/archivers.txt28
-rw-r--r--mailman/docs/chains.txt60
-rw-r--r--mailman/docs/pipelines.txt38
-rw-r--r--mailman/docs/registration.txt13
-rw-r--r--mailman/docs/requests.txt221
-rw-r--r--mailman/domain.py9
-rw-r--r--mailman/i18n.py21
-rw-r--r--mailman/inject.py2
-rw-r--r--mailman/interact.py7
-rw-r--r--mailman/interfaces/address.py13
-rw-r--r--mailman/interfaces/archiver.py3
-rw-r--r--mailman/interfaces/chain.py12
-rw-r--r--mailman/interfaces/command.py10
-rw-r--r--mailman/interfaces/database.py8
-rw-r--r--mailman/interfaces/domain.py8
-rw-r--r--mailman/interfaces/errors.py7
-rw-r--r--mailman/interfaces/handler.py8
-rw-r--r--mailman/interfaces/languages.py9
-rw-r--r--mailman/interfaces/listmanager.py2
-rw-r--r--mailman/interfaces/mailinglist.py4
-rw-r--r--mailman/interfaces/member.py3
-rw-r--r--mailman/interfaces/messages.py9
-rw-r--r--mailman/interfaces/mlistrequest.py8
-rw-r--r--mailman/interfaces/mta.py2
-rw-r--r--mailman/interfaces/pending.py11
-rw-r--r--mailman/interfaces/permissions.py8
-rw-r--r--mailman/interfaces/pipeline.py8
-rw-r--r--mailman/interfaces/preferences.py8
-rw-r--r--mailman/interfaces/registrar.py2
-rw-r--r--mailman/interfaces/requests.py10
-rw-r--r--mailman/interfaces/roster.py8
-rw-r--r--mailman/interfaces/rules.py8
-rw-r--r--mailman/interfaces/runner.py8
-rw-r--r--mailman/interfaces/styles.py2
-rw-r--r--mailman/interfaces/switchboard.py8
-rw-r--r--mailman/interfaces/user.py8
-rw-r--r--mailman/interfaces/usermanager.py8
-rw-r--r--mailman/languages.py7
-rw-r--r--mailman/mta/null.py6
-rw-r--r--mailman/mta/postfix.py2
-rw-r--r--mailman/mta/smtp_direct.py68
-rw-r--r--mailman/options.py13
-rw-r--r--mailman/passwords.py44
-rw-r--r--mailman/pipeline/__init__.py2
-rw-r--r--mailman/pipeline/acknowledge.py6
-rw-r--r--mailman/pipeline/after_delivery.py2
-rw-r--r--mailman/pipeline/avoid_duplicates.py6
-rw-r--r--mailman/pipeline/calculate_recipients.py12
-rw-r--r--mailman/pipeline/cleanse.py6
-rw-r--r--mailman/pipeline/cleanse_dkim.py2
-rw-r--r--mailman/pipeline/cook_headers.py21
-rw-r--r--mailman/pipeline/decorate.py29
-rw-r--r--mailman/pipeline/docs/archives.txt10
-rw-r--r--mailman/pipeline/docs/digests.txt57
-rw-r--r--mailman/pipeline/docs/nntp.txt9
-rw-r--r--mailman/pipeline/docs/to-outgoing.txt13
-rw-r--r--mailman/pipeline/file_recipients.py2
-rw-r--r--mailman/pipeline/mime_delete.py21
-rw-r--r--mailman/pipeline/moderate.py8
-rw-r--r--mailman/pipeline/owner_recipients.py11
-rw-r--r--mailman/pipeline/replybot.py10
-rw-r--r--mailman/pipeline/scrubber.py2
-rw-r--r--mailman/pipeline/tagger.py18
-rw-r--r--mailman/pipeline/to_archive.py2
-rw-r--r--mailman/pipeline/to_digest.py14
-rw-r--r--mailman/pipeline/to_outgoing.py2
-rw-r--r--mailman/pipeline/to_usenet.py6
-rw-r--r--mailman/queue/__init__.py36
-rw-r--r--mailman/queue/docs/runner.txt12
-rw-r--r--mailman/queue/docs/switchboard.txt55
-rw-r--r--mailman/rules/__init__.py2
-rw-r--r--mailman/rules/administrivia.py2
-rw-r--r--mailman/rules/any.py6
-rw-r--r--mailman/rules/approved.py4
-rw-r--r--mailman/rules/docs/administrivia.txt4
-rw-r--r--mailman/rules/docs/approve.txt4
-rw-r--r--mailman/rules/docs/implicit-dest.txt4
-rw-r--r--mailman/rules/docs/loop.txt4
-rw-r--r--mailman/rules/docs/max-size.txt4
-rw-r--r--mailman/rules/docs/moderation.txt8
-rw-r--r--mailman/rules/docs/news-moderation.txt4
-rw-r--r--mailman/rules/docs/no-subject.txt4
-rw-r--r--mailman/rules/docs/recipients.txt4
-rw-r--r--mailman/rules/docs/rules.txt4
-rw-r--r--mailman/rules/docs/suspicious.txt4
-rw-r--r--mailman/rules/emergency.py8
-rw-r--r--mailman/rules/implicit_dest.py6
-rw-r--r--mailman/rules/loop.py8
-rw-r--r--mailman/rules/max_recipients.py6
-rw-r--r--mailman/rules/max_size.py6
-rw-r--r--mailman/rules/moderation.py4
-rw-r--r--mailman/rules/news_moderation.py8
-rw-r--r--mailman/rules/no_subject.py6
-rw-r--r--mailman/rules/suspicious.py6
-rw-r--r--mailman/rules/truth.py6
-rw-r--r--mailman/styles/default.py38
-rw-r--r--mailman/styles/manager.py2
-rw-r--r--mailman/testing/helpers.py2
-rw-r--r--mailman/testing/layers.py10
-rw-r--r--mailman/testing/mta.py2
-rw-r--r--mailman/testing/smtplistener.py10
-rw-r--r--mailman/tests/test_bounces.py8
-rw-r--r--mailman/tests/test_documentation.py24
-rw-r--r--mailman/tests/test_membership.py8
-rw-r--r--mailman/tests/test_passwords.py8
-rw-r--r--mailman/tests/test_security_mgr.py8
-rw-r--r--mailman/utilities/__init__.py0
-rw-r--r--mailman/utilities/string.py59
-rw-r--r--template.py24
157 files changed, 1286 insertions, 560 deletions
diff --git a/mailman/Utils.py b/mailman/Utils.py
index 26235d47d..10e201638 100644
--- a/mailman/Utils.py
+++ b/mailman/Utils.py
@@ -36,13 +36,14 @@ import email.Iterators
from email.Errors import HeaderParseError
from lazr.config import as_boolean
-from string import ascii_letters, digits, whitespace, Template
+from string import ascii_letters, digits, whitespace
import mailman.templates
from mailman import passwords
from mailman.config import config
from mailman.core import errors
+from mailman.utilities.string import expand
AT = '@'
@@ -418,7 +419,7 @@ def UnobscureEmail(addr):
class OuterExit(Exception):
pass
-def findtext(templatefile, dict=None, raw=False, lang=None, mlist=None):
+def findtext(templatefile, raw_dict=None, raw=False, lang=None, mlist=None):
# Make some text from a template file. The order of searches depends on
# whether mlist and lang are provided. Once the templatefile is found,
# string substitution is performed by interpolation in `dict'. If `raw'
@@ -524,12 +525,8 @@ def findtext(templatefile, dict=None, raw=False, lang=None, mlist=None):
fp.close()
template = unicode(template, GetCharSet(lang), 'replace')
text = template
- if dict is not None:
- try:
- text = Template(template).safe_substitute(**dict)
- except (TypeError, ValueError):
- # The template is really screwed up
- log.exception('broken template: %s', filename)
+ if raw_dict is not None:
+ text = expand(template, raw_dict)
if raw:
return text, filename
return wrap(text), filename
diff --git a/mailman/app/bounces.py b/mailman/app/bounces.py
index 704921f38..875f615a5 100644
--- a/mailman/app/bounces.py
+++ b/mailman/app/bounces.py
@@ -17,6 +17,9 @@
"""Application level bounce handling."""
+from __future__ import unicode_literals
+
+__metaclass__ = type
__all__ = [
'bounce_message',
]
diff --git a/mailman/app/commands.py b/mailman/app/commands.py
index ef7b0a7c7..d7676af9c 100644
--- a/mailman/app/commands.py
+++ b/mailman/app/commands.py
@@ -17,6 +17,8 @@
"""Initialize the email commands."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'initialize',
@@ -37,6 +39,6 @@ def initialize():
if not IEmailCommand.implementedBy(command_class):
continue
assert command_class.name not in config.commands, (
- 'Duplicate email command "%s" found in %s' %
- (command_class.name, module))
+ 'Duplicate email command "{0}" found in {1}'.format(
+ command_class.name, module))
config.commands[command_class.name] = command_class()
diff --git a/mailman/app/lifecycle.py b/mailman/app/lifecycle.py
index 7b7daadf3..c57d7e1fc 100644
--- a/mailman/app/lifecycle.py
+++ b/mailman/app/lifecycle.py
@@ -17,6 +17,8 @@
"""Application level list creation."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'create_list',
@@ -83,7 +85,7 @@ def remove_list(fqdn_listname, mailing_list=None, archives=True):
# Do the MTA-specific list deletion tasks
module_name, class_name = config.mta.incoming.rsplit('.', 1)
__import__(module_name)
- getattr(sys.modules[module_name], class_name)().create(mlist)
+ getattr(sys.modules[module_name], class_name)().create(mailing_list)
# Remove the list directory.
removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname))
# Remove any stale locks associated with the list.
diff --git a/mailman/app/membership.py b/mailman/app/membership.py
index 4d20be91e..4b9609469 100644
--- a/mailman/app/membership.py
+++ b/mailman/app/membership.py
@@ -17,6 +17,8 @@
"""Application support for membership management."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'add_member',
@@ -99,7 +101,7 @@ def add_member(mlist, address, realname, password, delivery_mode, language):
break
else:
raise AssertionError(
- 'User should have had linked address: %s', address)
+ 'User should have had linked address: {0}'.format(address))
# Create the member and set the appropriate preferences.
member = address_obj.subscribe(mlist, MemberRole.member)
member.preferences.preferred_language = language
diff --git a/mailman/app/moderator.py b/mailman/app/moderator.py
index 73a341534..b40a34344 100644
--- a/mailman/app/moderator.py
+++ b/mailman/app/moderator.py
@@ -17,6 +17,8 @@
"""Application support for moderators."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'handle_message',
@@ -144,7 +146,7 @@ def handle_message(mlist, id, action,
# processing.
config.switchboards['in'].enqueue(msg, _metadata=msgdata)
else:
- raise AssertionError('Unexpected action: %s' % action)
+ raise AssertionError('Unexpected action: {0}'.format(action))
# Forward the message.
if forward:
# Get a copy of the original message from the message store.
@@ -257,7 +259,7 @@ def handle_subscription(mlist, id, action, comment=None):
delivery_mode, formataddr((realname, address)),
'via admin approval')
else:
- raise AssertionError('Unexpected action: %s' % action)
+ raise AssertionError('Unexpected action: {0}'.format(action))
# Delete the request from the database.
requestdb.delete_request(id)
@@ -313,7 +315,7 @@ def handle_unsubscription(mlist, id, action, comment=None):
pass
slog.info('%s: deleted %s', mlist.fqdn_listname, address)
else:
- raise AssertionError('Unexpected action: %s' % action)
+ raise AssertionError('Unexpected action: {0}'.format(action))
# Delete the request from the database.
requestdb.delete_request(id)
diff --git a/mailman/app/notifications.py b/mailman/app/notifications.py
index d60fe5fcf..9bef9998b 100644
--- a/mailman/app/notifications.py
+++ b/mailman/app/notifications.py
@@ -17,6 +17,8 @@
"""Sending notifications."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'send_admin_subscription_notice',
diff --git a/mailman/app/registrar.py b/mailman/app/registrar.py
index ad291891e..6a2abeba9 100644
--- a/mailman/app/registrar.py
+++ b/mailman/app/registrar.py
@@ -17,6 +17,8 @@
"""Implementation of the IUserRegistrar interface."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'Registrar',
diff --git a/mailman/app/replybot.py b/mailman/app/replybot.py
index e9c303418..0537f6645 100644
--- a/mailman/app/replybot.py
+++ b/mailman/app/replybot.py
@@ -21,6 +21,9 @@
# mailing list. The reply governor should really apply site-wide per
# recipient (I think).
+from __future__ import unicode_literals
+
+__metaclass__ = type
__all__ = [
'autorespond_to_sender',
'can_acknowledge',
diff --git a/mailman/archiving/mailarchive.py b/mailman/archiving/mailarchive.py
index 334818204..a5eb27db0 100644
--- a/mailman/archiving/mailarchive.py
+++ b/mailman/archiving/mailarchive.py
@@ -17,6 +17,8 @@
"""The Mail-Archive.com archiver."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'MailArchive',
diff --git a/mailman/archiving/mhonarc.py b/mailman/archiving/mhonarc.py
index 76d022c92..949a79144 100644
--- a/mailman/archiving/mhonarc.py
+++ b/mailman/archiving/mhonarc.py
@@ -17,6 +17,8 @@
"""MHonArc archiver."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'MHonArc',
@@ -28,12 +30,12 @@ import logging
import subprocess
from base64 import b32encode
-from string import Template
from urlparse import urljoin
from zope.interface import implements
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
+from mailman.utilities.string import expand
log = logging.getLogger('mailman.archiver')
@@ -52,11 +54,11 @@ class MHonArc:
"""See `IArchiver`."""
# XXX What about private MHonArc archives?
web_host = config.domains[mlist.host_name].url_host
- return Template(config.archiver.mhonarc.base_url).safe_substitute(
- listname=mlist.fqdn_listname,
- hostname=web_host,
- fqdn_listname=mlist.fqdn_listname,
- )
+ return expand(config.archiver.mhonarc.base_url,
+ dict(listname=mlist.fqdn_listname,
+ hostname=web_host,
+ fqdn_listname=mlist.fqdn_listname,
+ ))
@staticmethod
def permalink(mlist, msg):
@@ -83,8 +85,7 @@ class MHonArc:
"""See `IArchiver`."""
substitutions = config.__dict__.copy()
substitutions['listname'] = mlist.fqdn_listname
- command = Template(config.archiver.mhonarc.command).safe_substitute(
- substitutions)
+ command = expand(config.archiver.mhonarc.command, substitutions)
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True)
diff --git a/mailman/archiving/pipermail.py b/mailman/archiving/pipermail.py
index d4969b9f6..4ee2e8dff 100644
--- a/mailman/archiving/pipermail.py
+++ b/mailman/archiving/pipermail.py
@@ -17,6 +17,8 @@
"""Pipermail archiver."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Pipermail',
@@ -26,7 +28,6 @@ __all__ = [
import os
from cStringIO import StringIO
-from string import Template
from zope.interface import implements
from zope.interface.interface import adapter_hooks
@@ -34,6 +35,7 @@ from mailman.Utils import makedirs
from mailman.config import config
from mailman.interfaces.archiver import IArchiver, IPipermailMailingList
from mailman.interfaces.mailinglist import IMailingList
+from mailman.utilities.string import expand
from mailman.Archiver.HyperArch import HyperArchive
@@ -94,12 +96,11 @@ class Pipermail:
url = mlist.script_url('private') + '/index.html'
else:
web_host = config.domains[mlist.host_name].url_host
- url = Template(config.archiver.pipermail.base_url).safe_substitute(
- listname=mlist.fqdn_listname,
- hostname=web_host,
- fqdn_listname=mlist.fqdn_listname,
- )
- return url
+ return expand(config.archiver.pipermail.base_url,
+ dict(listname=mlist.fqdn_listname,
+ hostname=web_host,
+ fqdn_listname=mlist.fqdn_listname,
+ ))
@staticmethod
def permalink(mlist, message):
diff --git a/mailman/archiving/prototype.py b/mailman/archiving/prototype.py
index 8fbbe47c4..81163e184 100644
--- a/mailman/archiving/prototype.py
+++ b/mailman/archiving/prototype.py
@@ -17,6 +17,8 @@
"""Prototypical permalinking archiver."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Prototype',
diff --git a/mailman/chains/accept.py b/mailman/chains/accept.py
index d0c570c69..bd47f42c8 100644
--- a/mailman/chains/accept.py
+++ b/mailman/chains/accept.py
@@ -17,8 +17,12 @@
"""The terminal 'accept' chain."""
-__all__ = ['AcceptChain']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'AcceptChain',
+ ]
import logging
diff --git a/mailman/chains/base.py b/mailman/chains/base.py
index f4e70c303..bcd946b40 100644
--- a/mailman/chains/base.py
+++ b/mailman/chains/base.py
@@ -17,6 +17,8 @@
"""Base class for terminal chains."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Chain',
@@ -78,7 +80,8 @@ class Chain:
implements(IMutableChain)
def __init__(self, name, description):
- assert name not in config.chains, 'Duplicate chain name: %s' % name
+ assert name not in config.chains, (
+ 'Duplicate chain name: {0}'.format(name))
self.name = name
self.description = description
self._links = []
diff --git a/mailman/chains/builtin.py b/mailman/chains/builtin.py
index 1af1dd411..05912a2f2 100644
--- a/mailman/chains/builtin.py
+++ b/mailman/chains/builtin.py
@@ -17,6 +17,8 @@
"""The default built-in starting chain."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'BuiltInChain',
@@ -78,9 +80,7 @@ class BuiltInChain:
# Get the named rule.
rule = config.rules[rule_name]
# Get the chain, if one is defined.
- if chain_name is None:
- chain = None
- else:
- chain = config.chains[chain_name]
+ chain = (None if chain_name is None
+ else config.chains[chain_name])
links.append(Link(rule, action, chain))
return iter(self._cached_links)
diff --git a/mailman/chains/discard.py b/mailman/chains/discard.py
index 525505e36..1899e0340 100644
--- a/mailman/chains/discard.py
+++ b/mailman/chains/discard.py
@@ -17,8 +17,12 @@
"""The terminal 'discard' chain."""
-__all__ = ['DiscardChain']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'DiscardChain',
+ ]
import logging
diff --git a/mailman/chains/headers.py b/mailman/chains/headers.py
index d76e48c50..2f85d78d0 100644
--- a/mailman/chains/headers.py
+++ b/mailman/chains/headers.py
@@ -17,6 +17,8 @@
"""The header-matching chain."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'HeaderMatchChain',
@@ -56,7 +58,7 @@ def make_link(entry):
# We don't assert that the chain exists here because the jump
# chain may not yet have been created.
else:
- raise AssertionError('Bad link description: %s' % entry)
+ raise AssertionError('Bad link description: {0}'.format(entry))
rule = HeaderMatchRule(header, pattern)
chain = config.chains[chain_name]
return Link(rule, LinkAction.jump, chain)
@@ -73,9 +75,9 @@ class HeaderMatchRule:
def __init__(self, header, pattern):
self._header = header
self._pattern = pattern
- self.name = 'header-match-%002d' % HeaderMatchRule._count
+ self.name = 'header-match-{0:02}'.format(HeaderMatchRule._count)
HeaderMatchRule._count += 1
- self.description = u'%s: %s' % (header, pattern)
+ self.description = '{0}: {1}'.format(header, pattern)
# XXX I think we should do better here, somehow recording that a
# particular header matched a particular pattern, but that gets ugly
# with RFC 2822 headers. It also doesn't match well with the rule
diff --git a/mailman/chains/hold.py b/mailman/chains/hold.py
index f46733304..16238a541 100644
--- a/mailman/chains/hold.py
+++ b/mailman/chains/hold.py
@@ -17,6 +17,8 @@
"""The terminal 'hold' chain."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'HoldChain',
@@ -91,14 +93,14 @@ class HoldChain(TerminalChainBase):
original_subject = _('(no subject)')
else:
original_subject = oneline(original_subject, charset)
- substitutions = {
- 'listname' : mlist.fqdn_listname,
- 'subject' : original_subject,
- 'sender' : sender,
- 'reason' : 'XXX', #reason,
- 'confirmurl' : '%s/%s' % (mlist.script_url('confirm'), token),
- 'admindb_url': mlist.script_url('admindb'),
- }
+ substitutions = dict(
+ listname = mlist.fqdn_listname,
+ subject = original_subject,
+ sender = sender,
+ reason = 'XXX', #reason,
+ confirmurl = '{0}/{1}'.format(mlist.script_url('confirm'), token),
+ admindb_url = mlist.script_url('admindb'),
+ )
# At this point the message is held, but now we have to craft at least
# two responses. The first will go to the original author of the
# message and it will contain the token allowing them to approve or
@@ -167,7 +169,7 @@ also appear in the first line of the body of the reply.""")),
nmsg.attach(text)
nmsg.attach(MIMEMessage(msg))
nmsg.attach(MIMEMessage(dmsg))
- nmsg.send(mlist, **{'tomoderators': 1})
+ nmsg.send(mlist, **dict(tomoderators=True))
# Log the held message
# XXX reason
reason = 'n/a'
diff --git a/mailman/chains/reject.py b/mailman/chains/reject.py
index c7ec04fec..3faf563da 100644
--- a/mailman/chains/reject.py
+++ b/mailman/chains/reject.py
@@ -17,8 +17,12 @@
"""The terminal 'reject' chain."""
-__all__ = ['RejectChain']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'RejectChain',
+ ]
import logging
diff --git a/mailman/config/__init__.py b/mailman/config/__init__.py
index 0611fc154..11f4f0c80 100644
--- a/mailman/config/__init__.py
+++ b/mailman/config/__init__.py
@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Mailman configuration package."""
+
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'config',
diff --git a/mailman/config/config.py b/mailman/config/config.py
index 44bb6d491..189917999 100644
--- a/mailman/config/config.py
+++ b/mailman/config/config.py
@@ -17,6 +17,8 @@
"""Configuration file loading and management."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Configuration',
diff --git a/mailman/constants.py b/mailman/constants.py
index d924b6722..39c4547f8 100644
--- a/mailman/constants.py
+++ b/mailman/constants.py
@@ -17,6 +17,9 @@
"""Various constants and enumerations."""
+from __future__ import unicode_literals
+
+__metaclass__ = type
__all__ = [
'SystemDefaultPreferences',
]
@@ -29,7 +32,7 @@ from mailman.interfaces.preferences import IPreferences
-class SystemDefaultPreferences(object):
+class SystemDefaultPreferences:
implements(IPreferences)
acknowledge_posts = False
diff --git a/mailman/core/chains.py b/mailman/core/chains.py
index 58935ed39..40b8c779f 100644
--- a/mailman/core/chains.py
+++ b/mailman/core/chains.py
@@ -17,6 +17,8 @@
"""Application support for chain processing."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'initialize',
@@ -56,7 +58,7 @@ def process(mlist, msg, msgdata, start_chain='built-in'):
# we can capture a chain's link iterator in mid-flight. This supports
# the 'detour' link action
try:
- link = chain_iter.next()
+ link = next(chain_iter)
except StopIteration:
# This chain is exhausted. Pop the last chain on the stack and
# continue iterating through it. If there's nothing left on the
@@ -90,7 +92,8 @@ def process(mlist, msg, msgdata, start_chain='built-in'):
elif link.action is LinkAction.run:
link.function(mlist, msg, msgdata)
else:
- raise AssertionError('Bad link action: %s' % link.action)
+ raise AssertionError(
+ 'Bad link action: {0}'.format(link.action))
else:
# The rule did not match; keep going.
if link.rule.record:
@@ -103,7 +106,7 @@ def initialize():
for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain):
chain = chain_class()
assert chain.name not in config.chains, (
- 'Duplicate chain name: %s' % chain.name)
+ 'Duplicate chain name: {0}'.format(chain.name))
config.chains[chain.name] = chain
# Set up a couple of other default chains.
chain = BuiltInChain()
diff --git a/mailman/core/errors.py b/mailman/core/errors.py
index 51803ae79..39401127e 100644
--- a/mailman/core/errors.py
+++ b/mailman/core/errors.py
@@ -17,6 +17,34 @@
"""Mailman errors."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'AlreadyReceivingDigests',
+ 'AlreadyReceivingRegularDeliveries',
+ 'BadDomainSpecificationError',
+ 'BadPasswordSchemeError',
+ 'CantDigestError',
+ 'DiscardMessage',
+ 'EmailAddressError',
+ 'HandlerError',
+ 'HoldMessage',
+ 'HostileSubscriptionError',
+ 'InvalidEmailAddress',
+ 'LostHeldMessage',
+ 'MailmanError',
+ 'MailmanException',
+ 'MemberError',
+ 'MembershipIsBanned',
+ 'MustDigestError',
+ 'NotAMemberError',
+ 'PasswordError',
+ 'RejectMessage',
+ 'SomeRecipientsFailed',
+ 'SubscriptionError',
+ ]
+
# Base class for all exceptions raised in Mailman (XXX except legacy string
diff --git a/mailman/core/initialize.py b/mailman/core/initialize.py
index c8b457d75..bb16f0036 100644
--- a/mailman/core/initialize.py
+++ b/mailman/core/initialize.py
@@ -24,6 +24,17 @@ line argument parsing, since some of the initialization behavior is controlled
by the command line arguments.
"""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ 'initialize_1',
+ 'initialize_2',
+ 'initialize_3',
+ ]
+
+
import os
from zope.interface.interface import adapter_hooks
diff --git a/mailman/core/logging.py b/mailman/core/logging.py
index 43abac78d..a18065965 100644
--- a/mailman/core/logging.py
+++ b/mailman/core/logging.py
@@ -17,7 +17,7 @@
"""Logging initialization, using Python's standard logging package."""
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
@@ -74,11 +74,10 @@ class ReopenableFileHandler(logging.Handler):
stream = (self._stream if self._stream else sys.stderr)
try:
msg = self.format(record)
- fs = '%s\n'
try:
- stream.write(fs % msg)
+ stream.write('{0}'.format(msg))
except UnicodeError:
- stream.write(fs % msg.encode('string-escape'))
+ stream.write('{0}'.format(msg.encode('string-escape')))
self.flush()
except:
self.handleError(record)
diff --git a/mailman/core/pipelines.py b/mailman/core/pipelines.py
index 7be252388..8aae5cc25 100644
--- a/mailman/core/pipelines.py
+++ b/mailman/core/pipelines.py
@@ -17,6 +17,8 @@
"""Pipeline processor."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'initialize',
@@ -114,8 +116,8 @@ def initialize():
handler = handler_class()
verifyObject(IHandler, handler)
assert handler.name not in config.handlers, (
- 'Duplicate handler "%s" found in %s' %
- (handler.name, handler_finder))
+ 'Duplicate handler "{0}" found in {1}'.format(
+ handler.name, handler_finder))
config.handlers[handler.name] = handler
# Set up some pipelines.
for pipeline_class in (BuiltInPipeline, VirginPipeline):
diff --git a/mailman/core/plugins.py b/mailman/core/plugins.py
index cce95fddd..e9ba26571 100644
--- a/mailman/core/plugins.py
+++ b/mailman/core/plugins.py
@@ -17,6 +17,13 @@
"""Get a requested plugin."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ ]
+
+
import pkg_resources
@@ -36,7 +43,8 @@ def get_plugin(group):
"""
entry_points = list(pkg_resources.iter_entry_points(group))
if len(entry_points) == 0:
- raise RuntimeError('No entry points found for group: %s' % group)
+ raise RuntimeError(
+ 'No entry points found for group: {0}'.format(group))
elif len(entry_points) == 1:
# Okay, this is the one to use.
return entry_points[0].load()
@@ -44,14 +52,15 @@ def get_plugin(group):
# Find the one /not/ named 'stock'.
entry_points = [ep for ep in entry_points if ep.name <> 'stock']
if len(entry_points) == 0:
- raise RuntimeError('No stock plugin found for group: %s' % group)
+ raise RuntimeError(
+ 'No stock plugin found for group: {0}'.format(group))
elif len(entry_points) == 2:
raise RuntimeError('Too many stock plugins defined')
else:
raise AssertionError('Insanity')
return entry_points[0].load()
else:
- raise RuntimeError('Too many plugins for group: %s' % group)
+ raise RuntimeError('Too many plugins for group: {0}'.format(group))
diff --git a/mailman/core/rules.py b/mailman/core/rules.py
index e272846d3..83e24dfa2 100644
--- a/mailman/core/rules.py
+++ b/mailman/core/rules.py
@@ -17,6 +17,8 @@
"""Various rule helpers"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'initialize',
@@ -39,5 +41,6 @@ def initialize():
rule = rule_class()
verifyObject(IRule, rule)
assert rule.name not in config.rules, (
- 'Duplicate rule "%s" found in %s' % (rule.name, rule_finder))
+ 'Duplicate rule "{0}" found in {1}'.format(
+ rule.name, rule_finder))
config.rules[rule.name] = rule
diff --git a/mailman/database/__init__.py b/mailman/database/__init__.py
index 4c2a60e5d..8b7f584c2 100644
--- a/mailman/database/__init__.py
+++ b/mailman/database/__init__.py
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'StockDatabase',
@@ -27,7 +29,6 @@ from locknix.lockfile import Lock
from lazr.config import as_boolean
from pkg_resources import resource_string
from storm.locals import create_database, Store
-from string import Template
from urlparse import urlparse
from zope.interface import implements
@@ -41,6 +42,7 @@ from mailman.database.requests import Requests
from mailman.database.usermanager import UserManager
from mailman.database.version import Version
from mailman.interfaces.database import IDatabase, SchemaVersionMismatchError
+from mailman.utilities.string import expand
log = logging.getLogger('mailman.config')
@@ -86,7 +88,7 @@ class StockDatabase:
def _create(self, debug):
# Calculate the engine url.
- url = Template(config.database.url).safe_substitute(config.paths)
+ url = expand(config.database.url, config.paths)
log.debug('Database url: %s', url)
# XXX By design of SQLite, database file creation does not honor
# umask. See their ticket #1193:
@@ -123,7 +125,7 @@ class StockDatabase:
v = store.find(Version, component=u'schema').one()
if not v:
# Database has not yet been initialized
- v = Version(component=u'schema',
+ v = Version(component='schema',
version=mailman.version.DATABASE_SCHEMA_VERSION)
store.add(v)
elif v.version <> mailman.version.DATABASE_SCHEMA_VERSION:
diff --git a/mailman/database/address.py b/mailman/database/address.py
index f9740deaf..528d3af51 100644
--- a/mailman/database/address.py
+++ b/mailman/database/address.py
@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for addresses."""
+
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Address',
@@ -64,10 +68,10 @@ class Address(Model):
verified = ('verified' if self.verified_on else 'not verified')
address_str = str(self)
if self._original is None:
- return '<Address: %s [%s] at %#x>' % (
+ return '<Address: {0} [{1}] at {2:#x}>'.format(
address_str, verified, id(self))
else:
- return '<Address: %s [%s] key: %s at %#x>' % (
+ return '<Address: {0} [{1}] key: {2} at {3:#x}>'.format(
address_str, verified, self.address, id(self))
def subscribe(self, mailing_list, role):
diff --git a/mailman/database/language.py b/mailman/database/language.py
index 192c9a142..8adc5c4a5 100644
--- a/mailman/database/language.py
+++ b/mailman/database/language.py
@@ -15,6 +15,16 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for languages."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Language',
+ ]
+
+
from storm.locals import *
from zope.interface import implements
diff --git a/mailman/database/listmanager.py b/mailman/database/listmanager.py
index 0a80a773e..346580eb9 100644
--- a/mailman/database/listmanager.py
+++ b/mailman/database/listmanager.py
@@ -17,6 +17,14 @@
"""A mailing list manager."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ListManager',
+ ]
+
+
import datetime
from zope.interface import implements
diff --git a/mailman/database/mailinglist.py b/mailman/database/mailinglist.py
index 56caea296..fb45afe96 100644
--- a/mailman/database/mailinglist.py
+++ b/mailman/database/mailinglist.py
@@ -15,6 +15,16 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for mailing lists."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'MailingList',
+ ]
+
+
import os
import string
@@ -28,6 +38,7 @@ from mailman.database import roster
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.mailinglist import IMailingList, Personalization
+from mailman.utilities.string import expand
SPACE = ' '
@@ -220,42 +231,42 @@ class MailingList(Model):
@property
def no_reply_address(self):
- return '%s@%s' % (config.mailman.noreply_address, self.host_name)
+ return '{0}@{1}'.format(config.mailman.noreply_address, self.host_name)
@property
def owner_address(self):
- return '%s-owner@%s' % (self.list_name, self.host_name)
+ return '{0}-owner@{1}'.format(self.list_name, self.host_name)
@property
def request_address(self):
- return '%s-request@%s' % (self.list_name, self.host_name)
+ return '{0}-request@{1}'.format(self.list_name, self.host_name)
@property
def bounces_address(self):
- return '%s-bounces@%s' % (self.list_name, self.host_name)
+ return '{0}-bounces@{1}'.format(self.list_name, self.host_name)
@property
def join_address(self):
- return '%s-join@%s' % (self.list_name, self.host_name)
+ return '{0}-join@{1}'.format(self.list_name, self.host_name)
@property
def leave_address(self):
- return '%s-leave@%s' % (self.list_name, self.host_name)
+ return '{0}-leave@{1}'.format(self.list_name, self.host_name)
@property
def subscribe_address(self):
- return '%s-subscribe@%s' % (self.list_name, self.host_name)
+ return '{0}-subscribe@{1}'.format(self.list_name, self.host_name)
@property
def unsubscribe_address(self):
- return '%s-unsubscribe@%s' % (self.list_name, self.host_name)
+ return '{0}-unsubscribe@{1}'.format(self.list_name, self.host_name)
def confirm_address(self, cookie):
- template = string.Template(config.mta.verp_confirm_format)
- local_part = template.safe_substitute(
- address = '%s-confirm' % self.list_name,
- cookie = cookie)
- return '%s@%s' % (local_part, self.host_name)
+ local_part = expand(config.mta.verp_confirm_format, dict(
+ address = '{0}-confirm'.format(self.list_name),
+ cookie = cookie))
+ return '{0}@{1}'.format(local_part, self.host_name)
def __repr__(self):
- return '<mailing list "%s" at %#x>' % (self.fqdn_listname, id(self))
+ return '<mailing list "{0}" at {1:#x}>'.format(
+ self.fqdn_listname, id(self))
diff --git a/mailman/database/member.py b/mailman/database/member.py
index 66d215e4d..22bf042f6 100644
--- a/mailman/database/member.py
+++ b/mailman/database/member.py
@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for members."""
+
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Member',
@@ -51,7 +55,7 @@ class Member(Model):
self.is_moderated = False
def __repr__(self):
- return '<Member: %s on %s as %s>' % (
+ return '<Member: {0} on {1} as {2}>'.format(
self.address, self.mailing_list, self.role)
def _lookup(self, preference):
diff --git a/mailman/database/message.py b/mailman/database/message.py
index c7ec6bcf6..e77e11429 100644
--- a/mailman/database/message.py
+++ b/mailman/database/message.py
@@ -15,6 +15,11 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for messages."""
+
+
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Message',
diff --git a/mailman/database/messagestore.py b/mailman/database/messagestore.py
index 38c353172..d16fc93c4 100644
--- a/mailman/database/messagestore.py
+++ b/mailman/database/messagestore.py
@@ -15,6 +15,11 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for message stores."""
+
+
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'MessageStore',
@@ -54,8 +59,9 @@ class MessageStore:
existing = config.db.store.find(Message,
Message.message_id == message_id).one()
if existing is not None:
- raise ValueError('Message ID already exists in message store: %s',
- message_id)
+ raise ValueError(
+ 'Message ID already exists in message store: {0}'.format(
+ message_id))
shaobj = hashlib.sha1(message_id)
hash32 = base64.b32encode(shaobj.digest())
del message['X-Message-ID-Hash']
@@ -86,8 +92,8 @@ class MessageStore:
# -1 says to use the highest protocol available.
pickle.dump(message, fp, -1)
break
- except IOError, e:
- if e.errno <> errno.ENOENT:
+ except IOError as error:
+ if error.errno <> errno.ENOENT:
raise
os.makedirs(os.path.dirname(path))
return hash32
diff --git a/mailman/database/model.py b/mailman/database/model.py
index 9a83002bd..85fa033c7 100644
--- a/mailman/database/model.py
+++ b/mailman/database/model.py
@@ -17,6 +17,8 @@
"""Base class for all database classes."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Model',
diff --git a/mailman/database/pending.py b/mailman/database/pending.py
index dc82085a2..75555b976 100644
--- a/mailman/database/pending.py
+++ b/mailman/database/pending.py
@@ -17,6 +17,8 @@
"""Implementations of the IPendable and IPending interfaces."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Pended',
@@ -122,8 +124,8 @@ class Pendings:
value = u'__builtin__.bool\1%s' % value
elif type(value) is list:
# We expect this to be a list of strings.
- value = u'mailman.database.pending.unpack_list\1%s' % (
- '\2'.join(value))
+ value = ('mailman.database.pending.unpack_list\1' +
+ '\2'.join(value))
keyval = PendedKeyValue(key=key, value=value)
pending.key_values.add(keyval)
config.db.store.add(pending)
@@ -135,7 +137,7 @@ class Pendings:
if pendings.count() == 0:
return None
assert pendings.count() == 1, (
- 'Unexpected token count: %d' % pendings.count())
+ 'Unexpected token count: {0}'.format(pendings.count()))
pending = pendings[0]
pendable = UnpendedPendable()
# Find all PendedKeyValue entries that are associated with the pending
diff --git a/mailman/database/preferences.py b/mailman/database/preferences.py
index 244a1d2a7..f3ee55673 100644
--- a/mailman/database/preferences.py
+++ b/mailman/database/preferences.py
@@ -15,6 +15,16 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for preferences."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Preferences',
+ ]
+
+
from storm.locals import *
from zope.interface import implements
@@ -37,4 +47,4 @@ class Preferences(Model):
delivery_status = Enum()
def __repr__(self):
- return '<Preferences object at %#x>' % id(self)
+ return '<Preferences object at {0:#x}>'.format(id(self))
diff --git a/mailman/database/requests.py b/mailman/database/requests.py
index 812c5a0ab..249feb6b6 100644
--- a/mailman/database/requests.py
+++ b/mailman/database/requests.py
@@ -17,6 +17,8 @@
"""Implementations of the IRequests and IListRequests interfaces."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Requests',
diff --git a/mailman/database/roster.py b/mailman/database/roster.py
index fcebfb7f0..fc0a24c7d 100644
--- a/mailman/database/roster.py
+++ b/mailman/database/roster.py
@@ -22,6 +22,8 @@ the ones that fit a particular role. These are used as the member, owner,
moderator, and administrator roster filters.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'AdministratorRoster',
@@ -100,8 +102,9 @@ class AbstractRoster:
elif results.count() == 1:
return results[0]
else:
- raise AssertionError('Too many matching member results: %s' %
- results.count())
+ raise AssertionError(
+ 'Too many matching member results: {0}'.format(
+ results.count()))
@@ -160,7 +163,7 @@ class AdministratorRoster(AbstractRoster):
return results[0]
else:
raise AssertionError(
- 'Too many matching member results: %s' % results)
+ 'Too many matching member results: {0}'.format(results))
@@ -262,5 +265,6 @@ class Memberships:
elif results.count() == 1:
return results[0]
else:
- raise AssertionError('Too many matching member results: %s' %
- results.count())
+ raise AssertionError(
+ 'Too many matching member results: {0}'.format(
+ results.count()))
diff --git a/mailman/database/transaction.py b/mailman/database/transaction.py
index 4663f6110..d42562389 100644
--- a/mailman/database/transaction.py
+++ b/mailman/database/transaction.py
@@ -17,6 +17,8 @@
"""Transactional support."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'txn',
diff --git a/mailman/database/types.py b/mailman/database/types.py
index 17719c941..2f901fe49 100644
--- a/mailman/database/types.py
+++ b/mailman/database/types.py
@@ -15,6 +15,12 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Storm type conversions."""
+
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
__all__ = [
'Enum',
]
@@ -46,9 +52,10 @@ class _EnumVariable(Variable):
return None
if not to_db:
return value
- return '%s.%s:%d' % (value.enumclass.__module__,
- value.enumclass.__name__,
- int(value))
+ return '{0}.{1}:{2}'.format(
+ value.enumclass.__module__,
+ value.enumclass.__name__,
+ int(value))
class Enum(SimpleProperty):
diff --git a/mailman/database/user.py b/mailman/database/user.py
index ec428f0a2..23701686b 100644
--- a/mailman/database/user.py
+++ b/mailman/database/user.py
@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model for users."""
+
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'User',
@@ -48,7 +52,7 @@ class User(Model):
preferences = Reference(preferences_id, 'Preferences.id')
def __repr__(self):
- return '<User "%s" at %#x>' % (self.real_name, id(self))
+ return '<User "{0}" at {1:#x}>'.format(self.real_name, id(self))
def link(self, address):
"""See `IUser`."""
@@ -76,7 +80,7 @@ class User(Model):
addrobj = config.db.store.find(Address, address=address).one()
if addrobj is None:
if real_name is None:
- real_name = u''
+ real_name = ''
addrobj = Address(address=address, real_name=real_name)
addrobj.preferences = Preferences()
# Link the address to the user if it is not already linked.
diff --git a/mailman/database/usermanager.py b/mailman/database/usermanager.py
index c30c40d12..3b0c8b534 100644
--- a/mailman/database/usermanager.py
+++ b/mailman/database/usermanager.py
@@ -17,6 +17,8 @@
"""A user manager."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'UserManager',
@@ -38,7 +40,7 @@ class UserManager(object):
def create_user(self, address=None, real_name=None):
user = User()
- user.real_name = (u'' if real_name is None else real_name)
+ user.real_name = ('' if real_name is None else real_name)
if address:
addrobj = Address(address, user.real_name)
addrobj.preferences = Preferences()
@@ -71,7 +73,7 @@ class UserManager(object):
raise ExistingAddressError(found.original_address)
assert addresses.count() == 0, 'Unexpected results'
if real_name is None:
- real_name = u''
+ real_name = ''
# It's okay not to lower case the 'address' argument because the
# constructor will do the right thing.
address = Address(address, real_name)
diff --git a/mailman/database/version.py b/mailman/database/version.py
index 519c5438c..d15065395 100644
--- a/mailman/database/version.py
+++ b/mailman/database/version.py
@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+"""Model class for version numbers."""
+
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Version',
diff --git a/mailman/docs/archivers.txt b/mailman/docs/archivers.txt
index 489e3f15b..ef36a25ac 100644
--- a/mailman/docs/archivers.txt
+++ b/mailman/docs/archivers.txt
@@ -77,10 +77,10 @@ archiver; by enabling it messages to public lists will get sent there
automatically.
>>> archiver = archivers['mail-archive']
- >>> archiver.list_url(mlist)
- 'http://go.mail-archive.dev/test%40example.com'
- >>> archiver.permalink(mlist, msg)
- 'http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc='
+ >>> print archiver.list_url(mlist)
+ http://go.mail-archive.dev/test%40example.com
+ >>> print archiver.permalink(mlist, msg)
+ http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
To archive the message, the archiver actually mails the message to a special
address at the Mail-Archive.
@@ -137,23 +137,23 @@ Additionally, this archiver can handle malformed Message-IDs.
>>> mlist.archive_private = False
>>> del msg['message-id']
>>> msg['Message-ID'] = '12345>'
- >>> archiver.permalink(mlist, msg)
- 'http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8='
+ >>> print archiver.permalink(mlist, msg)
+ http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8=
>>> del msg['message-id']
>>> msg['Message-ID'] = '<12345'
- >>> archiver.permalink(mlist, msg)
- 'http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY='
+ >>> print archiver.permalink(mlist, msg)
+ http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY=
>>> del msg['message-id']
>>> msg['Message-ID'] = '12345'
- >>> archiver.permalink(mlist, msg)
- 'http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc='
+ >>> print archiver.permalink(mlist, msg)
+ http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
>>> del msg['message-id']
>>> msg['Message-ID'] = ' 12345 '
- >>> archiver.permalink(mlist, msg)
- 'http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc='
+ >>> print archiver.permalink(mlist, msg)
+ http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
MHonArc
@@ -162,8 +162,8 @@ MHonArc
The MHonArc archiver <http://www.mhonarc.org> is also available.
>>> archiver = archivers['mhonarc']
- >>> archiver.name
- 'mhonarc'
+ >>> print archiver.name
+ mhonarc
Messages sent to a local MHonArc instance are added to its archive via a
subprocess call.
diff --git a/mailman/docs/chains.txt b/mailman/docs/chains.txt
index e9c5d74e4..b6e75e6e1 100644
--- a/mailman/docs/chains.txt
+++ b/mailman/docs/chains.txt
@@ -19,10 +19,10 @@ The Discard chain simply throws the message away.
>>> chain = config.chains['discard']
>>> verifyObject(IChain, chain)
True
- >>> chain.name
- 'discard'
- >>> chain.description
- u'Discard a message and stop processing.'
+ >>> print chain.name
+ discard
+ >>> print chain.description
+ Discard a message and stop processing.
>>> from mailman.app.lifecycle import create_list
>>> mlist = create_list(u'_xtest@example.com')
@@ -58,10 +58,10 @@ this action.
>>> chain = config.chains['reject']
>>> verifyObject(IChain, chain)
True
- >>> chain.name
- 'reject'
- >>> chain.description
- u'Reject/bounce a message and stop processing.'
+ >>> print chain.name
+ reject
+ >>> print chain.description
+ Reject/bounce a message and stop processing.
>>> file_pos = fp.tell()
>>> process(mlist, msg, {}, 'reject')
>>> fp.seek(file_pos)
@@ -104,10 +104,10 @@ sender and the list moderators.
>>> chain = config.chains['hold']
>>> verifyObject(IChain, chain)
True
- >>> chain.name
- 'hold'
- >>> chain.description
- u'Hold a message and stop processing.'
+ >>> print chain.name
+ hold
+ >>> print chain.description
+ Hold a message and stop processing.
>>> file_pos = fp.tell()
>>> process(mlist, msg, {}, 'hold')
@@ -231,7 +231,7 @@ first item is a type code and the second item is a message id.
[(u'id', ...), (u'type', u'held message')]
The message itself is held in the message store.
-
+
>>> rkey, rdata = config.db.requests.get_list_requests(mlist).get_request(
... data['id'])
>>> msg = config.db.message_store.get_message_by_id(
@@ -256,10 +256,10 @@ processed and sent on to the list membership.
>>> chain = config.chains['accept']
>>> verifyObject(IChain, chain)
True
- >>> chain.name
- 'accept'
- >>> chain.description
- u'Accept a message.'
+ >>> print chain.name
+ accept
+ >>> print chain.description
+ Accept a message.
>>> file_pos = fp.tell()
>>> process(mlist, msg, {}, 'accept')
>>> fp.seek(file_pos)
@@ -297,10 +297,10 @@ the Hold handler from previous versions of Mailman.
>>> chain = config.chains['built-in']
>>> verifyObject(IChain, chain)
True
- >>> chain.name
- 'built-in'
- >>> chain.description
- u'The built-in moderation chain.'
+ >>> print chain.name
+ built-in
+ >>> print chain.description
+ The built-in moderation chain.
The previously created message is innocuous enough that it should pass through
all default rules. This message will end up in the pipeline queue.
@@ -320,7 +320,7 @@ all default rules. This message will end up in the pipeline queue.
X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
X-Mailman-Rule-Misses: approved; emergency; loop; administrivia;
implicit-dest;
- max-recipients; max-size; news-moderation; no-subject;
+ max-recipients; max-size; news-moderation; no-subject;
suspicious-header
<BLANKLINE>
An important message.
@@ -331,7 +331,15 @@ hit and all rules that have missed.
>>> sorted(qdata['rule_hits'])
[]
- >>> sorted(qdata['rule_misses'])
- ['administrivia', 'approved', 'emergency', 'implicit-dest', 'loop',
- 'max-recipients', 'max-size', 'news-moderation', 'no-subject',
- 'suspicious-header']
+ >>> for rule_name in sorted(qdata['rule_misses']):
+ ... print rule_name
+ administrivia
+ approved
+ emergency
+ implicit-dest
+ loop
+ max-recipients
+ max-size
+ news-moderation
+ no-subject
+ suspicious-header
diff --git a/mailman/docs/pipelines.txt b/mailman/docs/pipelines.txt
index 205d210b1..0e6dad8e8 100644
--- a/mailman/docs/pipelines.txt
+++ b/mailman/docs/pipelines.txt
@@ -62,11 +62,11 @@ etc.
And the message metadata has information about recipients and other stuff.
However there are currently no recipients for this message.
- >>> sorted(msgdata.items())
- [('original_sender', u'aperson@example.com'),
- ('origsubj', u'My first post'),
- ('recips', set([])),
- ('stripped_subject', <email.header.Header instance at ...>)]
+ >>> dump_msgdata(msgdata)
+ original_sender : aperson@example.com
+ origsubj : My first post
+ recips : set([])
+ stripped_subject: My first post
And the message is now sitting in various other processing queues.
@@ -98,12 +98,13 @@ And the message is now sitting in various other processing queues.
<BLANKLINE>
First post!
<BLANKLINE>
- >>> print sorted(messages[0].msgdata.items())
- [('_parsemsg', False), ('original_sender', u'aperson@example.com'),
- ('origsubj', u'My first post'),
- ('received_time', ...), ('recips', set([])),
- ('stripped_subject', <email.header.Header instance at ...>),
- ('version', 3)]
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ original_sender : aperson@example.com
+ origsubj : My first post
+ recips : set([])
+ stripped_subject: My first post
+ version : 3
This mailing list is not linked to an NNTP newsgroup, so there's nothing in
the outgoing nntp queue.
@@ -141,13 +142,14 @@ This is the message that will actually get delivered to end recipients.
<BLANKLINE>
First post!
<BLANKLINE>
- >>> print sorted(messages[0].msgdata.items())
- [('_parsemsg', False), ('listname', u'xtest@example.com'),
- ('original_sender', u'aperson@example.com'),
- ('origsubj', u'My first post'), ('received_time', ...),
- ('recips', set([])),
- ('stripped_subject', <email.header.Header instance at ...>),
- ('version', 3)]
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : xtest@example.com
+ original_sender : aperson@example.com
+ origsubj : My first post
+ recips : set([])
+ stripped_subject: My first post
+ version : 3
There's now one message in the digest mailbox, getting ready to be sent.
diff --git a/mailman/docs/registration.txt b/mailman/docs/registration.txt
index 2e04f7ddf..d243188bc 100644
--- a/mailman/docs/registration.txt
+++ b/mailman/docs/registration.txt
@@ -158,13 +158,12 @@ message is sent to the user in order to verify the registered address.
<BLANKLINE>
postmaster@mail.example.com
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False),
- ('nodecorate', True),
- ('received_time', ...),
- ('recips', [u'aperson@example.com']),
- ('reduced_list_headers', True),
- ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ nodecorate : True
+ recips : [u'aperson@example.com']
+ reduced_list_headers: True
+ version : 3
The confirmation token shows up in several places, each of which provides an
easy way for the user to complete the confirmation. The token will always
diff --git a/mailman/docs/requests.txt b/mailman/docs/requests.txt
index aa6f031bd..87b835fb8 100644
--- a/mailman/docs/requests.txt
+++ b/mailman/docs/requests.txt
@@ -10,9 +10,10 @@ Here is a helper function for printing out held requests.
>>> def show_holds(requests):
... for request in requests.held_requests:
... key, data = requests.get_request(request.id)
+ ... print request.id, str(request.request_type), key
... if data is not None:
- ... data = sorted(data.items())
- ... print request.id, str(request.request_type), key, data
+ ... for key in sorted(data):
+ ... print ' {0}: {1}'.format(key, data[key])
And another helper for displaying messages in the virgin queue.
@@ -80,10 +81,10 @@ And of course, now we can see that there are four requests being held.
>>> requests.count_of(RequestType.unsubscription)
1
>>> show_holds(requests)
- 1 RequestType.held_message hold_1 None
- 2 RequestType.subscription hold_2 None
- 3 RequestType.unsubscription hold_3 None
- 4 RequestType.held_message hold_4 None
+ 1 RequestType.held_message hold_1
+ 2 RequestType.subscription hold_2
+ 3 RequestType.unsubscription hold_3
+ 4 RequestType.held_message hold_4
If we try to hold a request with a bogus type, we get an exception.
@@ -101,11 +102,13 @@ We can hold requests with additional data.
>>> requests.count
5
>>> show_holds(requests)
- 1 RequestType.held_message hold_1 None
- 2 RequestType.subscription hold_2 None
- 3 RequestType.unsubscription hold_3 None
- 4 RequestType.held_message hold_4 None
- 5 RequestType.held_message hold_5 [(u'bar', u'no'), (u'foo', u'yes')]
+ 1 RequestType.held_message hold_1
+ 2 RequestType.subscription hold_2
+ 3 RequestType.unsubscription hold_3
+ 4 RequestType.held_message hold_4
+ 5 RequestType.held_message hold_5
+ bar: no
+ foo: yes
Getting requests
@@ -116,8 +119,8 @@ of the request data we want. This returns a 2-tuple of the key and data we
originally held.
>>> key, data = requests.get_request(2)
- >>> key
- u'hold_2'
+ >>> print key
+ hold_2
Because we did not store additional data with request 2, it comes back as None
now.
@@ -128,10 +131,11 @@ now.
However, if we ask for a request that had data, we'd get it back now.
>>> key, data = requests.get_request(5)
- >>> key
- u'hold_5'
- >>> sorted(data.items())
- [(u'bar', u'no'), (u'foo', u'yes')]
+ >>> print key
+ hold_5
+ >>> dump_msgdata(data)
+ bar: no
+ foo: yes
If we ask for a request that is not in the database, we get None back.
@@ -150,12 +154,15 @@ over by type.
>>> for request in requests.of_type(RequestType.held_message):
... assert request.request_type is RequestType.held_message
... key, data = requests.get_request(request.id)
+ ... print request.id, key
... if data is not None:
- ... data = sorted(data.items())
- ... print request.id, key, data
- 1 hold_1 None
- 4 hold_4 None
- 5 hold_5 [(u'bar', u'no'), (u'foo', u'yes')]
+ ... for key in sorted(data):
+ ... print ' {0}: {1}'.format(key, data[key])
+ 1 hold_1
+ 4 hold_4
+ 5 hold_5
+ bar: no
+ foo: yes
Deleting requests
@@ -168,10 +175,12 @@ database.
>>> requests.count
4
>>> show_holds(requests)
- 1 RequestType.held_message hold_1 None
- 3 RequestType.unsubscription hold_3 None
- 4 RequestType.held_message hold_4 None
- 5 RequestType.held_message hold_5 [(u'bar', u'no'), (u'foo', u'yes')]
+ 1 RequestType.held_message hold_1
+ 3 RequestType.unsubscription hold_3
+ 4 RequestType.held_message hold_4
+ 5 RequestType.held_message hold_5
+ bar: no
+ foo: yes
>>> print requests.get_request(2)
None
@@ -286,14 +295,13 @@ The message can be rejected, meaning it is bounced back to the sender.
<BLANKLINE>
alist-owner@example.com
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False),
- ('listname', u'alist@example.com'),
- ('nodecorate', True),
- ('received_time', ...),
- ('recips', [u'aperson@example.org']),
- ('reduced_list_headers', True),
- ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'aperson@example.org']
+ reduced_list_headers: True
+ version : 3
Or the message can be approved. This actually places the message back into
the incoming queue for further processing, however the message metadata
@@ -313,11 +321,12 @@ indicates that the message has been approved.
<BLANKLINE>
Here's something important about our mailing list.
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False),
- (u'approved', True), ('moderator_approved', True),
- (u'received_time', 123.45), (u'sender', u'aperson@example.com'),
- ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ approved : True
+ moderator_approved: True
+ sender : aperson@example.com
+ version : 3
In addition to any of the above dispositions, the message can also be
preserved for further study. Ordinarily the message is removed from the
@@ -386,11 +395,13 @@ moderators.
<BLANKLINE>
Here's something important about our mailing list.
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False), ('listname', u'alist@example.com'),
- ('nodecorate', True), ('received_time', ...),
- ('recips', [u'zperson@example.com']),
- ('reduced_list_headers', True), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'zperson@example.com']
+ reduced_list_headers: True
+ version : 3
Holding subscription requests
@@ -454,13 +465,14 @@ queue when the message is held.
<BLANKLINE>
to process the request.
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False),
- ('listname', u'alist@example.com'),
- ('nodecorate', True),
- ('received_time', ...),
- ('recips', [u'alist-owner@example.com']),
- ('reduced_list_headers', True), ('tomoderators', True), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'alist-owner@example.com']
+ reduced_list_headers: True
+ tomoderators : True
+ version : 3
Once held, the moderator can select one of several dispositions. The most
trivial is to simply defer a decision for now.
@@ -508,12 +520,13 @@ subscriber.
<BLANKLINE>
alist-owner@example.com
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False), ('listname', u'alist@example.com'),
- ('nodecorate', True),
- ('received_time', ...),
- ('recips', [u'cperson@example.org']),
- ('reduced_list_headers', True), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'cperson@example.org']
+ reduced_list_headers: True
+ version : 3
The subscription can also be accepted. This subscribes the address to the
mailing list.
@@ -551,11 +564,14 @@ subscription and the fact that they may need to approve it.
<BLANKLINE>
to process the request.
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False), ('listname', u'alist@example.com'),
- ('nodecorate', True), ('received_time', ...),
- ('recips', [u'alist-owner@example.com']),
- ('reduced_list_headers', True), ('tomoderators', True), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'alist-owner@example.com']
+ reduced_list_headers: True
+ tomoderators : True
+ version : 3
Accept the subscription request.
@@ -621,11 +637,14 @@ The welcome message is sent to the person who just subscribed.
this email is not included here. There is also a button on your
options page that will send your current password to you.
<BLANKLINE>
- >>> sorted(welcome_qdata.items())
- [('_parsemsg', False), ('listname', u'alist@example.com'),
- ('nodecorate', True), ('received_time', ...),
- ('recips', [u'fperson@example.org']),
- ('reduced_list_headers', True), ('verp', False), ('version', 3)]
+ >>> dump_msgdata(welcome_qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'fperson@example.org']
+ reduced_list_headers: True
+ verp : False
+ version : 3
The admin message is sent to the moderators.
@@ -643,11 +662,14 @@ The admin message is sent to the moderators.
Frank Person <fperson@example.org> has been successfully subscribed to
A Test List.
<BLANKLINE>
- >>> sorted(admin_qdata.items())
- [('_parsemsg', False), ('envsender', 'changeme@example.com'),
- ('listname', u'alist@example.com'),
- ('nodecorate', True), ('received_time', ...),
- ('recips', []), ('reduced_list_headers', True), ('version', 3)]
+ >>> dump_msgdata(admin_qdata)
+ _parsemsg : False
+ envsender : changeme@example.com
+ listname : alist@example.com
+ nodecorate : True
+ recips : []
+ reduced_list_headers: True
+ version : 3
Frank Person is now a member of the mailing list.
@@ -714,12 +736,14 @@ unsubscription holds can send the list's moderators an immediate notification.
<BLANKLINE>
to process the request.
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False),
- ('listname', u'alist@example.com'), ('nodecorate', True),
- ('received_time', ...),
- ('recips', [u'alist-owner@example.com']),
- ('reduced_list_headers', True), ('tomoderators', True), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'alist-owner@example.com']
+ reduced_list_headers: True
+ tomoderators : True
+ version : 3
There are now two addresses with held unsubscription requests. As above, one
of the actions we can take is to defer to the decision.
@@ -770,12 +794,14 @@ and the person remains a member of the mailing list.
<BLANKLINE>
alist-owner@example.com
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False),
- ('listname', u'alist@example.com'),
- ('nodecorate', True), ('received_time', ...),
- ('recips', [u'hperson@example.com']),
- ('reduced_list_headers', True), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'hperson@example.com']
+ reduced_list_headers: True
+ version : 3
+
>>> mlist.members.get_member(u'hperson@example.com')
<Member: hperson@example.com on alist@example.com as MemberRole.member>
@@ -823,12 +849,14 @@ The goodbye message...
<BLANKLINE>
So long!
<BLANKLINE>
- >>> sorted(goodbye_qdata.items())
- [('_parsemsg', False),
- ('listname', u'alist@example.com'),
- ('nodecorate', True), ('received_time', ...),
- ('recips', [u'gperson@example.com']),
- ('reduced_list_headers', True), ('verp', False), ('version', 3)]
+ >>> dump_msgdata(goodbye_qdata)
+ _parsemsg : False
+ listname : alist@example.com
+ nodecorate : True
+ recips : [u'gperson@example.com']
+ reduced_list_headers: True
+ verp : False
+ version : 3
...and the admin message.
@@ -845,8 +873,11 @@ The goodbye message...
<BLANKLINE>
gperson@example.com has been removed from A Test List.
<BLANKLINE>
- >>> sorted(admin_qdata.items())
- [('_parsemsg', False), ('envsender', 'changeme@example.com'),
- ('listname', u'alist@example.com'),
- ('nodecorate', True), ('received_time', ...),
- ('recips', []), ('reduced_list_headers', True), ('version', 3)]
+ >>> dump_msgdata(admin_qdata)
+ _parsemsg : False
+ envsender : changeme@example.com
+ listname : alist@example.com
+ nodecorate : True
+ recips : []
+ reduced_list_headers: True
+ version : 3
diff --git a/mailman/domain.py b/mailman/domain.py
index 03c1ad39c..4a7bef755 100644
--- a/mailman/domain.py
+++ b/mailman/domain.py
@@ -17,6 +17,8 @@
"""Domains."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'Domain',
@@ -50,8 +52,9 @@ class Domain:
domain. If not given, postmaster@`email_host` will be used.
"""
self.email_host = email_host
- self.base_url = (base_url if base_url is not None else
- 'http://' + email_host)
+ self.base_url = (base_url
+ if base_url is not None
+ else 'http://' + email_host)
self.description = description
self.contact_address = (contact_address
if contact_address is not None
@@ -60,7 +63,7 @@ class Domain:
def confirm_address(self, token=''):
"""See `IDomain`."""
- return 'confirm-%s@%s' % (token, self.email_host)
+ return 'confirm-{0}@{1}'.format(token, self.email_host)
def confirm_url(self, token=''):
"""See `IDomain`."""
diff --git a/mailman/i18n.py b/mailman/i18n.py
index 278199535..d5e54ded2 100644
--- a/mailman/i18n.py
+++ b/mailman/i18n.py
@@ -17,6 +17,8 @@
"""Internationalization support."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'_',
@@ -34,6 +36,7 @@ import string
import gettext
import mailman.messages
+from mailman.utilities.string import expand
_translation = None
_missing = object()
@@ -103,8 +106,8 @@ if _translation is None:
def _(s):
if s == '':
- return u''
- assert s, 'Cannot translate: %s' % s
+ return ''
+ assert s, 'Cannot translate: {0}'.format(s)
# Do translation of the given string into the current language, and do PEP
# 292 style $-string interpolation into the resulting string.
#
@@ -120,8 +123,8 @@ def _(s):
frame = sys._getframe(1)
# A `safe' dictionary is used so we won't get an exception if there's a
# missing key in the dictionary.
- d = frame.f_globals.copy()
- d.update(frame.f_locals)
+ raw_dict = frame.f_globals.copy()
+ raw_dict.update(frame.f_locals)
# Mailman must be unicode safe internally (i.e. all strings inside Mailman
# must be unicodes). The translation service is one boundary to the
# outside world, so to honor this constraint, make sure that all strings
@@ -129,10 +132,9 @@ def _(s):
# dictionary values are 8-bit strings.
tns = _translation.ugettext(s)
charset = _translation.charset() or 'us-ascii'
- for k, v in d.items():
- if isinstance(v, str):
- d[k] = unicode(v, charset, 'replace')
- translated_string = Template(tns).safe_substitute(attrdict(d))
+ # Python requires ** dictionaries to have str, not unicode keys. For our
+ # purposes, keys should always be ascii. Values though should be unicode.
+ translated_string = expand(tns, attrdict(raw_dict), Template)
if isinstance(translated_string, str):
translated_string = unicode(translated_string, charset)
return translated_string
@@ -191,5 +193,4 @@ def ctime(date):
wday = daysofweek[wday]
mon = months[mon]
- return _('%(wday)s %(mon)s %(day)2i %(hh)02i:%(mm)02i:%(ss)02i '
- '%(tzname)s %(year)04i')
+ return _('$wday $mon $day $hh:$mm:$ss $tzname $year')
diff --git a/mailman/inject.py b/mailman/inject.py
index 27edf827f..079236932 100644
--- a/mailman/inject.py
+++ b/mailman/inject.py
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'inject_message',
diff --git a/mailman/interact.py b/mailman/interact.py
index e8ff09b16..a30f22cee 100644
--- a/mailman/interact.py
+++ b/mailman/interact.py
@@ -17,6 +17,13 @@
"""Provide an interactive prompt, mimicking the Python interpreter."""
+from __future__ import unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'interact',
+ ]
+
import os
import sys
import code
diff --git a/mailman/interfaces/address.py b/mailman/interfaces/address.py
index 802952774..968ded3ee 100644
--- a/mailman/interfaces/address.py
+++ b/mailman/interfaces/address.py
@@ -17,8 +17,19 @@
"""Interface for email address related information."""
-from zope.interface import Interface, Attribute
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'AddressAlreadyLinkedError',
+ 'AddressError',
+ 'AddressNotLinkedError',
+ 'ExistingAddressError',
+ 'IAddress',
+ ]
+
+from zope.interface import Interface, Attribute
from mailman.interfaces.errors import MailmanError
diff --git a/mailman/interfaces/archiver.py b/mailman/interfaces/archiver.py
index c69b13427..35fee2c9e 100644
--- a/mailman/interfaces/archiver.py
+++ b/mailman/interfaces/archiver.py
@@ -17,12 +17,15 @@
"""Interface for archiving schemes."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'IArchiver',
'IPipermailMailingList',
]
+
from zope.interface import Interface, Attribute
from mailman.interfaces.mailinglist import IMailingList
diff --git a/mailman/interfaces/chain.py b/mailman/interfaces/chain.py
index 2a18d3dea..2685b7980 100644
--- a/mailman/interfaces/chain.py
+++ b/mailman/interfaces/chain.py
@@ -17,6 +17,18 @@
"""Interfaces describing the basics of chains and links."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IChain',
+ 'IChainIterator',
+ 'IChainLink',
+ 'IMutableChain',
+ 'LinkAction',
+ ]
+
+
from munepy import Enum
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/command.py b/mailman/interfaces/command.py
index 202d2a148..a9ad292f4 100644
--- a/mailman/interfaces/command.py
+++ b/mailman/interfaces/command.py
@@ -17,6 +17,16 @@
"""Interfaces defining email commands."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ContinueProcessing',
+ 'IEmailCommand',
+ 'IEmailResults',
+ ]
+
+
from munepy import Enum
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/database.py b/mailman/interfaces/database.py
index 27ab4bf83..4a9d6cde5 100644
--- a/mailman/interfaces/database.py
+++ b/mailman/interfaces/database.py
@@ -23,6 +23,8 @@ setup.py file as an entry point in the 'mailman.database' group with the name
Mailman's back end.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'DatabaseError',
@@ -49,9 +51,9 @@ class SchemaVersionMismatchError(DatabaseError):
self._got = got
def __str__(self):
- return (
- 'Incompatible database schema version (got: %d, expected: %d)'
- % (self._got, DATABASE_SCHEMA_VERSION))
+ return ('Incompatible database schema version '
+ '(got: {0}, expected: {1})'.format(
+ self._got, DATABASE_SCHEMA_VERSION))
diff --git a/mailman/interfaces/domain.py b/mailman/interfaces/domain.py
index 9ce402a70..8c2a27e79 100644
--- a/mailman/interfaces/domain.py
+++ b/mailman/interfaces/domain.py
@@ -17,6 +17,14 @@
"""Interface representing domains."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IDomain',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/errors.py b/mailman/interfaces/errors.py
index b174aa3cf..608193f7a 100644
--- a/mailman/interfaces/errors.py
+++ b/mailman/interfaces/errors.py
@@ -22,6 +22,13 @@ components. More specific exceptions will be located in the relevant
interfaces.
"""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'MailmanError',
+ ]
+
class MailmanError(Exception):
diff --git a/mailman/interfaces/handler.py b/mailman/interfaces/handler.py
index 9d03b7263..41d791d5d 100644
--- a/mailman/interfaces/handler.py
+++ b/mailman/interfaces/handler.py
@@ -17,6 +17,14 @@
"""Interface describing a pipeline handler."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IHandler',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/languages.py b/mailman/interfaces/languages.py
index d1cdf9f43..272cf4372 100644
--- a/mailman/interfaces/languages.py
+++ b/mailman/interfaces/languages.py
@@ -17,6 +17,15 @@
"""Interfaces for managing languages."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ILanguage',
+ 'ILanguageManager',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/listmanager.py b/mailman/interfaces/listmanager.py
index 6a774a04e..e7cdd9da7 100644
--- a/mailman/interfaces/listmanager.py
+++ b/mailman/interfaces/listmanager.py
@@ -17,6 +17,8 @@
"""Interface for list storage, deleting, and finding."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'IListManager',
diff --git a/mailman/interfaces/mailinglist.py b/mailman/interfaces/mailinglist.py
index 970c1ac07..cd7b11d64 100644
--- a/mailman/interfaces/mailinglist.py
+++ b/mailman/interfaces/mailinglist.py
@@ -17,12 +17,16 @@
"""Interface for a mailing list."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
__all__ = [
'IMailingList',
'Personalization',
'ReplyToMunging',
]
+
from munepy import Enum
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/member.py b/mailman/interfaces/member.py
index 697117219..2e966879d 100644
--- a/mailman/interfaces/member.py
+++ b/mailman/interfaces/member.py
@@ -17,6 +17,7 @@
"""Interface describing the basics of a member."""
+from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
@@ -78,7 +79,7 @@ class AlreadySubscribedError(SubscriptionError):
self._role = role
def __str__(self):
- return '%s is already a %s of mailing list %s' % (
+ return '{0} is already a {1} of mailing list {2}'.format(
self._address, self._role, self._fqdn_listname)
diff --git a/mailman/interfaces/messages.py b/mailman/interfaces/messages.py
index 0da595d0b..72efe960f 100644
--- a/mailman/interfaces/messages.py
+++ b/mailman/interfaces/messages.py
@@ -17,6 +17,15 @@
"""The message storage service."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IMessage',
+ 'IMessageStore',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/mlistrequest.py b/mailman/interfaces/mlistrequest.py
index f348d617c..924421406 100644
--- a/mailman/interfaces/mlistrequest.py
+++ b/mailman/interfaces/mlistrequest.py
@@ -17,6 +17,14 @@
"""Interface for a web request accessing a mailing list."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IMailingListRequest',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/mta.py b/mailman/interfaces/mta.py
index 5e647cfb2..a8c794750 100644
--- a/mailman/interfaces/mta.py
+++ b/mailman/interfaces/mta.py
@@ -17,6 +17,8 @@
"""Interface for mail transport agent integration."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'IMailTransportAgent',
diff --git a/mailman/interfaces/pending.py b/mailman/interfaces/pending.py
index 7fcde07ad..0e43278e0 100644
--- a/mailman/interfaces/pending.py
+++ b/mailman/interfaces/pending.py
@@ -22,6 +22,17 @@ maps these events to a unique hash that can be used as a token for end user
confirmation.
"""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IPendable',
+ 'IPended',
+ 'IPendedKeyValue',
+ 'IPendings',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/permissions.py b/mailman/interfaces/permissions.py
index fb36f35b5..7613064f5 100644
--- a/mailman/interfaces/permissions.py
+++ b/mailman/interfaces/permissions.py
@@ -17,6 +17,14 @@
"""Interfaces for various permissions."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IPostingPermission',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/pipeline.py b/mailman/interfaces/pipeline.py
index a95fb7628..0268693b7 100644
--- a/mailman/interfaces/pipeline.py
+++ b/mailman/interfaces/pipeline.py
@@ -17,6 +17,14 @@
"""Interface for describing pipelines."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IPipeline',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/preferences.py b/mailman/interfaces/preferences.py
index fbd81c801..9aeed8496 100644
--- a/mailman/interfaces/preferences.py
+++ b/mailman/interfaces/preferences.py
@@ -17,6 +17,14 @@
"""Interface for preferences."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IPreferences',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/registrar.py b/mailman/interfaces/registrar.py
index d6c174d85..d90966e2d 100644
--- a/mailman/interfaces/registrar.py
+++ b/mailman/interfaces/registrar.py
@@ -22,6 +22,8 @@ etc. than the IUserManager. The latter does no validation, syntax checking,
or confirmation, while this interface does.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'IRegistrar',
diff --git a/mailman/interfaces/requests.py b/mailman/interfaces/requests.py
index b9c0802f4..e3e316fa0 100644
--- a/mailman/interfaces/requests.py
+++ b/mailman/interfaces/requests.py
@@ -21,6 +21,16 @@ The request database handles events that must be approved by the list
moderators, such as subscription requests and held messages.
"""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IListRequests',
+ 'IRequests',
+ 'RequestType',
+ ]
+
+
from munepy import Enum
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/roster.py b/mailman/interfaces/roster.py
index f7b2125fd..9fb648be1 100644
--- a/mailman/interfaces/roster.py
+++ b/mailman/interfaces/roster.py
@@ -17,6 +17,14 @@
"""Interface for a roster of members."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IRoster',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/rules.py b/mailman/interfaces/rules.py
index 24501848f..632cc85de 100644
--- a/mailman/interfaces/rules.py
+++ b/mailman/interfaces/rules.py
@@ -17,6 +17,14 @@
"""Interface describing the basics of rules."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IRule',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/runner.py b/mailman/interfaces/runner.py
index 500efd541..e923e099b 100644
--- a/mailman/interfaces/runner.py
+++ b/mailman/interfaces/runner.py
@@ -17,6 +17,14 @@
"""Interface for queue runners."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IRunner',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/styles.py b/mailman/interfaces/styles.py
index 7cc0e50e8..e70050f02 100644
--- a/mailman/interfaces/styles.py
+++ b/mailman/interfaces/styles.py
@@ -17,6 +17,8 @@
"""Interfaces for list styles."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'DuplicateStyleError',
diff --git a/mailman/interfaces/switchboard.py b/mailman/interfaces/switchboard.py
index 96cf1aea2..866093d12 100644
--- a/mailman/interfaces/switchboard.py
+++ b/mailman/interfaces/switchboard.py
@@ -17,6 +17,14 @@
"""Interface for switchboards."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ISwitchboard',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/user.py b/mailman/interfaces/user.py
index 8824a2d7c..5c3ff58cd 100644
--- a/mailman/interfaces/user.py
+++ b/mailman/interfaces/user.py
@@ -17,6 +17,14 @@
"""Interface describing the basics of a user."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IUser',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/interfaces/usermanager.py b/mailman/interfaces/usermanager.py
index 7454faea8..41bec49ba 100644
--- a/mailman/interfaces/usermanager.py
+++ b/mailman/interfaces/usermanager.py
@@ -17,6 +17,14 @@
"""Interface describing a user manager service."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IUserManager',
+ ]
+
+
from zope.interface import Interface, Attribute
diff --git a/mailman/languages.py b/mailman/languages.py
index 27e2353de..7e093bc62 100644
--- a/mailman/languages.py
+++ b/mailman/languages.py
@@ -17,6 +17,13 @@
"""Language manager."""
+from __future__ import unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'LanguageManager',
+ ]
+
from zope.interface import implements
from mailman.interfaces.languages import ILanguageManager
diff --git a/mailman/mta/null.py b/mailman/mta/null.py
index 058ecb548..07cb7a869 100644
--- a/mailman/mta/null.py
+++ b/mailman/mta/null.py
@@ -20,17 +20,19 @@
Exim one example of an MTA that Just Works.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
- 'LMTP',
+ 'NullMTA',
]
from zope.interface import implements
-
from mailman.interfaces.mta import IMailTransportAgent
+
class NullMTA:
"""Null MTA that just satisfies the interface."""
diff --git a/mailman/mta/postfix.py b/mailman/mta/postfix.py
index da265fecf..4cb172d99 100644
--- a/mailman/mta/postfix.py
+++ b/mailman/mta/postfix.py
@@ -17,6 +17,8 @@
"""Creation/deletion hooks for the Postfix MTA."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'LMTP',
diff --git a/mailman/mta/smtp_direct.py b/mailman/mta/smtp_direct.py
index b82fb9227..717b3eb90 100644
--- a/mailman/mta/smtp_direct.py
+++ b/mailman/mta/smtp_direct.py
@@ -26,6 +26,8 @@ Note: This file only handles single threaded delivery. See SMTPThreaded.py
for a threaded implementation.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'SMTPDirect',
@@ -41,7 +43,6 @@ import smtplib
from email.Charset import Charset
from email.Header import Header
from email.Utils import formataddr
-from string import Template
from zope.interface import implements
from mailman import Utils
@@ -50,6 +51,7 @@ from mailman.core import errors
from mailman.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.mailinglist import Personalization
+from mailman.utilities.string import expand
DOT = '.'
@@ -61,43 +63,43 @@ log = logging.getLogger('mailman.smtp')
# Manage a connection to the SMTP server
class Connection:
def __init__(self):
- self.__conn = None
+ self._conn = None
- def __connect(self):
- self.__conn = smtplib.SMTP()
+ def _connect(self):
+ self._conn = smtplib.SMTP()
host = config.mta.smtp_host
port = int(config.mta.smtp_port)
log.debug('Connecting to %s:%s', host, port)
- self.__conn.connect(host, port)
- self.__numsessions = int(config.mta.max_sessions_per_connection)
+ self._conn.connect(host, port)
+ self._numsessions = int(config.mta.max_sessions_per_connection)
def sendmail(self, envsender, recips, msgtext):
- if self.__conn is None:
- self.__connect()
+ if self._conn is None:
+ self._connect()
try:
- results = self.__conn.sendmail(envsender, recips, msgtext)
+ results = self._conn.sendmail(envsender, recips, msgtext)
except smtplib.SMTPException:
# For safety, close this connection. The next send attempt will
# automatically re-open it. Pass the exception on up.
self.quit()
raise
# This session has been successfully completed.
- self.__numsessions -= 1
+ self._numsessions -= 1
# By testing exactly for equality to 0, we automatically handle the
# case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close
# the connection. We won't worry about wraparound <wink>.
- if self.__numsessions == 0:
+ if self._numsessions == 0:
self.quit()
return results
def quit(self):
- if self.__conn is None:
+ if self._conn is None:
return
try:
- self.__conn.quit()
+ self._conn.quit()
except smtplib.SMTPException:
pass
- self.__conn = None
+ self._conn = None
@@ -123,7 +125,7 @@ def process(mlist, msg, msgdata):
if (not msgdata.has_key('personalize') or msgdata['personalize']) and (
msgdata.get('verp') or mlist.personalize <> Personalization.none):
chunks = [[recip] for recip in recips]
- msgdata['personalize'] = 1
+ msgdata['personalize'] = True
deliveryfunc = verpdeliver
elif int(config.mta.max_recipients) <= 0:
chunks = [recips]
@@ -192,13 +194,11 @@ def process(mlist, msg, msgdata):
# Log this message.
template = config.logging.smtp.every
if template != 'no':
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
+ log.info('%s', expand(template, substitutions))
if refused:
template = config.logging.smtp.refused
if template != 'no':
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
+ log.info('%s', expand(template, substitutions))
else:
# Log the successful post, but if it was not destined to the mailing
# list (e.g. to the owner or admin), print the actual recipients
@@ -209,8 +209,7 @@ def process(mlist, msg, msgdata):
substitutions['recips'] = COMMA.join(recips)
template = config.logging.smtp.success
if template != 'no':
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
+ log.info('%s', expand(template, substitutions))
# Process any failed deliveries.
tempfailures = []
permfailures = []
@@ -238,8 +237,7 @@ def process(mlist, msg, msgdata):
smtpcode = code,
smtpmsg = smtpmsg,
)
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
+ log.info('%s', expand(template, substitutions))
# Return the results
if tempfailures or permfailures:
raise errors.SomeRecipientsFailed(tempfailures, permfailures)
@@ -315,9 +313,9 @@ def verpdeliver(mlist, msg, msgdata, envsender, failures, conn):
# this recipient.
log.info('Skipping VERP delivery to unqual recip: %s', recip)
continue
- envsender = Template(config.mta.verp_format).safe_substitute(
+ envsender = expand(config.mta.verp_format, dict(
bounces=bmailbox, mailbox=rmailbox,
- host=DOT.join(rdomain)) + '@' + DOT.join(bdomain)
+ host=DOT.join(rdomain))) + '@' + DOT.join(bdomain)
if mlist.personalize == Personalization.full:
# When fully personalizing, we want the To address to point to the
# recipient, not to the mailing list
@@ -378,28 +376,28 @@ def bulkdeliver(mlist, msg, msgdata, envsender, failures, conn):
try:
# Send the message
refused = conn.sendmail(envsender, recips, msgtext)
- except smtplib.SMTPRecipientsRefused, e:
- log.error('%s recipients refused: %s', msgid, e)
- refused = e.recipients
- except smtplib.SMTPResponseException, e:
+ except smtplib.SMTPRecipientsRefused as error:
+ log.error('%s recipients refused: %s', msgid, error)
+ refused = error.recipients
+ except smtplib.SMTPResponseException as error:
log.error('%s SMTP session failure: %s, %s',
- msgid, e.smtp_code, e.smtp_error)
+ msgid, error.smtp_code, error.smtp_error)
# If this was a permanent failure, don't add the recipients to the
# refused, because we don't want them to be added to failures.
# Otherwise, if the MTA rejects the message because of the message
# content (e.g. it's spam, virii, or has syntactic problems), then
# this will end up registering a bounce score for every recipient.
# Definitely /not/ what we want.
- if e.smtp_code < 500 or e.smtp_code == 552:
+ if error.smtp_code < 500 or error.smtp_code == 552:
# It's a temporary failure
for r in recips:
- refused[r] = (e.smtp_code, e.smtp_error)
- except (socket.error, IOError, smtplib.SMTPException), e:
+ refused[r] = (error.smtp_code, error.smtp_error)
+ except (socket.error, IOError, smtplib.SMTPException) as error:
# MTA not responding, or other socket problems, or any other kind of
# SMTPException. In that case, nothing got delivered, so treat this
# as a temporary failure.
- log.error('%s low level smtp error: %s', msgid, e)
- error = str(e)
+ log.error('%s low level smtp error: %s', msgid, error)
+ error = str(error)
for r in recips:
refused[r] = (-1, error)
failures.update(refused)
diff --git a/mailman/options.py b/mailman/options.py
index 8d752bfb5..5431b2da8 100644
--- a/mailman/options.py
+++ b/mailman/options.py
@@ -17,10 +17,13 @@
"""Common argument parsing."""
+from __future__ import unicode_literals
+
__metaclass__ = type
__all__ = [
'Options',
'SingleMailingListOptions',
+ 'MultipleMailingListOptions',
]
@@ -44,13 +47,13 @@ def check_unicode(option, opt, value):
return value.decode(sys.getdefaultencoding())
except UnicodeDecodeError:
raise OptionValueError(
- 'option %s: Cannot decode: %r' % (opt, value))
+ 'option {0}: Cannot decode: {1}'.format(opt, value))
def check_yesno(option, opt, value):
value = value.lower()
if value not in ('yes', 'no', 'y', 'n'):
- raise OptionValueError('option s: invalid: %r' % (opt, value))
+ raise OptionValueError('option {0}: invalid: {1}'.format(opt, value))
return value[0] == 'y'
@@ -92,8 +95,9 @@ class Options:
def add_common_options(self):
"""Add options common to all scripts."""
+ # Python requires str types here.
self.parser.add_option(
- '-C', '--config',
+ str('-C'), str('--config'),
help=_('Alternative configuration file to use'))
def initialize(self, propagate_logs=None):
@@ -135,6 +139,5 @@ class MultipleMailingListOptions(Options):
self.parser.add_option(
'-l', '--listname',
default=[], action='append', dest='listnames', type='unicode',
- help=("""\
+ help=_("""\
A mailing list name. It is okay to have multiple --listname options."""))
-
diff --git a/mailman/passwords.py b/mailman/passwords.py
index 9ce403cc7..0c8284c1c 100644
--- a/mailman/passwords.py
+++ b/mailman/passwords.py
@@ -20,6 +20,16 @@
Represents passwords using RFC 2307 syntax (as best we can tell).
"""
+from __future__ import unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Schemes',
+ 'make_secret',
+ 'check_response',
+ ]
+
+
import os
import re
import hmac
@@ -35,16 +45,10 @@ from mailman.core import errors
SALT_LENGTH = 20 # bytes
ITERATIONS = 2000
-__all__ = [
- 'Schemes',
- 'make_secret',
- 'check_response',
- ]
-
-class PasswordScheme(object):
- TAG = ''
+class PasswordScheme:
+ TAG = b''
@staticmethod
def make_secret(password):
@@ -63,11 +67,11 @@ class PasswordScheme(object):
class NoPasswordScheme(PasswordScheme):
- TAG = 'NONE'
+ TAG = b'NONE'
@staticmethod
def make_secret(password):
- return ''
+ return b''
@staticmethod
def check_response(challenge, response):
@@ -76,7 +80,7 @@ class NoPasswordScheme(PasswordScheme):
class ClearTextPasswordScheme(PasswordScheme):
- TAG = 'CLEARTEXT'
+ TAG = b'CLEARTEXT'
@staticmethod
def make_secret(password):
@@ -89,7 +93,7 @@ class ClearTextPasswordScheme(PasswordScheme):
class SHAPasswordScheme(PasswordScheme):
- TAG = 'SHA'
+ TAG = b'SHA'
@staticmethod
def make_secret(password):
@@ -104,7 +108,7 @@ class SHAPasswordScheme(PasswordScheme):
class SSHAPasswordScheme(PasswordScheme):
- TAG = 'SSHA'
+ TAG = b'SSHA'
@staticmethod
def make_secret(password):
@@ -130,7 +134,7 @@ class PBKDF2PasswordScheme(PasswordScheme):
# This is a bit nasty if we wanted a different prf or iterations. OTOH,
# we really have no clue what the standard LDAP-ish specification for
# those options is.
- TAG = 'PBKDF2 SHA %d' % ITERATIONS
+ TAG = b'PBKDF2 SHA {0}'.format(ITERATIONS)
@staticmethod
def _pbkdf2(password, salt, iterations):
@@ -141,13 +145,13 @@ class PBKDF2PasswordScheme(PasswordScheme):
"""
h = hmac.new(password, None, hashlib.sha1)
prf = h.copy()
- prf.update(salt + '\x00\x00\x00\x01')
- T = U = array('l', prf.digest())
+ prf.update(salt + b'\x00\x00\x00\x01')
+ T = U = array(b'l', prf.digest())
while iterations:
prf = h.copy()
prf.update(U.tostring())
- U = array('l', prf.digest())
- T = array('l', (t ^ u for t, u in zip(T, U)))
+ U = array(b'l', prf.digest())
+ T = array(b'l', (t ^ u for t, u in zip(T, U)))
iterations -= 1
return T.tostring()
@@ -167,7 +171,7 @@ class PBKDF2PasswordScheme(PasswordScheme):
def check_response(challenge, response, prf, iterations):
# Decode the challenge to get the number of iterations and salt
# XXX we don't support anything but sha prf
- if prf.lower() <> 'sha':
+ if prf.lower() <> b'sha':
return False
try:
iterations = int(iterations)
@@ -223,7 +227,7 @@ def make_secret(password, scheme=None):
if not scheme_class:
raise errors.BadPasswordSchemeError(scheme)
secret = scheme_class.make_secret(password)
- return '{%s}%s' % (scheme_class.TAG, secret)
+ return b'{{{0}}}{1}'.format(scheme_class.TAG, secret)
def check_response(challenge, response):
diff --git a/mailman/pipeline/__init__.py b/mailman/pipeline/__init__.py
index d6e1b0129..f73061874 100644
--- a/mailman/pipeline/__init__.py
+++ b/mailman/pipeline/__init__.py
@@ -17,6 +17,8 @@
"""The built in set of pipeline handlers."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'initialize',
diff --git a/mailman/pipeline/acknowledge.py b/mailman/pipeline/acknowledge.py
index f1bd585f6..de520df65 100644
--- a/mailman/pipeline/acknowledge.py
+++ b/mailman/pipeline/acknowledge.py
@@ -20,8 +20,12 @@
This only happens if the sender has set their AcknowledgePosts attribute.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['Acknowledge']
+__all__ = [
+ 'Acknowledge',
+ ]
from zope.interface import implements
diff --git a/mailman/pipeline/after_delivery.py b/mailman/pipeline/after_delivery.py
index d19d4a541..4626ba292 100644
--- a/mailman/pipeline/after_delivery.py
+++ b/mailman/pipeline/after_delivery.py
@@ -17,6 +17,8 @@
"""Perform some bookkeeping after a successful post."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'AfterDelivery',
diff --git a/mailman/pipeline/avoid_duplicates.py b/mailman/pipeline/avoid_duplicates.py
index 10c047004..0458e117c 100644
--- a/mailman/pipeline/avoid_duplicates.py
+++ b/mailman/pipeline/avoid_duplicates.py
@@ -23,8 +23,12 @@ 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, unicode_literals
+
__metaclass__ = type
-__all__ = ['AvoidDuplicates']
+__all__ = [
+ 'AvoidDuplicates',
+ ]
from email.Utils import getaddresses, formataddr
diff --git a/mailman/pipeline/calculate_recipients.py b/mailman/pipeline/calculate_recipients.py
index 9385c64e8..9837c1e6b 100644
--- a/mailman/pipeline/calculate_recipients.py
+++ b/mailman/pipeline/calculate_recipients.py
@@ -23,8 +23,12 @@ on the `recips' attribute of the message. This attribute is used by the
SendmailDeliver and BulkDeliver modules.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['CalculateRecipients']
+__all__ = [
+ 'CalculateRecipients',
+ ]
from zope.interface import implements
@@ -83,7 +87,7 @@ class CalculateRecipients:
Your urgent message to the %(realname)s mailing list was not authorized for
delivery. The original message as received by Mailman is attached.
""")
- raise errors.RejectMessage, Utils.wrap(text)
+ raise errors.RejectMessage(Utils.wrap(text))
# Calculate the regular recipients of the message
recips = set(member.address.address
for member in mlist.regular_members.members
@@ -133,8 +137,8 @@ def do_topic_filters(mlist, msg, msgdata, recips):
# The user did not select any topics of interest, so he gets
# this message by default.
continue
- if not mlist.getMemberOption(user,
- config.ReceiveNonmatchingTopics):
+ if not mlist.getMemberOption(
+ user, config.ReceiveNonmatchingTopics):
# The user has interest in some topics, but elects not to
# receive message that match no topics, so zap him.
zaprecips.append(user)
diff --git a/mailman/pipeline/cleanse.py b/mailman/pipeline/cleanse.py
index 28c660bdc..330f415c2 100644
--- a/mailman/pipeline/cleanse.py
+++ b/mailman/pipeline/cleanse.py
@@ -17,8 +17,12 @@
"""Cleanse certain headers from all messages."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['Cleanse']
+__all__ = [
+ 'Cleanse',
+ ]
import logging
diff --git a/mailman/pipeline/cleanse_dkim.py b/mailman/pipeline/cleanse_dkim.py
index 774fa78cf..38623079c 100644
--- a/mailman/pipeline/cleanse_dkim.py
+++ b/mailman/pipeline/cleanse_dkim.py
@@ -25,6 +25,8 @@ 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, unicode_literals
+
__metaclass__ = type
__all__ = [
'CleanseDKIM',
diff --git a/mailman/pipeline/cook_headers.py b/mailman/pipeline/cook_headers.py
index 88051970e..529d7ce5d 100644
--- a/mailman/pipeline/cook_headers.py
+++ b/mailman/pipeline/cook_headers.py
@@ -17,6 +17,8 @@
"""Cook a message's headers."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'CookHeaders',
@@ -178,7 +180,7 @@ def process(mlist, msg, msgdata):
if msgdata.get('_nolist') or not mlist.include_rfc2369_headers:
return
# This will act like an email address for purposes of formataddr()
- listid = '%s.%s' % (mlist.list_name, mlist.host_name)
+ listid = '{0}.{1}'.format(mlist.list_name, mlist.host_name)
cset = Utils.GetCharSet(mlist.preferred_language)
if mlist.description:
# Don't wrap the header since here we just want to get it properly RFC
@@ -187,7 +189,7 @@ def process(mlist, msg, msgdata):
listid_h = formataddr((str(i18ndesc), listid))
else:
# without desc we need to ensure the MUST brackets
- listid_h = '<%s>' % listid
+ listid_h = '<{0}>'.format(listid)
# We always add a List-ID: header.
del msg['list-id']
msg['List-Id'] = listid_h
@@ -195,7 +197,7 @@ def process(mlist, msg, msgdata):
# "X-List-Administrivia: yes" header. For all others (i.e. those coming
# from list posts), we add a bunch of other RFC 2369 headers.
requestaddr = mlist.request_address
- subfieldfmt = '<%s>, <mailto:%s>'
+ subfieldfmt = '<{0}>, <mailto:{1}>'
listinfo = mlist.script_url('listinfo')
headers = {}
# XXX reduced_list_headers used to suppress List-Help, List-Subject, and
@@ -203,20 +205,21 @@ def process(mlist, msg, msgdata):
# any more, so always add those three headers (others will still be
# suppressed).
headers.update({
- 'List-Help' : '<mailto:%s?subject=help>' % requestaddr,
- 'List-Unsubscribe': subfieldfmt % (listinfo, mlist.leave_address),
- 'List-Subscribe' : subfieldfmt % (listinfo, mlist.join_address),
+ 'List-Help' : '<mailto:{0}?subject=help>'.format(requestaddr),
+ 'List-Unsubscribe': subfieldfmt.format(listinfo, mlist.leave_address),
+ 'List-Subscribe' : subfieldfmt.format(listinfo, mlist.join_address),
})
if msgdata.get('reduced_list_headers'):
headers['X-List-Administrivia'] = 'yes'
else:
# List-Post: is controlled by a separate attribute
if mlist.include_list_post_header:
- headers['List-Post'] = '<mailto:%s>' % mlist.posting_address
+ headers['List-Post'] = '<mailto:{0}>'.format(mlist.posting_address)
# Add RFC 2369 and 5064 archiving headers, if archiving is enabled.
if mlist.archive:
for archiver in config.archivers:
- headers['List-Archive'] = '<%s>' % archiver.list_url(mlist)
+ headers['List-Archive'] = '<{0}>'.format(
+ archiver.list_url(mlist))
permalink = archiver.permalink(mlist, msg)
if permalink is not None:
headers['Archived-At'] = permalink
@@ -333,7 +336,7 @@ def ch_oneline(headerstr):
cset = 'utf-8'
h = make_header(d)
ustr = unicode(h)
- oneline = u''.join(ustr.splitlines())
+ 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.
diff --git a/mailman/pipeline/decorate.py b/mailman/pipeline/decorate.py
index bf9fbea80..82851fbf1 100644
--- a/mailman/pipeline/decorate.py
+++ b/mailman/pipeline/decorate.py
@@ -17,15 +17,18 @@
"""Decorate a message by sticking the header and footer around it."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['Decorate']
+__all__ = [
+ 'Decorate',
+ ]
import re
import logging
from email.MIMEText import MIMEText
-from string import Template
from zope.interface import implements
from mailman import Utils
@@ -33,6 +36,7 @@ from mailman.Message import Message
from mailman.config import config
from mailman.i18n import _
from mailman.interfaces.handler import IHandler
+from mailman.utilities.string import expand
log = logging.getLogger('mailman.error')
@@ -197,17 +201,18 @@ def decorate(mlist, template, extradict=None):
# Create a dictionary which includes the default set of interpolation
# variables allowed in headers and footers. These will be augmented by
# any key/value pairs in the extradict.
- d = dict(real_name = mlist.real_name,
- list_name = mlist.list_name,
- fqdn_listname = mlist.fqdn_listname,
- host_name = mlist.host_name,
- listinfo_page = mlist.script_url('listinfo'),
- description = mlist.description,
- info = mlist.info,
- )
+ substitutions = dict(
+ real_name = mlist.real_name,
+ list_name = mlist.list_name,
+ fqdn_listname = mlist.fqdn_listname,
+ host_name = mlist.host_name,
+ listinfo_page = mlist.script_url('listinfo'),
+ description = mlist.description,
+ info = mlist.info,
+ )
if extradict is not None:
- d.update(extradict)
- text = Template(template).safe_substitute(d)
+ substitutions.update(extradict)
+ text = expand(template, substitutions)
# Turn any \r\n line endings into just \n
return re.sub(r' *\r?\n', r'\n', text)
diff --git a/mailman/pipeline/docs/archives.txt b/mailman/pipeline/docs/archives.txt
index d81f6e27b..d90228525 100644
--- a/mailman/pipeline/docs/archives.txt
+++ b/mailman/pipeline/docs/archives.txt
@@ -105,8 +105,9 @@ But if the value is 'yes', then the message will be archived.
<BLANKLINE>
A message of great import.
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False), ('received_time', ...), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg: False
+ version : 3
Without either archiving header, and all other things being the same, the
message will get archived.
@@ -127,5 +128,6 @@ message will get archived.
<BLANKLINE>
A message of great import.
<BLANKLINE>
- >>> sorted(qdata.items())
- [('_parsemsg', False), ('received_time', ...), ('version', 3)]
+ >>> dump_msgdata(qdata)
+ _parsemsg: False
+ version : 3
diff --git a/mailman/pipeline/docs/digests.txt b/mailman/pipeline/docs/digests.txt
index e94f9912f..cb939f7ca 100644
--- a/mailman/pipeline/docs/digests.txt
+++ b/mailman/pipeline/docs/digests.txt
@@ -254,12 +254,14 @@ digest and an RFC 1153 plain text digest. The size threshold is in KB.
<BLANKLINE>
--...
--...
- >>> sorted(mimedata.items())
- [('_parsemsg', False),
- ('isdigest', True),
- ('listname', u'_xtest@example.com'),
- ('received_time', ...),
- ('recips', set([])), ('version', 3)]
+ >>> dump_msgdata(mimedata)
+ _parsemsg: False
+ isdigest : True
+ listname : _xtest@example.com
+ recips : set([])
+ version : 3
+
+
>>> print rfc1153msg.as_string()
From: _xtest-request@example.com
Subject: XTest Digest, Vol 2, Issue 10
@@ -389,12 +391,12 @@ digest and an RFC 1153 plain text digest. The size threshold is in KB.
End of XTest Digest, Vol 2, Issue 10
************************************
<BLANKLINE>
- >>> sorted(rfc1153data.items())
- [('_parsemsg', False),
- ('isdigest', True),
- ('listname', u'_xtest@example.com'),
- ('received_time', ...),
- ('recips', set([])), ('version', 3)]
+ >>> dump_msgdata(rfc1153data)
+ _parsemsg: False
+ isdigest : True
+ listname : _xtest@example.com
+ recips : set([])
+ version : 3
Internationalized digests
@@ -505,12 +507,13 @@ Set the digest threshold to zero so that the digests will be sent immediately.
<BLANKLINE>
--...
--...
- >>> sorted(mimedata.items())
- [('_parsemsg', False),
- ('isdigest', True),
- ('listname', u'_xtest@example.com'),
- ('received_time', ...),
- ('recips', set([])), ('version', 3)]
+ >>> dump_msgdata(mimedata)
+ _parsemsg: False
+ isdigest : True
+ listname : _xtest@example.com
+ recips : set([])
+ version : 3
+
>>> print rfc1153msg.as_string()
From: _xtest-request@example.com
Subject: Groupe XTest, Vol. 2, Parution 11
@@ -524,15 +527,9 @@ Set the digest threshold to zero so that the digests will be sent immediately.
<BLANKLINE>
...
<BLANKLINE>
- >>> sorted(rfc1153data.items())
- [('_parsemsg', False),
- ('isdigest', True),
- ('listname', u'_xtest@example.com'),
- ('received_time', ...),
- ('recips', set([])), ('version', 3)]
-
-
-Clean up
---------
-
- >>> config.DEFAULT_SERVER_LANGUAGE = u'en'
+ >>> dump_msgdata(rfc1153data)
+ _parsemsg: False
+ isdigest : True
+ listname : _xtest@example.com
+ recips : set([])
+ version : 3
diff --git a/mailman/pipeline/docs/nntp.txt b/mailman/pipeline/docs/nntp.txt
index 0120de394..3f48be1da 100644
--- a/mailman/pipeline/docs/nntp.txt
+++ b/mailman/pipeline/docs/nntp.txt
@@ -59,8 +59,7 @@ messages are gated to.
<BLANKLINE>
Something of great import.
<BLANKLINE>
- >>> sorted(msgdata.items())
- [('_parsemsg', False),
- ('listname', u'_xtest@example.com'),
- ('received_time', ...),
- ('version', 3)]
+ >>> dump_msgdata(msgdata)
+ _parsemsg: False
+ listname : _xtest@example.com
+ version : 3
diff --git a/mailman/pipeline/docs/to-outgoing.txt b/mailman/pipeline/docs/to-outgoing.txt
index 6142d86c7..5305db19f 100644
--- a/mailman/pipeline/docs/to-outgoing.txt
+++ b/mailman/pipeline/docs/to-outgoing.txt
@@ -51,12 +51,13 @@ additional key set: the mailing list name.
Subject: Here is a message
<BLANKLINE>
Something of great import.
- >>> sorted(qmsgdata.items())
- [('_parsemsg', False),
- ('bar', 2), ('foo', 1),
- ('listname', u'_xtest@example.com'),
- ('received_time', ...),
- ('verp', True), ('version', 3)]
+ >>> dump_msgdata(qmsgdata)
+ _parsemsg: False
+ bar : 2
+ foo : 1
+ listname : _xtest@example.com
+ verp : True
+ version : 3
>>> queue_size()
0
diff --git a/mailman/pipeline/file_recipients.py b/mailman/pipeline/file_recipients.py
index f356e2e18..89d10d783 100644
--- a/mailman/pipeline/file_recipients.py
+++ b/mailman/pipeline/file_recipients.py
@@ -17,6 +17,8 @@
"""Get the normal delivery recipients from a Sendmail style :include: file."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'FileRecipients',
diff --git a/mailman/pipeline/mime_delete.py b/mailman/pipeline/mime_delete.py
index c7e27bd5f..3c4e4154f 100644
--- a/mailman/pipeline/mime_delete.py
+++ b/mailman/pipeline/mime_delete.py
@@ -24,8 +24,12 @@ wrapping only single sections after other processing are replaced by their
contents.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['MIMEDelete']
+__all__ = [
+ 'MIMEDelete',
+ ]
import os
@@ -118,7 +122,7 @@ def process(mlist, msg, msgdata):
reset_payload(msg, useful)
changedp = 1
if changedp:
- msg['X-Content-Filtered-By'] = 'Mailman/MimeDel %s' % VERSION
+ msg['X-Content-Filtered-By'] = 'Mailman/MimeDel {0}'.format(VERSION)
@@ -146,7 +150,7 @@ def reset_payload(msg, subpart):
def filter_parts(msg, filtertypes, passtypes, filterexts, passexts):
# Look at all the message's subparts, and recursively filter
if not msg.is_multipart():
- return 1
+ return True
payload = msg.get_payload()
prelen = len(payload)
newpayload = []
@@ -176,8 +180,8 @@ def filter_parts(msg, filtertypes, passtypes, filterexts, passexts):
msg.set_payload(newpayload)
if postlen == 0 and prelen > 0:
# We threw away everything
- return 0
- return 1
+ return False
+ return True
@@ -199,12 +203,12 @@ def collapse_multipart_alternatives(msg):
def to_plaintext(msg):
- changedp = 0
+ changedp = False
for subpart in typed_subpart_iterator(msg, 'text', 'html'):
filename = tempfile.mktemp('.html')
fp = open(filename, 'w')
try:
- fp.write(subpart.get_payload(decode=1))
+ fp.write(subpart.get_payload(decode=True))
fp.close()
cmd = os.popen(config.HTML_TO_PLAIN_TEXT_COMMAND %
{'filename': filename})
@@ -222,7 +226,7 @@ def to_plaintext(msg):
del subpart['content-transfer-encoding']
subpart.set_payload(plaintext)
subpart.set_type('text/plain')
- changedp = 1
+ changedp = True
return changedp
@@ -251,6 +255,7 @@ are receiving the only remaining copy of the discarded message.
# Most cases also discard the message
raise errors.DiscardMessage
+
def get_file_ext(m):
"""
Get filename extension. Caution: some virus don't put filename
diff --git a/mailman/pipeline/moderate.py b/mailman/pipeline/moderate.py
index ec0a555d8..0b38c3a5a 100644
--- a/mailman/pipeline/moderate.py
+++ b/mailman/pipeline/moderate.py
@@ -17,6 +17,14 @@
"""Posting moderation filter."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'process',
+ ]
+
+
import re
from email.MIMEMessage import MIMEMessage
diff --git a/mailman/pipeline/owner_recipients.py b/mailman/pipeline/owner_recipients.py
index 046cac50c..ceb6ae0a1 100644
--- a/mailman/pipeline/owner_recipients.py
+++ b/mailman/pipeline/owner_recipients.py
@@ -17,11 +17,18 @@
"""Calculate the list owner recipients (includes moderators)."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'process',
+ ]
+
def process(mlist, msg, msgdata):
# The recipients are the owner and the moderator
msgdata['recips'] = mlist.owner + mlist.moderator
# Don't decorate these messages with the header/footers
- msgdata['nodecorate'] = 1
- msgdata['personalize'] = 0
+ msgdata['nodecorate'] = True
+ msgdata['personalize'] = False
diff --git a/mailman/pipeline/replybot.py b/mailman/pipeline/replybot.py
index ee0894d12..e24777774 100644
--- a/mailman/pipeline/replybot.py
+++ b/mailman/pipeline/replybot.py
@@ -17,21 +17,25 @@
"""Handler for auto-responses."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['Replybot']
+__all__ = [
+ 'Replybot',
+ ]
import time
import logging
import datetime
-from string import Template
from zope.interface import implements
from mailman import Message
from mailman import Utils
from mailman.i18n import _
from mailman.interfaces.handler import IHandler
+from mailman.utilities.string import expand
log = logging.getLogger('mailman.error')
@@ -97,7 +101,7 @@ def process(mlist, msg, msgdata):
else:
rtext = mlist.autoresponse_postings_text
# Interpolation and Wrap the response text.
- text = Utils.wrap(Template(rtext).safe_substitute(d))
+ text = Utils.wrap(expand(rtext, d))
outmsg = Message.UserNotification(sender, mlist.bounces_address,
subject, text, mlist.preferred_language)
outmsg['X-Mailer'] = _('The Mailman Replybot')
diff --git a/mailman/pipeline/scrubber.py b/mailman/pipeline/scrubber.py
index f7ffd51e1..fa9c8577f 100644
--- a/mailman/pipeline/scrubber.py
+++ b/mailman/pipeline/scrubber.py
@@ -17,6 +17,8 @@
"""Cleanse a message for archiving."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Scrubber',
diff --git a/mailman/pipeline/tagger.py b/mailman/pipeline/tagger.py
index db4bb13b9..9a0acc1e3 100644
--- a/mailman/pipeline/tagger.py
+++ b/mailman/pipeline/tagger.py
@@ -17,8 +17,12 @@
"""Extract topics from the original mail message."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['Tagger']
+__all__ = [
+ 'Tagger',
+ ]
import re
@@ -35,7 +39,7 @@ from mailman.interfaces.handler import IHandler
OR = '|'
CRNL = '\r\n'
-EMPTYSTRING = ''
+EMPTYBYTES = b''
NLTAB = '\n\t'
@@ -69,8 +73,10 @@ def process(mlist, msg, msgdata):
hits[name] = 1
break
if hits:
- msgdata['topichits'] = hits.keys()
- msg['X-Topics'] = NLTAB.join(hits.keys())
+ # Sort the keys and make them available both in the message metadata
+ # and in a message header.
+ msgdata['topichits'] = sorted(hits)
+ msg['X-Topics'] = NLTAB.join(sorted(hits))
@@ -97,7 +103,7 @@ def scanbody(msg, numlines=None):
reader = list(email.Iterators.body_line_iterator(msg))
while numlines is None or lineno < numlines:
try:
- line = reader.pop(0)
+ line = bytes(reader.pop(0))
except IndexError:
break
# Blank lines don't count
@@ -108,7 +114,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(EMPTYSTRING.join(lines))
+ msg = p.parsestr(EMPTYBYTES.join(lines))
return msg.get_all('subject', []) + msg.get_all('keywords', [])
diff --git a/mailman/pipeline/to_archive.py b/mailman/pipeline/to_archive.py
index 15f4c856e..7f1702fe9 100644
--- a/mailman/pipeline/to_archive.py
+++ b/mailman/pipeline/to_archive.py
@@ -17,6 +17,8 @@
"""Add the message to the archives."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'ToArchive',
diff --git a/mailman/pipeline/to_digest.py b/mailman/pipeline/to_digest.py
index e56c4a109..df2684986 100644
--- a/mailman/pipeline/to_digest.py
+++ b/mailman/pipeline/to_digest.py
@@ -25,6 +25,8 @@
# directory and the DigestRunner will craft the MIME, rfc1153, and
# (eventually) URL-subject linked digests from the mbox.
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'ToDigest',
@@ -243,9 +245,9 @@ def send_i18n_digests(mlist, mboxfp):
if not username:
username = addresses[0][1]
if username:
- username = ' (%s)' % username
+ username = ' ({0})'.format(username)
# Put count and Wrap the toc subject line
- wrapped = Utils.wrap('%2d. %s' % (msgcount, subject), 65)
+ wrapped = Utils.wrap('{0:2}. {1}'.format(msgcount, subject), 65)
slines = wrapped.split('\n')
# See if the user's name can fit on the last line
if len(slines[-1]) + len(username) > 70:
@@ -326,8 +328,8 @@ def send_i18n_digests(mlist, mboxfp):
# Honor the default setting
for h in config.digests.plain_digest_keep_headers.split():
if msg[h]:
- uh = Utils.wrap('%s: %s' % (h, Utils.oneline(msg[h],
- in_unicode=True)))
+ uh = Utils.wrap('{0}: {1}'.format(
+ h, Utils.oneline(msg[h], in_unicode=True)))
uh = '\n\t'.join(uh.split('\n'))
print >> plainmsg, uh
print >> plainmsg
@@ -402,8 +404,8 @@ def send_i18n_digests(mlist, mboxfp):
mimerecips.add(email_address)
else:
raise AssertionError(
- 'Digest member "%s" unexpected delivery mode: %s' %
- (email_address, member.delivery_mode))
+ 'Digest member "{0}" unexpected delivery mode: {1}'.format(
+ email_address, member.delivery_mode))
# Zap this since we're now delivering the last digest to these folks.
mlist.one_last_digest.clear()
# MIME
diff --git a/mailman/pipeline/to_outgoing.py b/mailman/pipeline/to_outgoing.py
index 9ff7ab88a..ff27593c4 100644
--- a/mailman/pipeline/to_outgoing.py
+++ b/mailman/pipeline/to_outgoing.py
@@ -22,6 +22,8 @@ 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, unicode_literals
+
__metaclass__ = type
__all__ = [
'ToOutgoing',
diff --git a/mailman/pipeline/to_usenet.py b/mailman/pipeline/to_usenet.py
index a635b65b3..220374348 100644
--- a/mailman/pipeline/to_usenet.py
+++ b/mailman/pipeline/to_usenet.py
@@ -17,8 +17,12 @@
"""Move the message to the mail->news queue."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['ToUsenet']
+__all__ = [
+ 'ToUsenet',
+ ]
import logging
diff --git a/mailman/queue/__init__.py b/mailman/queue/__init__.py
index e6d39ee1b..cb2225b13 100644
--- a/mailman/queue/__init__.py
+++ b/mailman/queue/__init__.py
@@ -24,6 +24,8 @@ written. First, the message is written to the pickle, then the metadata
dictionary is written.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Runner',
@@ -45,7 +47,6 @@ import traceback
from cStringIO import StringIO
from lazr.config import as_boolean, as_timedelta
-from string import Template
from zope.interface import implements
from mailman import Message
@@ -54,6 +55,7 @@ from mailman import i18n
from mailman.config import config
from mailman.interfaces.runner import IRunner
from mailman.interfaces.switchboard import ISwitchboard
+from mailman.utilities.string import expand
# 20 bytes of all bits set, maximum hashlib.sha.digest() value
shamax = 0xffffffffffffffffffffffffffffffffffffffffL
@@ -81,10 +83,10 @@ class Switchboard:
for conf in config.qrunner_configs:
name = conf.name.split('.')[-1]
assert name not in config.switchboards, (
- 'Duplicate qrunner name: %s' % name)
+ 'Duplicate qrunner name: {0}'.format(name))
substitutions = config.paths
substitutions['name'] = name
- path = Template(conf.path).safe_substitute(substitutions)
+ path = expand(conf.path, substitutions)
config.switchboards[name] = Switchboard(path)
def __init__(self, queue_directory,
@@ -103,7 +105,7 @@ class Switchboard:
:type recover: bool
"""
assert (numslices & (numslices - 1)) == 0, (
- 'Not a power of 2: %s' % numslices)
+ 'Not a power of 2: {0}'.format(numslices))
self.queue_directory = queue_directory
# Create the directory if it doesn't yet exist.
Utils.makedirs(self.queue_directory, 0770)
@@ -148,7 +150,8 @@ class Switchboard:
tmpfile = filename + '.tmp'
# Always add the metadata schema version number
data['version'] = config.QFILE_SCHEMA_VERSION
- # Filter out volatile entries
+ # Filter out volatile entries. Use .keys() so that we can mutate the
+ # dictionary during the iteration.
for k in data.keys():
if k.startswith('_'):
del data[k]
@@ -197,7 +200,7 @@ class Switchboard:
os.rename(bakfile, psvfile)
else:
os.unlink(bakfile)
- except EnvironmentError, e:
+ except EnvironmentError:
elog.exception(
'Failed to unlink/preserve backup file: %s', bakfile)
@@ -246,11 +249,11 @@ class Switchboard:
msg = cPickle.load(fp)
data_pos = fp.tell()
data = cPickle.load(fp)
- except Exception, s:
+ except Exception as error:
# If unpickling throws any exception, just log and
# preserve this entry
elog.error('Unpickling .bak exception: %s\n'
- 'Preserving file: %s', s, filebase)
+ 'Preserving file: %s', error, filebase)
self.finish(filebase, preserve=True)
else:
data['_bak_count'] = data.get('_bak_count', 0) + 1
@@ -289,8 +292,7 @@ class Runner:
section = getattr(config, 'qrunner.' + name)
substitutions = config.paths
substitutions['name'] = name
- self.queue_directory = Template(section.path).safe_substitute(
- substitutions)
+ self.queue_directory = expand(section.path, substitutions)
numslices = int(section.instances)
self.switchboard = Switchboard(
self.queue_directory, slice, numslices, True)
@@ -304,7 +306,7 @@ class Runner:
self._stop = False
def __repr__(self):
- return '<%s at %s>' % (self.__class__.__name__, id(self))
+ return '<{0} at {1:#x}>'.format(self.__class__.__name__, id(self))
def stop(self):
"""See `IRunner`."""
@@ -345,13 +347,13 @@ class Runner:
# Ask the switchboard for the message and metadata objects
# associated with this queue file.
msg, msgdata = self.switchboard.dequeue(filebase)
- except Exception, e:
+ except Exception as error:
# This used to just catch email.Errors.MessageParseError, but
# other problems can occur in message parsing, e.g.
# ValueError, and exceptions can occur in unpickling too. We
# don't want the runner to die, so we just log and skip this
# entry, but preserve it for analysis.
- self._log(e)
+ self._log(error)
elog.error('Skipping and preserving unparseable message: %s',
filebase)
self.switchboard.finish(filebase, preserve=True)
@@ -362,14 +364,14 @@ class Runner:
self._process_one_file(msg, msgdata)
dlog.debug('[%s] finishing filebase: %s', me, filebase)
self.switchboard.finish(filebase)
- except Exception, e:
+ except Exception as error:
# All runners that implement _dispose() must guarantee that
# exceptions are caught and dealt with properly. Still, there
# may be a bug in the infrastructure, and we do not want those
# to cause messages to be lost. Any uncaught exceptions will
# cause the message to be stored in the shunt queue for human
# intervention.
- self._log(e)
+ self._log(error)
# Put a marker in the metadata for unshunting.
msgdata['whichq'] = self.switchboard.queue_directory
# It is possible that shunting can throw an exception, e.g. a
@@ -380,11 +382,11 @@ class Runner:
new_filebase = shunt.enqueue(msg, msgdata)
elog.error('SHUNTING: %s', new_filebase)
self.switchboard.finish(filebase)
- except Exception, e:
+ except Exception as error:
# The message wasn't successfully shunted. Log the
# exception and try to preserve the original queue entry
# for possible analysis.
- self._log(e)
+ self._log(error)
elog.error(
'SHUNTING FAILED, preserving original entry: %s',
filebase)
diff --git a/mailman/queue/docs/runner.txt b/mailman/queue/docs/runner.txt
index db493110c..d24a8334c 100644
--- a/mailman/queue/docs/runner.txt
+++ b/mailman/queue/docs/runner.txt
@@ -61,10 +61,12 @@ on instance variables.
<BLANKLINE>
A test message.
<BLANKLINE>
- >>> sorted(runner.msgdata.items())
- [('_parsemsg', False),
- ('bar', 'no'), ('foo', 'yes'),
- ('lang', u'en'), ('listname', u'_xtest@example.com'),
- ('received_time', ...), ('version', 3)]
+ >>> dump_msgdata(runner.msgdata)
+ _parsemsg: False
+ bar : no
+ foo : yes
+ lang : en
+ listname : _xtest@example.com
+ version : 3
XXX More of the Runner API should be tested.
diff --git a/mailman/queue/docs/switchboard.txt b/mailman/queue/docs/switchboard.txt
index 741d435e1..88ab6ea93 100644
--- a/mailman/queue/docs/switchboard.txt
+++ b/mailman/queue/docs/switchboard.txt
@@ -29,7 +29,10 @@ Here's a helper function for ensuring things work correctly.
... for qfile in os.listdir(directory):
... root, ext = os.path.splitext(qfile)
... files[ext] = files.get(ext, 0) + 1
- ... return sorted(files.items())
+ ... if len(files) == 0:
+ ... print 'empty'
+ ... for ext in sorted(files):
+ ... print '{0}: {1}'.format(ext, files[ext])
Enqueing and dequeing
@@ -40,7 +43,7 @@ dictionary.
>>> filebase = switchboard.enqueue(msg)
>>> check_qfiles()
- [('.pck', 1)]
+ .pck: 1
To read the contents of a queue file, dequeue it.
@@ -51,17 +54,18 @@ To read the contents of a queue file, dequeue it.
<BLANKLINE>
A test message.
<BLANKLINE>
- >>> sorted(msgdata.items())
- [('_parsemsg', False), ('received_time', ...), ('version', 3)]
+ >>> dump_msgdata(msgdata)
+ _parsemsg: False
+ version : 3
>>> check_qfiles()
- [('.bak', 1)]
+ .bak: 1
To complete the dequeing process, removing all traces of the message file,
finish it (without preservation).
>>> switchboard.finish(filebase)
>>> check_qfiles()
- []
+ empty
When enqueing a file, you can provide additional metadata keys by using
keyword arguments.
@@ -69,20 +73,21 @@ keyword arguments.
>>> filebase = switchboard.enqueue(msg, {'foo': 1}, bar=2)
>>> msg, msgdata = switchboard.dequeue(filebase)
>>> switchboard.finish(filebase)
- >>> sorted(msgdata.items())
- [('_parsemsg', False),
- ('bar', 2), ('foo', 1),
- ('received_time', ...), ('version', 3)]
+ >>> dump_msgdata(msgdata)
+ _parsemsg: False
+ bar : 2
+ foo : 1
+ version : 3
Keyword arguments override keys from the metadata dictionary.
>>> filebase = switchboard.enqueue(msg, {'foo': 1}, foo=2)
>>> msg, msgdata = switchboard.dequeue(filebase)
>>> switchboard.finish(filebase)
- >>> sorted(msgdata.items())
- [('_parsemsg', False),
- ('foo', 2),
- ('received_time', ...), ('version', 3)]
+ >>> dump_msgdata(msgdata)
+ _parsemsg: False
+ foo : 2
+ version : 3
Iterating over files
@@ -99,7 +104,7 @@ iterate over just these files is to use the .files attribute.
>>> sorted(switchboard.files) == filebases
True
>>> check_qfiles()
- [('.pck', 3)]
+ .pck: 3
You can also use the .get_files() method if you want to iterate over all the
file bases for some other extension.
@@ -110,11 +115,11 @@ file bases for some other extension.
>>> bakfiles == filebases
True
>>> check_qfiles()
- [('.bak', 3)]
+ .bak: 3
>>> for filebase in switchboard.get_files('.bak'):
... switchboard.finish(filebase)
>>> check_qfiles()
- []
+ empty
Recovering files
@@ -130,10 +135,10 @@ place. These can be recovered when the switchboard is instantiated.
... msg, msgdata = switchboard.dequeue(filebase)
... # Don't call .finish()
>>> check_qfiles()
- [('.bak', 3)]
+ .bak: 3
>>> switchboard_2 = Switchboard(queue_directory, recover=True)
>>> check_qfiles()
- [('.pck', 3)]
+ .pck: 3
The files can be recovered explicitly.
@@ -141,10 +146,10 @@ The files can be recovered explicitly.
... msg, msgdata = switchboard.dequeue(filebase)
... # Don't call .finish()
>>> check_qfiles()
- [('.bak', 3)]
+ .bak: 3
>>> switchboard.recover_backup_files()
>>> check_qfiles()
- [('.pck', 3)]
+ .pck: 3
But the files will only be recovered at most three times before they are
considered defective. In order to prevent mail bombs and loops, once this
@@ -154,21 +159,21 @@ maximum is reached, the files will be preserved in the 'bad' queue.
... msg, msgdata = switchboard.dequeue(filebase)
... # Don't call .finish()
>>> check_qfiles()
- [('.bak', 3)]
+ .bak: 3
>>> switchboard.recover_backup_files()
>>> check_qfiles()
- []
+ empty
>>> bad = config.switchboards['bad']
>>> check_qfiles(bad.queue_directory)
- [('.psv', 3)]
+ .psv: 3
Clean up
>>> for file in os.listdir(bad.queue_directory):
... os.remove(os.path.join(bad.queue_directory, file))
>>> check_qfiles(bad.queue_directory)
- []
+ empty
Queue slices
diff --git a/mailman/rules/__init__.py b/mailman/rules/__init__.py
index 82a463f76..6c7034772 100644
--- a/mailman/rules/__init__.py
+++ b/mailman/rules/__init__.py
@@ -17,6 +17,8 @@
"""The built in rule set."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'initialize',
diff --git a/mailman/rules/administrivia.py b/mailman/rules/administrivia.py
index f21db6744..8807ef952 100644
--- a/mailman/rules/administrivia.py
+++ b/mailman/rules/administrivia.py
@@ -17,6 +17,8 @@
"""The administrivia rule."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Administrivia',
diff --git a/mailman/rules/any.py b/mailman/rules/any.py
index 71df37584..c337df7f1 100644
--- a/mailman/rules/any.py
+++ b/mailman/rules/any.py
@@ -17,8 +17,12 @@
"""Check if any previous rules have matched."""
-__all__ = ['Any']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'Any',
+ ]
from zope.interface import implements
diff --git a/mailman/rules/approved.py b/mailman/rules/approved.py
index 79dc0d934..f81c1ad04 100644
--- a/mailman/rules/approved.py
+++ b/mailman/rules/approved.py
@@ -17,6 +17,8 @@
"""Look for moderator pre-approval."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Approved',
@@ -31,7 +33,7 @@ from mailman.i18n import _
from mailman.interfaces.rules import IRule
-EMPTYSTRING = u''
+EMPTYSTRING = ''
diff --git a/mailman/rules/docs/administrivia.txt b/mailman/rules/docs/administrivia.txt
index df6685074..dba882775 100644
--- a/mailman/rules/docs/administrivia.txt
+++ b/mailman/rules/docs/administrivia.txt
@@ -9,8 +9,8 @@ used to catch messages posted to the list which should have been sent to the
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> mlist.administrivia = True
>>> rule = config.rules['administrivia']
- >>> rule.name
- 'administrivia'
+ >>> print rule.name
+ administrivia
For example, if the Subject header contains the word 'unsubscribe', the rule
matches.
diff --git a/mailman/rules/docs/approve.txt b/mailman/rules/docs/approve.txt
index cb3093e0d..dda531a4c 100644
--- a/mailman/rules/docs/approve.txt
+++ b/mailman/rules/docs/approve.txt
@@ -20,8 +20,8 @@ The 'approved' rule determines whether the message contains the proper
approval or not.
>>> rule = config.rules['approved']
- >>> rule.name
- 'approved'
+ >>> print rule.name
+ approved
No approval
diff --git a/mailman/rules/docs/implicit-dest.txt b/mailman/rules/docs/implicit-dest.txt
index 30db86611..e5c340dcd 100644
--- a/mailman/rules/docs/implicit-dest.txt
+++ b/mailman/rules/docs/implicit-dest.txt
@@ -6,8 +6,8 @@ not explicitly mentioned in the set of message recipients.
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['implicit-dest']
- >>> rule.name
- 'implicit-dest'
+ >>> print rule.name
+ implicit-dest
This rule matches messages that have implicit destination, meaning that the
mailing list's posting address isn't included in the explicit recipients.
diff --git a/mailman/rules/docs/loop.txt b/mailman/rules/docs/loop.txt
index 6391864b2..61612cd75 100644
--- a/mailman/rules/docs/loop.txt
+++ b/mailman/rules/docs/loop.txt
@@ -6,8 +6,8 @@ X-BeenThere header with the value of the list's posting address.
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['loop']
- >>> rule.name
- 'loop'
+ >>> print rule.name
+ loop
The header could be missing, in which case the rule does not match.
diff --git a/mailman/rules/docs/max-size.txt b/mailman/rules/docs/max-size.txt
index ae7921f9c..117691e59 100644
--- a/mailman/rules/docs/max-size.txt
+++ b/mailman/rules/docs/max-size.txt
@@ -8,8 +8,8 @@ bytes).
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['max-size']
- >>> rule.name
- 'max-size'
+ >>> print rule.name
+ max-size
For example, setting the maximum message size to 1 means that any message
bigger than that will match the rule.
diff --git a/mailman/rules/docs/moderation.txt b/mailman/rules/docs/moderation.txt
index 5e002a850..65be0d7da 100644
--- a/mailman/rules/docs/moderation.txt
+++ b/mailman/rules/docs/moderation.txt
@@ -8,8 +8,8 @@ email the list without having those messages be held for approval. The
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['moderation']
- >>> rule.name
- 'moderation'
+ >>> print rule.name
+ moderation
In the simplest case, the sender is not a member of the mailing list, so the
moderation rule can't match.
@@ -49,8 +49,8 @@ There is another, related rule for matching non-members, which simply matches
if the sender is /not/ a member of the mailing list.
>>> rule = config.rules['non-member']
- >>> rule.name
- 'non-member'
+ >>> print rule.name
+ non-member
If the sender is a member of this mailing list, the rule does not match.
diff --git a/mailman/rules/docs/news-moderation.txt b/mailman/rules/docs/news-moderation.txt
index 36310797c..4c095cc81 100644
--- a/mailman/rules/docs/news-moderation.txt
+++ b/mailman/rules/docs/news-moderation.txt
@@ -10,8 +10,8 @@ directly to the mailing list.
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['news-moderation']
- >>> rule.name
- 'news-moderation'
+ >>> print rule.name
+ news-moderation
Set the list configuraiton variable to enable newsgroup moderation.
diff --git a/mailman/rules/docs/no-subject.txt b/mailman/rules/docs/no-subject.txt
index 4fa001308..576111cd7 100644
--- a/mailman/rules/docs/no-subject.txt
+++ b/mailman/rules/docs/no-subject.txt
@@ -6,8 +6,8 @@ the empty string when stripped.
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['no-subject']
- >>> rule.name
- 'no-subject'
+ >>> print rule.name
+ no-subject
A message with a non-empty subject does not match the rule.
diff --git a/mailman/rules/docs/recipients.txt b/mailman/rules/docs/recipients.txt
index b364dd031..3cd49d501 100644
--- a/mailman/rules/docs/recipients.txt
+++ b/mailman/rules/docs/recipients.txt
@@ -6,8 +6,8 @@ number of explicit recipients addressed by the message.
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['max-recipients']
- >>> rule.name
- 'max-recipients'
+ >>> print rule.name
+ max-recipients
In this case, we'll create a message with 5 recipients. These include all
addresses in the To and CC headers.
diff --git a/mailman/rules/docs/rules.txt b/mailman/rules/docs/rules.txt
index 1fba63b64..095d11466 100644
--- a/mailman/rules/docs/rules.txt
+++ b/mailman/rules/docs/rules.txt
@@ -57,8 +57,8 @@ For example, the emergency rule just checks to see if the emergency flag is
set on the mailing list, and the message has not been pre-approved by the list
administrator.
- >>> rule.name
- 'emergency'
+ >>> print rule.name
+ emergency
>>> mlist.emergency = False
>>> rule.check(mlist, msg, {})
False
diff --git a/mailman/rules/docs/suspicious.txt b/mailman/rules/docs/suspicious.txt
index 0cc515ae4..190a34aca 100644
--- a/mailman/rules/docs/suspicious.txt
+++ b/mailman/rules/docs/suspicious.txt
@@ -7,8 +7,8 @@ confusing to users, and the list attribute that controls this is misnamed.
>>> mlist = config.db.list_manager.create(u'_xtest@example.com')
>>> rule = config.rules['suspicious-header']
- >>> rule.name
- 'suspicious-header'
+ >>> print rule.name
+ suspicious-header
Set the so-called suspicious header configuration variable.
diff --git a/mailman/rules/emergency.py b/mailman/rules/emergency.py
index f24acba8c..c2cee06c4 100644
--- a/mailman/rules/emergency.py
+++ b/mailman/rules/emergency.py
@@ -17,8 +17,12 @@
"""The emergency hold rule."""
-__all__ = ['Emergency']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'Emergency',
+ ]
from zope.interface import implements
@@ -33,10 +37,12 @@ class Emergency:
implements(IRule)
name = 'emergency'
+
description = _(
"""The mailing list is in emergency hold and this message was not
pre-approved by the list administrator.
""")
+
record = True
def check(self, mlist, msg, msgdata):
diff --git a/mailman/rules/implicit_dest.py b/mailman/rules/implicit_dest.py
index 9320a9ea1..3ddffa2cf 100644
--- a/mailman/rules/implicit_dest.py
+++ b/mailman/rules/implicit_dest.py
@@ -17,8 +17,12 @@
"""The implicit destination rule."""
-__all__ = ['ImplicitDestination']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'ImplicitDestination',
+ ]
import re
diff --git a/mailman/rules/loop.py b/mailman/rules/loop.py
index a0abbbaa4..564d20dc6 100644
--- a/mailman/rules/loop.py
+++ b/mailman/rules/loop.py
@@ -17,8 +17,12 @@
"""Look for a posting loop."""
-__all__ = ['Loop']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'Loop',
+ ]
from zope.interface import implements
@@ -33,7 +37,7 @@ class Loop:
implements(IRule)
name = 'loop'
- description = _("""Look for a posting loop, via the X-BeenThere header.""")
+ description = _('Look for a posting loop, via the X-BeenThere header.')
record = True
def check(self, mlist, msg, msgdata):
diff --git a/mailman/rules/max_recipients.py b/mailman/rules/max_recipients.py
index 6fa3e1ee4..a9cfd4a7f 100644
--- a/mailman/rules/max_recipients.py
+++ b/mailman/rules/max_recipients.py
@@ -17,8 +17,12 @@
"""The maximum number of recipients rule."""
-__all__ = ['MaximumRecipients']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'MaximumRecipients',
+ ]
from email.utils import getaddresses
diff --git a/mailman/rules/max_size.py b/mailman/rules/max_size.py
index 1ce280e88..bac79bbab 100644
--- a/mailman/rules/max_size.py
+++ b/mailman/rules/max_size.py
@@ -17,8 +17,12 @@
"""The maximum message size rule."""
-__all__ = ['MaximumSize']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'MaximumSize',
+ ]
from zope.interface import implements
diff --git a/mailman/rules/moderation.py b/mailman/rules/moderation.py
index 6f6bcfb36..708983f6b 100644
--- a/mailman/rules/moderation.py
+++ b/mailman/rules/moderation.py
@@ -17,11 +17,13 @@
"""Membership related rules."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
__all__ = [
'Moderation',
'NonMember',
]
-__metaclass__ = type
from zope.interface import implements
diff --git a/mailman/rules/news_moderation.py b/mailman/rules/news_moderation.py
index 822e43445..3ead80086 100644
--- a/mailman/rules/news_moderation.py
+++ b/mailman/rules/news_moderation.py
@@ -17,8 +17,12 @@
"""The news moderation rule."""
-__all__ = ['ModeratedNewsgroup']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'ModeratedNewsgroup',
+ ]
from zope.interface import implements
@@ -35,7 +39,7 @@ class ModeratedNewsgroup:
name = 'news-moderation'
description = _(
- u"""Match all messages posted to a mailing list that gateways to a
+ """Match all messages posted to a mailing list that gateways to a
moderated newsgroup.
""")
record = True
diff --git a/mailman/rules/no_subject.py b/mailman/rules/no_subject.py
index 08848154d..2487867e7 100644
--- a/mailman/rules/no_subject.py
+++ b/mailman/rules/no_subject.py
@@ -17,8 +17,12 @@
"""The no-Subject header rule."""
-__all__ = ['NoSubject']
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
+__all__ = [
+ 'NoSubject',
+ ]
from zope.interface import implements
diff --git a/mailman/rules/suspicious.py b/mailman/rules/suspicious.py
index 019e6c09b..00e9a5e9e 100644
--- a/mailman/rules/suspicious.py
+++ b/mailman/rules/suspicious.py
@@ -17,6 +17,8 @@
"""The historical 'suspicious header' rule."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'SuspiciousHeader',
@@ -72,12 +74,12 @@ def _parse_matching_header_opt(mlist):
value = line[i+1:].lstrip()
try:
cre = re.compile(value, re.IGNORECASE)
- except re.error, e:
+ except re.error as error:
# The regexp was malformed. BAW: should do a better
# job of informing the list admin.
log.error("""\
bad regexp in bounce_matching_header line: %s
-\n%s (cause: %s)""", mlist.real_name, value, e)
+\n%s (cause: %s)""", mlist.real_name, value, error)
else:
all.append((header, cre, line))
return all
diff --git a/mailman/rules/truth.py b/mailman/rules/truth.py
index 4d1a3cca7..45b5560c2 100644
--- a/mailman/rules/truth.py
+++ b/mailman/rules/truth.py
@@ -17,8 +17,12 @@
"""A rule which always matches."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
-__all__ = ['Truth']
+__all__ = [
+ 'Truth',
+ ]
from zope.interface import implements
diff --git a/mailman/styles/default.py b/mailman/styles/default.py
index 76b697296..d60773f51 100644
--- a/mailman/styles/default.py
+++ b/mailman/styles/default.py
@@ -17,6 +17,8 @@
"""Application of list styles to new and existing lists."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'DefaultStyle',
@@ -59,16 +61,16 @@ class DefaultStyle:
mlist.max_num_recipients = 10
mlist.max_message_size = 40 # KB
mlist.reply_goes_to_list = ReplyToMunging.no_munging
- mlist.reply_to_address = u''
+ mlist.reply_to_address = ''
mlist.first_strip_reply_to = False
mlist.admin_immed_notify = True
mlist.admin_notify_mchanges = False
mlist.require_explicit_destination = True
- mlist.acceptable_aliases = u''
+ mlist.acceptable_aliases = ''
mlist.send_reminders = True
mlist.send_welcome_msg = True
mlist.send_goodbye_msg = True
- mlist.bounce_matching_headers = u"""
+ mlist.bounce_matching_headers = """
# Lines that *start* with a '#' are comments.
to: friend@public.com
message-id: relay.comanche.denmark.eu
@@ -77,10 +79,10 @@ from: .*@uplinkpro.com
"""
mlist.header_matches = []
mlist.anonymous_list = False
- mlist.description = u''
- mlist.info = u''
- mlist.welcome_msg = u''
- mlist.goodbye_msg = u''
+ mlist.description = ''
+ mlist.info = ''
+ mlist.welcome_msg = ''
+ mlist.goodbye_msg = ''
mlist.subscribe_policy = 1
mlist.subscribe_auto_approval = []
mlist.unsubscribe_policy = 0
@@ -111,8 +113,8 @@ from: .*@uplinkpro.com
mlist.mime_is_default_digest = False
mlist.digest_size_threshold = 30 # KB
mlist.digest_send_periodic = True
- mlist.digest_header = u''
- mlist.digest_footer = u"""\
+ mlist.digest_header = ''
+ mlist.digest_footer = """\
_______________________________________________
$real_name mailing list
$fqdn_listname
@@ -131,14 +133,14 @@ ${listinfo_page}
mlist.archive_volume_frequency = 1
mlist.emergency = False
mlist.member_moderation_action = Action.hold
- mlist.member_moderation_notice = u''
+ mlist.member_moderation_notice = ''
mlist.accept_these_nonmembers = []
mlist.hold_these_nonmembers = []
mlist.reject_these_nonmembers = []
mlist.discard_these_nonmembers = []
mlist.forward_auto_discards = True
mlist.generic_nonmember_action = 1
- mlist.nonmember_rejection_notice = u''
+ mlist.nonmember_rejection_notice = ''
# Ban lists
mlist.ban_list = []
# Max autoresponses per day. A mapping between addresses and a
@@ -146,8 +148,8 @@ ${listinfo_page}
# autoresponses sent on that date.
mlist.hold_and_cmd_autoresponses = {}
mlist.subject_prefix = _(u'[$mlist.real_name] ')
- mlist.msg_header = u''
- mlist.msg_footer = u"""\
+ mlist.msg_header = ''
+ mlist.msg_footer = """\
_______________________________________________
$real_name mailing list
$fqdn_listname
@@ -171,9 +173,9 @@ ${listinfo_page}
# 1 - autorespond, but discard the original message
# 2 - autorespond, and forward the message on to be processed
mlist.autorespond_requests = 0
- mlist.autoresponse_postings_text = u''
- mlist.autoresponse_admin_text = u''
- mlist.autoresponse_request_text = u''
+ mlist.autoresponse_postings_text = ''
+ mlist.autoresponse_admin_text = ''
+ mlist.autoresponse_request_text = ''
mlist.autoresponse_graceperiod = datetime.timedelta(days=90)
mlist.postings_responses = {}
mlist.admin_responses = {}
@@ -196,8 +198,8 @@ ${listinfo_page}
# New style delivery status
mlist.delivery_status = {}
# NNTP gateway
- mlist.nntp_host = u''
- mlist.linked_newsgroup = u''
+ mlist.nntp_host = ''
+ mlist.linked_newsgroup = ''
mlist.gateway_to_news = False
mlist.gateway_to_mail = False
mlist.news_prefix_subject_too = True
diff --git a/mailman/styles/manager.py b/mailman/styles/manager.py
index 0b84f20e0..cf3d07711 100644
--- a/mailman/styles/manager.py
+++ b/mailman/styles/manager.py
@@ -17,6 +17,8 @@
"""Style manager."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'StyleManager',
diff --git a/mailman/testing/helpers.py b/mailman/testing/helpers.py
index 9b5252b71..f92c5f012 100644
--- a/mailman/testing/helpers.py
+++ b/mailman/testing/helpers.py
@@ -17,6 +17,8 @@
"""Various test helpers."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'TestableMaster',
diff --git a/mailman/testing/layers.py b/mailman/testing/layers.py
index f3a4266fd..19300ba1e 100644
--- a/mailman/testing/layers.py
+++ b/mailman/testing/layers.py
@@ -17,6 +17,8 @@
"""Mailman test layers."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'ConfigLayer',
@@ -31,7 +33,6 @@ import logging
import tempfile
from pkg_resources import resource_string
-from string import Template
from textwrap import dedent
from mailman.config import config
@@ -39,6 +40,7 @@ from mailman.core import initialize
from mailman.core.logging import get_handler
from mailman.i18n import _
from mailman.testing.helpers import SMTPServer
+from mailman.utilities.string import expand
NL = '\n'
@@ -105,11 +107,11 @@ class ConfigLayer:
# If stderr debugging is enabled, make sure subprocesses are also
# more verbose.
if cls.stderr:
- test_config += Template(dedent("""
+ test_config += expand(dedent("""
[logging.$name]
propagate: yes
level: debug
- """)).substitute(name=sub_name, path=path)
+ """), dict(name=sub_name, path=path))
# zope.testing sets up logging before we get to our own initialization
# function. This messes with the root logger, so explicitly set it to
# go to stderr.
@@ -170,7 +172,7 @@ class ConfigLayer:
zc.testing package. There should be a better way!
"""
from zope.testing.testrunner.options import parser
- parser.add_option('-e', '--stderr',
+ parser.add_option(str('-e'), str('--stderr'),
action='callback', callback=cls.handle_stderr,
help=_('Propagate log errors to stderr.'))
diff --git a/mailman/testing/mta.py b/mailman/testing/mta.py
index af1c19223..a10ba3c81 100644
--- a/mailman/testing/mta.py
+++ b/mailman/testing/mta.py
@@ -17,6 +17,8 @@
"""Fake MTA for testing purposes."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'FakeMTA',
diff --git a/mailman/testing/smtplistener.py b/mailman/testing/smtplistener.py
index f7f584f92..2094e20de 100644
--- a/mailman/testing/smtplistener.py
+++ b/mailman/testing/smtplistener.py
@@ -17,6 +17,14 @@
"""A test SMTP listener."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Server',
+ ]
+
+
import smtpd
import socket
import logging
@@ -71,7 +79,7 @@ class Server(smtpd.SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data):
"""Process a message by adding it to the mailbox."""
message = message_from_string(data)
- message['X-Peer'] = '%s:%s' % peer
+ message['X-Peer'] = '{0}:{1}'.format(*peer)
message['X-MailFrom'] = mailfrom
message['X-RcptTo'] = COMMASPACE.join(rcpttos)
log.info('[SMTPServer] processed message: %s',
diff --git a/mailman/tests/test_bounces.py b/mailman/tests/test_bounces.py
index 564c285c8..d5e525648 100644
--- a/mailman/tests/test_bounces.py
+++ b/mailman/tests/test_bounces.py
@@ -17,6 +17,14 @@
"""Test the bounce detection modules."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
import os
import sys
import email
diff --git a/mailman/tests/test_documentation.py b/mailman/tests/test_documentation.py
index b43b4378e..dd205c307 100644
--- a/mailman/tests/test_documentation.py
+++ b/mailman/tests/test_documentation.py
@@ -21,6 +21,8 @@ Note that doctest extraction does not currently work for zip file
distributions. doctest discovery currently requires file system traversal.
"""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'test_suite',
@@ -87,6 +89,19 @@ def stop():
pdb.set_trace()
+def dump_msgdata(msgdata, *additional_skips):
+ """Dump in a more readable way a message metadata dictionary."""
+ skips = set(additional_skips)
+ # Some stuff we always want to skip, because their values will always be
+ # variable data.
+ skips.add('received_time')
+ longest = max(len(key) for key in msgdata if key not in skips)
+ for key in sorted(msgdata):
+ if key in skips:
+ continue
+ print '{0:{2}}: {1}'.format(key, msgdata[key], longest)
+
+
def setup(testobj):
"""Test setup."""
# In general, I don't like adding convenience functions, since I think
@@ -95,6 +110,7 @@ def setup(testobj):
# hide some icky test implementation details.
testobj.globs['commit'] = config.db.commit
testobj.globs['config'] = config
+ testobj.globs['dump_msgdata'] = dump_msgdata
testobj.globs['message_from_string'] = specialized_message_from_string
testobj.globs['smtpd'] = SMTPLayer.smtpd
testobj.globs['stop'] = stop
@@ -114,8 +130,6 @@ def test_suite():
flags = (doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_NDIFF)
-## if config.tests.verbosity <= 2:
-## flags |= doctest.REPORT_ONLY_FIRST_FAILURE
# Add all the doctests in all subpackages.
doctest_files = {}
with chdir(topdir):
@@ -123,12 +137,6 @@ def test_suite():
for filename in os.listdir(docsdir):
if os.path.splitext(filename)[1] == '.txt':
doctest_files[filename] = os.path.join(docsdir, filename)
- # Sort or randomize the tests.
-## if config.tests.randomize:
-## files = doctest_files.keys()
-## random.shuffle(files)
-## else:
-## files = sorted(doctest_files)
files = sorted(doctest_files)
for filename in files:
path = doctest_files[filename]
diff --git a/mailman/tests/test_membership.py b/mailman/tests/test_membership.py
index fdb00eec9..9fa00867d 100644
--- a/mailman/tests/test_membership.py
+++ b/mailman/tests/test_membership.py
@@ -17,6 +17,14 @@
"""Unit tests for OldStyleMemberships."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
import time
import unittest
diff --git a/mailman/tests/test_passwords.py b/mailman/tests/test_passwords.py
index b84307c19..d8d310f08 100644
--- a/mailman/tests/test_passwords.py
+++ b/mailman/tests/test_passwords.py
@@ -17,6 +17,14 @@
"""Unit tests for the passwords module."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
import unittest
from mailman import passwords
diff --git a/mailman/tests/test_security_mgr.py b/mailman/tests/test_security_mgr.py
index a70129210..cdcd2021a 100644
--- a/mailman/tests/test_security_mgr.py
+++ b/mailman/tests/test_security_mgr.py
@@ -17,6 +17,14 @@
"""Unit tests for the SecurityManager module."""
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
import os
import errno
import unittest
diff --git a/mailman/utilities/__init__.py b/mailman/utilities/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/mailman/utilities/__init__.py
diff --git a/mailman/utilities/string.py b/mailman/utilities/string.py
new file mode 100644
index 000000000..2ca8c2021
--- /dev/null
+++ b/mailman/utilities/string.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2009 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/>.
+
+"""String utilities."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'expand'
+ ]
+
+
+import logging
+from string import Template
+
+log = logging.getLogger('mailman.error')
+
+
+
+def expand(template, substitutions, template_class=Template):
+ """Expand string template with substitutions.
+
+ :param template: A PEP 292 $-string template.
+ :type template: string
+ :param substitutions: The substitutions dictionary.
+ :type substitutions: dict
+ :param template_class: The template class to use.
+ :type template_class: class
+ :return: The substituted string.
+ :rtype: string
+ """
+ # Python 2.6 requires ** dictionaries to have str, not unicode keys, so
+ # convert as necessary. Note that string.Template uses **. For our
+ # purposes, keys should always be ascii. Values though can be anything.
+ cooked = substitutions.__class__()
+ for key in substitutions:
+ if isinstance(key, unicode):
+ key = key.encode('ascii')
+ cooked[key] = substitutions[key]
+ try:
+ return template_class(template).safe_substitute(cooked)
+ except (TypeError, ValueError):
+ # The template is really screwed up.
+ log.exception('broken template: %s', template)
diff --git a/template.py b/template.py
new file mode 100644
index 000000000..709f4f7c4
--- /dev/null
+++ b/template.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2009 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/>.
+
+"""Module stuff."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ ]