diff options
| author | Barry Warsaw | 2009-01-16 21:04:21 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2009-01-16 21:04:21 -0500 |
| commit | ae3d0cc316b826b8325507d960ccf84da601c3b0 (patch) | |
| tree | 3485e2ca463c2131a0ffb1693bc60d569cc9d8b7 | |
| parent | a3f7d07c62b2f7d6ac9d0b700883826c2838db60 (diff) | |
| download | mailman-ae3d0cc316b826b8325507d960ccf84da601c3b0.tar.gz mailman-ae3d0cc316b826b8325507d960ccf84da601c3b0.tar.zst mailman-ae3d0cc316b826b8325507d960ccf84da601c3b0.zip | |
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__ = [ + ] |
