summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bzrignore1
-rw-r--r--MANIFEST.in8
-rw-r--r--src/mailman/app/docs/pipelines.rst19
-rw-r--r--src/mailman/commands/cli_conf.py27
-rw-r--r--src/mailman/commands/docs/conf.rst34
-rw-r--r--src/mailman/docs/NEWS.rst27
-rw-r--r--src/mailman/docs/START.rst56
-rw-r--r--src/mailman/handlers/cook_headers.py2
-rw-r--r--src/mailman/handlers/rfc_2369.py37
-rw-r--r--src/mailman/mta/docs/bulk.rst2
-rw-r--r--src/mailman/rest/docs/helpers.rst4
-rw-r--r--src/mailman/rest/docs/preferences.rst2
-rw-r--r--src/mailman/rest/helpers.py18
-rw-r--r--src/mailman/runners/docs/outgoing.rst12
-rw-r--r--src/mailman/testing/mta.py5
-rw-r--r--src/mailman/utilities/importer.py18
-rw-r--r--src/mailman/utilities/tests/test_import.py5
-rw-r--r--tox.ini8
18 files changed, 146 insertions, 139 deletions
diff --git a/.bzrignore b/.bzrignore
index 862dd0b10..b348eb336 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -21,3 +21,4 @@ distribute-*.egg
distribute-*.tar.gz
.coverage
htmlcov
+.tox
diff --git a/MANIFEST.in b/MANIFEST.in
index 94a369ae5..e4c64eedc 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,16 +1,16 @@
-include *.py *.rc
-include COPYING
+include *.py
+include COPYING MANIFEST.in
recursive-include .buildout *
recursive-include contrib *
recursive-include cron *
recursive-include data *
-global-include *.txt *.rst *.po *.mo *.cfg *.sql *.zcml *.html
+global-include *.txt *.rst *.po *.mo *.cfg *.sql *.zcml *.html *.ini
global-exclude *.egg-info
prune src/attic
prune src/web
prune eggs
prune parts
-include MANIFEST.in
+prune .tox
include src/mailman/testing/config.pck
include src/mailman/database/alembic/script.py.mako
include src/mailman/database/alembic/versions/*.py
diff --git a/src/mailman/app/docs/pipelines.rst b/src/mailman/app/docs/pipelines.rst
index 5aaf7cf62..adcdd1ea5 100644
--- a/src/mailman/app/docs/pipelines.rst
+++ b/src/mailman/app/docs/pipelines.rst
@@ -13,6 +13,12 @@ no way to stop a pipeline from processing the message once it's started.
default-posting-pipeline
>>> from mailman.core.pipelines import process
+For the purposes of these examples, we'll enable just one archiver.
+
+ >>> from mailman.interfaces.mailinglist import IListArchiverSet
+ >>> for archiver in IListArchiverSet(mlist).archivers:
+ ... archiver.is_enabled = (archiver.name == 'mhonarc')
+
Processing a message
====================
@@ -43,14 +49,14 @@ etc.
X-Mailman-Version: ...
Precedence: list
List-Id: <test.example.com>
+ Archived-At: http://lists.example.com/.../4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
+ List-Archive: <http://lists.example.com/archives/test@example.com>
+ List-Help: <mailto:test-request@example.com?subject=help>
List-Post: <mailto:test@example.com>
List-Subscribe: <http://lists.example.com/listinfo/test@example.com>,
<mailto:test-join@example.com>
- Archived-At: http://lists.example.com/.../4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB
List-Unsubscribe: <http://lists.example.com/listinfo/test@example.com>,
<mailto:test-leave@example.com>
- List-Archive: <http://lists.example.com/archives/test@example.com>
- List-Help: <mailto:test-request@example.com?subject=help>
<BLANKLINE>
First post!
<BLANKLINE>
@@ -154,10 +160,3 @@ There's now one message in the digest mailbox, getting ready to be sent.
<BLANKLINE>
First post!
<BLANKLINE>
-
-
-.. Clean up the digests
- >>> digest.clear()
- >>> digest.flush()
- >>> sum(1 for msg in digest_mbox(mlist))
- 0
diff --git a/src/mailman/commands/cli_conf.py b/src/mailman/commands/cli_conf.py
index 55f63c712..7fe9fce7d 100644
--- a/src/mailman/commands/cli_conf.py
+++ b/src/mailman/commands/cli_conf.py
@@ -65,19 +65,12 @@ class Conf:
key-values pair from any section matching the given key will be
displayed.
"""))
- command_parser.add_argument(
- '-t', '--sort',
- default=False, action='store_true',
- help=_('Sort the output by sections and keys.'))
def _get_value(self, section, key):
return getattr(getattr(config, section), key)
- def _sections(self, sort_p):
- sections = config.schema._section_schemas
- if sort_p:
- sections = sorted(sections)
- return sections
+ def _sections(self):
+ return sorted(config.schema._section_schemas)
def _print_full_syntax(self, section, key, value, output):
print('[{}] {}: {}'.format(section, key, value), file=output)
@@ -88,10 +81,8 @@ class Conf:
def _show_section_error(self, section):
self.parser.error('No such section: {}'.format(section))
- def _print_values_for_section(self, section, output, sort_p):
- current_section = getattr(config, section)
- if sort_p:
- current_section = sorted(current_section)
+ def _print_values_for_section(self, section, output):
+ current_section = sorted(getattr(config, section))
for key in current_section:
self._print_full_syntax(section, key,
self._get_value(section, key), output)
@@ -106,7 +97,6 @@ class Conf:
# Process the command, ignoring the closing of the output file.
section = args.section
key = args.key
- sort_p = args.sort
# Case 1: Both section and key are given, so we can directly look up
# the value.
if section is not None and key is not None:
@@ -119,12 +109,12 @@ class Conf:
# Case 2: Section is given, key is not given.
elif section is not None and key is None:
if self._section_exists(section):
- self._print_values_for_section(section, output, sort_p)
+ self._print_values_for_section(section, output)
else:
self._show_section_error(section)
# Case 3: Section is not given, key is given.
elif section is None and key is not None:
- for current_section in self._sections(sort_p):
+ for current_section in self._sections():
# We have to ensure that the current section actually exists
# and that it contains the given key.
if (self._section_exists(current_section) and
@@ -137,13 +127,12 @@ class Conf:
# Case 4: Neither section nor key are given, just display all the
# sections and their corresponding key/value pairs.
elif section is None and key is None:
- for current_section in self._sections(sort_p):
+ for current_section in self._sections():
# However, we have to make sure that the current sections and
# key which are being looked up actually exist before trying
# to print them.
if self._section_exists(current_section):
- self._print_values_for_section(
- current_section, output, sort_p)
+ self._print_values_for_section(current_section, output)
def process(self, args):
"""See `ICLISubCommand`."""
diff --git a/src/mailman/commands/docs/conf.rst b/src/mailman/commands/docs/conf.rst
index 2f708edc5..0ff5064bb 100644
--- a/src/mailman/commands/docs/conf.rst
+++ b/src/mailman/commands/docs/conf.rst
@@ -14,7 +14,6 @@ a specific key-value pair, or several key-value pairs.
... key = None
... section = None
... output = None
- ... sort = False
>>> from mailman.commands.cli_conf import Conf
>>> command = Conf()
@@ -22,9 +21,9 @@ To get a list of all key-value pairs of any section, you need to call the
command without any options.
>>> command.process(FakeArgs)
- [logging.archiver] path: mailman.log
+ [antispam] header_checks:
...
- [passwords] password_length: 8
+ [logging.bounce] level: info
...
[mailman] site_owner: noreply@example.com
...
@@ -33,9 +32,9 @@ You can list all the key-value pairs of a specific section.
>>> FakeArgs.section = 'shell'
>>> command.process(FakeArgs)
- [shell] use_ipython: no
[shell] banner: Welcome to the GNU Mailman shell
[shell] prompt: >>>
+ [shell] use_ipython: no
You can also pass a key and display all key-value pairs matching the given
key, along with the names of the corresponding sections.
@@ -44,20 +43,20 @@ key, along with the names of the corresponding sections.
>>> FakeArgs.key = 'path'
>>> command.process(FakeArgs)
[logging.archiver] path: mailman.log
- [logging.locks] path: mailman.log
- [logging.mischief] path: mailman.log
+ [logging.bounce] path: bounce.log
[logging.config] path: mailman.log
- [logging.error] path: mailman.log
- [logging.smtp] path: smtp.log
[logging.database] path: mailman.log
+ [logging.debug] path: debug.log
+ [logging.error] path: mailman.log
+ [logging.fromusenet] path: mailman.log
[logging.http] path: mailman.log
+ [logging.locks] path: mailman.log
+ [logging.mischief] path: mailman.log
[logging.root] path: mailman.log
- [logging.fromusenet] path: mailman.log
- [logging.bounce] path: bounce.log
- [logging.vette] path: mailman.log
[logging.runner] path: mailman.log
+ [logging.smtp] path: smtp.log
[logging.subscribe] path: mailman.log
- [logging.debug] path: debug.log
+ [logging.vette] path: mailman.log
If you specify both a section and a key, you will get the corresponding value.
@@ -66,16 +65,5 @@ If you specify both a section and a key, you will get the corresponding value.
>>> command.process(FakeArgs)
noreply@example.com
-You can also sort the output. The output is first sorted by section, then by
-key.
-
- >>> FakeArgs.key = None
- >>> FakeArgs.section = 'shell'
- >>> FakeArgs.sort = True
- >>> command.process(FakeArgs)
- [shell] banner: Welcome to the GNU Mailman shell
- [shell] prompt: >>>
- [shell] use_ipython: no
-
.. _`Postfix command postconf(1)`: http://www.postfix.org/postconf.1.html
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 3153a1fa0..2881eaacd 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -12,6 +12,15 @@ Here is a history of user visible changes to Mailman.
====================================
(2014-XX-XX)
+Commands
+--------
+ * The `mailman conf` command no longer takes the `-t/--sort` option; the
+ output is always sorted.
+
+Configuration
+-------------
+ * The ``[database]migrations_path`` setting is removed.
+
Database
--------
* The ORM layer, previously implemented with Storm, has been replaced by
@@ -19,8 +28,15 @@ Database
Bompard. Alembic is now used for all database schema migrations.
* The new logger `mailman.database` logs any errors at the database layer.
-API
----
+Development
+-----------
+ * You no longer have to create a virtual environment separately when running
+ the test suite. Just use `tox`.
+
+Interfaces
+----------
+ * The RFC 2369 headers added to outgoing messages are now added in sorted
+ order.
* Several changes to the internal API:
- `IListManager.mailing_lists` is guaranteed to be sorted in List-ID order.
- `IDomains.mailing_lists` is guaranteed to be sorted in List-ID order.
@@ -28,9 +44,10 @@ API
by `IDomain.mail_host` order.
- `ITemporaryDatabase` interface and all implementations are removed.
-Configuration
--------------
- * The ``[database]migrations_path`` setting is removed.
+REST
+----
+ * The JSON representation `http_etag` key uses an algorithm that is
+ insensitive to Python's dictionary sort order.
3.0 beta 4 -- "Time and Motion"
diff --git a/src/mailman/docs/START.rst b/src/mailman/docs/START.rst
index 960ceeedc..8123a726c 100644
--- a/src/mailman/docs/START.rst
+++ b/src/mailman/docs/START.rst
@@ -65,15 +65,30 @@ The `Development Setup Guide`_ is a recent step-by-step explanation of
how to set up a complete Mailman 3 system including the Mailman 3 core
and basic client API, Postorius, and HyperKitty.
-Building Mailman 3
-==================
+Testing Mailman 3
+=================
+
+To run the Mailman test suite, just use the `tox`_ command. `tox` creates a
+virtual environment (virtualenv) for you, installs all the dependencies into
+that virtualenv, and runs the test suite from that virtualenv. By default it
+does not use the `--system-site-packages` so it downloads everything from the
+Cheeseshop.
+
+You do have access to the virtualenv, and you can use this to run individual
+tests, e.g.::
+
+ % .tox/py27/bin/python -m nose2 -vv -P user
+
+Use `.tox/py27/bin/python -m nose2 --help` for more options.
-To build Mailman for development purposes, you will create a virtual
-environment. You need to have the `virtualenv`_ program installed.
Building for development
------------------------
+To build Mailman for development purposes, you can create a virtual
+environment outside of tox. You need to have the `virtualenv`_ program
+installed.
+
First, create a virtual environment. By default ``virtualenv`` uses the
``python`` executable it finds first on your ``$PATH``. Make sure this is
Python 2.7 (just start the interactive interpreter and check the version in
@@ -96,37 +111,7 @@ Now, activate the virtual environment and set it up for development::
% python setup.py develop
Sit back and have some Kombucha while you wait for everything to download and
-install. If you have older versions of some of the packages, the installation
-may be interrupted with an error such as::
-
- error: Installed distribution zope.interface 3.8.0 conflicts with requirement zope.interface>=4.1.0
-
-(It appears that this is a defect specific to the ``zope.interface``
-package; it's expected that it should upgrade in this situation.
-However, we cannot rule out similar problems with other packages.)
-
-This issue can be addressed in two ways. If you are worried about backward
-compatibility with the installed version of the package for some reason, you
-can restart the process by creating a virtualenv without the
-``--system-site-packages`` option. This may require installation of duplicates
-of many packages, as only the standard library and packages freshly installed
-in the virtualenv will be available to Python.
-
-The alternative is to keep the virtualenv installed with
-``--system-site-packages``, explicitly upgrade the package, and then
-restart the installation::
-
- % pip install --upgrade zope.interface
- % python setup.py develop
-
-Now you can run the test suite via::
-
- % nose2 -v
-
-You should see no failures. You can also run a subset of the full test suite
-by filter tests on the module or test name using the ``-P`` option::
-
- % nose2 -v -P user
+install.
Build the online docs by running::
@@ -249,3 +234,4 @@ A `five minute guide to Hyperkitty`_ is based on Toshio Kuratomi's README.
.. _`Postorius documentation`: http://www.pythonhosted.org/postorius/
.. _`HyperKitty documentation`: https://hyperkitty.readthedocs.org/en/latest/development.html
.. _`Development Setup Guide`: https://fedorahosted.org/hyperkitty/wiki/DevelopmentSetupGuide
+.. _tox: https://testrun.org/tox/latest/
diff --git a/src/mailman/handlers/cook_headers.py b/src/mailman/handlers/cook_headers.py
index 2ce3f653e..d5d096448 100644
--- a/src/mailman/handlers/cook_headers.py
+++ b/src/mailman/handlers/cook_headers.py
@@ -136,7 +136,7 @@ def process(mlist, msg, msgdata):
# Set Reply-To: header to point back to this list. Add this last
# because some folks think that some MUAs make it easier to delete
# addresses from the right than from the left.
- if mlist.reply_goes_to_list == ReplyToMunging.point_to_list:
+ if mlist.reply_goes_to_list is ReplyToMunging.point_to_list:
i18ndesc = uheader(mlist, mlist.description, 'Reply-To')
add((str(i18ndesc), mlist.posting_address))
del msg['reply-to']
diff --git a/src/mailman/handlers/rfc_2369.py b/src/mailman/handlers/rfc_2369.py
index 3ac721d19..ea909f41b 100644
--- a/src/mailman/handlers/rfc_2369.py
+++ b/src/mailman/handlers/rfc_2369.py
@@ -54,7 +54,7 @@ def process(mlist, msg, msgdata):
listid_h = formataddr((str(i18ndesc), list_id))
else:
# Without a description, we need to ensure the MUST brackets.
- listid_h = '<{0}>'.format(list_id)
+ listid_h = '<{}>'.format(list_id)
# No other agent should add a List-ID header except Mailman.
del msg['list-id']
msg['List-Id'] = listid_h
@@ -62,43 +62,50 @@ 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 = '<{0}>, <mailto:{1}>'
+ subfieldfmt = '<{}>, <mailto:{}>'
listinfo = mlist.script_url('listinfo')
- headers = {}
+ headers = []
# XXX reduced_list_headers used to suppress List-Help, List-Subject, and
# List-Unsubscribe from UserNotification. That doesn't seem to make sense
# any more, so always add those three headers (others will still be
# suppressed).
- headers.update({
- 'List-Help' : '<mailto:{0}?subject=help>'.format(requestaddr),
- 'List-Unsubscribe': subfieldfmt.format(listinfo, mlist.leave_address),
- 'List-Subscribe' : subfieldfmt.format(listinfo, mlist.join_address),
- })
+ headers.extend((
+ ('List-Help', '<mailto:{}?subject=help>'.format(requestaddr)),
+ ('List-Unsubscribe',
+ subfieldfmt.format(listinfo, mlist.leave_address)),
+ ('List-Subscribe', subfieldfmt.format(listinfo, mlist.join_address)),
+ ))
if not msgdata.get('reduced_list_headers'):
# List-Post: is controlled by a separate attribute, which is somewhat
# misnamed. RFC 2369 requires a value of NO if posting is not
# allowed, i.e. for an announce-only list.
- list_post = ('<mailto:{0}>'.format(mlist.posting_address)
+ list_post = ('<mailto:{}>'.format(mlist.posting_address)
if mlist.allow_list_posts
else 'NO')
- headers['List-Post'] = list_post
+ headers.append(('List-Post', list_post))
# Add RFC 2369 and 5064 archiving headers, if archiving is enabled.
if mlist.archive_policy is not ArchivePolicy.never:
archiver_set = IListArchiverSet(mlist)
for archiver in archiver_set.archivers:
if not archiver.is_enabled:
continue
- headers['List-Archive'] = '<{0}>'.format(
+ archiver_url = '<{}>'.format(
archiver.system_archiver.list_url(mlist))
+ headers.append(('List-Archive', archiver_url))
permalink = archiver.system_archiver.permalink(mlist, msg)
if permalink is not None:
- headers['Archived-At'] = permalink
+ headers.append(('Archived-At', permalink))
# XXX RFC 2369 also defines a List-Owner header which we are not currently
# supporting, but should.
- for h, v in headers.items():
- # First we delete any pre-existing headers because the RFC permits
- # only one copy of each, and we want to be sure it's ours.
+ #
+ # Some headers will appear more than once in the new set, e.g. the
+ # List-Archive and Archived-At headers. We want to delete any RFC 2369
+ # headers from the original message, but make sure to preserve all of the
+ # new headers we're adding. Go through the list of new headers twice,
+ # first removing any old ones, then adding all the new ones.
+ for h, v in headers:
del msg[h]
+ for h, v in sorted(headers):
# Wrap these lines if they are too long. 78 character width probably
# shouldn't be hardcoded, but is at least text-MUA friendly. The
# adding of 2 is for the colon-space separator.
diff --git a/src/mailman/mta/docs/bulk.rst b/src/mailman/mta/docs/bulk.rst
index f0c4e0132..f2a76229b 100644
--- a/src/mailman/mta/docs/bulk.rst
+++ b/src/mailman/mta/docs/bulk.rst
@@ -194,7 +194,7 @@ message sent, with all the recipients packed into the envelope recipients
<BLANKLINE>
This is a test.
-The ``X-RcptTo:`` header contains the set of recipients, in random order.
+The ``X-RcptTo:`` header contains the set of recipients, in sorted order.
>>> len(messages[0]['x-rcptto'].split(','))
100
diff --git a/src/mailman/rest/docs/helpers.rst b/src/mailman/rest/docs/helpers.rst
index 7405fc98f..0acbb5f45 100644
--- a/src/mailman/rest/docs/helpers.rst
+++ b/src/mailman/rest/docs/helpers.rst
@@ -45,7 +45,7 @@ gets modified to contain the etag under the ``http_etag`` key.
>>> resource = dict(geddy='bass', alex='guitar', neil='drums')
>>> json_data = etag(resource)
>>> print(resource['http_etag'])
- "43942176d8d5bb4414ccf35e2720ccd5251e66da"
+ "96e036d66248cab746b7d97047e08896fcfb2493"
For convenience, the etag function also returns the JSON representation of the
dictionary after tagging, since that's almost always what you want.
@@ -58,7 +58,7 @@ dictionary after tagging, since that's almost always what you want.
>>> dump_msgdata(data)
alex : guitar
geddy : bass
- http_etag: "43942176d8d5bb4414ccf35e2720ccd5251e66da"
+ http_etag: "96e036d66248cab746b7d97047e08896fcfb2493"
neil : drums
diff --git a/src/mailman/rest/docs/preferences.rst b/src/mailman/rest/docs/preferences.rst
index 8694364a4..f4839b1f4 100644
--- a/src/mailman/rest/docs/preferences.rst
+++ b/src/mailman/rest/docs/preferences.rst
@@ -162,7 +162,7 @@ deleted.
>>> dump_json('http://localhost:9001/3.0/addresses/anne@example.com'
... '/preferences')
acknowledge_posts: True
- http_etag: "5219245d1eea98bc107032013af20ef91bfb5c51"
+ http_etag: "1ff07b0367bede79ade27d217e12df3915aaee2b"
preferred_language: ja
self_link: http://localhost:9001/3.0/addresses/anne@example.com/preferences
diff --git a/src/mailman/rest/helpers.py b/src/mailman/rest/helpers.py
index 025ad1779..db3e43af0 100644
--- a/src/mailman/rest/helpers.py
+++ b/src/mailman/rest/helpers.py
@@ -38,6 +38,7 @@ from cStringIO import StringIO
from datetime import datetime, timedelta
from enum import Enum
from lazr.config import as_boolean
+from pprint import pformat
from restish import http
from restish.http import Response
from restish.resource import MethodDecorator
@@ -89,11 +90,12 @@ class ExtendedEncoder(json.JSONEncoder):
def etag(resource):
"""Calculate the etag and return a JSON representation.
- The input is a dictionary representing the resource. This dictionary must
- not contain an `http_etag` key. This function calculates the etag by
- using the sha1 hexdigest of the repr of the dictionary. It then inserts
- this value under the `http_etag` key, and returns the JSON representation
- of the modified dictionary.
+ The input is a dictionary representing the resource. This
+ dictionary must not contain an `http_etag` key. This function
+ calculates the etag by using the sha1 hexdigest of the
+ pretty-printed (and thus key-sorted and predictable) representation
+ of the dictionary. It then inserts this value under the `http_etag`
+ key, and returns the JSON representation of the modified dictionary.
:param resource: The original resource representation.
:type resource: dictionary
@@ -101,8 +103,10 @@ def etag(resource):
:rtype string
"""
assert 'http_etag' not in resource, 'Resource already etagged'
- etag = hashlib.sha1(repr(resource)).hexdigest()
-
+ # Calculate the tag from a predictable (i.e. sorted) representation of the
+ # dictionary. The actual details aren't so important. pformat() is
+ # guaranteed to sort the keys.
+ etag = hashlib.sha1(pformat(resource)).hexdigest()
resource['http_etag'] = '"{0}"'.format(etag)
return json.dumps(resource, cls=ExtendedEncoder)
diff --git a/src/mailman/runners/docs/outgoing.rst b/src/mailman/runners/docs/outgoing.rst
index a3220e423..d4a20d497 100644
--- a/src/mailman/runners/docs/outgoing.rst
+++ b/src/mailman/runners/docs/outgoing.rst
@@ -81,7 +81,7 @@ Every recipient got the same copy of the message.
Message-ID: <first>
X-Peer: ...
X-MailFrom: test-bounces@example.com
- X-RcptTo: cperson@example.com, bperson@example.com, aperson@example.com
+ X-RcptTo: aperson@example.com, bperson@example.com, cperson@example.com
<BLANKLINE>
First post!
@@ -222,7 +222,7 @@ VERP'd.
1
>>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
+ aperson@example.com, bperson@example.com, cperson@example.com
test-bounces@example.com
# Perform post-delivery bookkeeping.
@@ -242,7 +242,7 @@ The second message sent to the list is also not VERP'd.
1
>>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
+ aperson@example.com, bperson@example.com, cperson@example.com
test-bounces@example.com
# Perform post-delivery bookkeeping.
@@ -281,7 +281,7 @@ The next one is back to bulk delivery.
1
>>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
+ aperson@example.com, bperson@example.com, cperson@example.com
test-bounces@example.com
>>> config.pop('verp occasionally')
@@ -394,7 +394,7 @@ Neither the first message...
1
>>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
+ aperson@example.com, bperson@example.com, cperson@example.com
test-bounces@example.com
...nor the second message is VERP'd.
@@ -409,5 +409,5 @@ Neither the first message...
1
>>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
+ aperson@example.com, bperson@example.com, cperson@example.com
test-bounces@example.com
diff --git a/src/mailman/testing/mta.py b/src/mailman/testing/mta.py
index 7481e0093..875647485 100644
--- a/src/mailman/testing/mta.py
+++ b/src/mailman/testing/mta.py
@@ -161,6 +161,11 @@ class ConnectionCountingServer(QueueServer):
log.info('[ConnectionCountingServer] accepted: %s', address)
StatisticsChannel(self, connection, address)
+ def process_message(self, peer, mailfrom, rcpttos, data):
+ # Provide a guaranteed order to recpttos.
+ QueueServer.process_message(
+ self, peer, mailfrom, sorted(rcpttos), data)
+
def reset(self):
"""See `lazr.smtp.server.Server`."""
QueueServer.reset(self)
diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py
index baaa0e020..0523bb06f 100644
--- a/src/mailman/utilities/importer.py
+++ b/src/mailman/utilities/importer.py
@@ -308,13 +308,15 @@ def import_config_pck(mlist, config_dict):
'digest_header': 'digest_header_uri',
'digest_footer': 'digest_footer_uri',
}
- # The best we can do is convert only the most common ones.
- convert_placeholders = {
- '%(real_name)s': '$display_name',
- '%(real_name)s@%(host_name)s': '$fqdn_listname',
- '%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s':
- '$listinfo_uri',
- }
+ # The best we can do is convert only the most common ones. These are
+ # order dependent; the longer substitution with the common prefix must
+ # show up earlier.
+ convert_placeholders = [
+ ('%(real_name)s@%(host_name)s', '$fqdn_listname'),
+ ('%(real_name)s', '$display_name'),
+ ('%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s',
+ '$listinfo_uri'),
+ ]
# Collect defaults.
defaults = {}
for oldvar, newvar in convert_to_uri.items():
@@ -339,7 +341,7 @@ def import_config_pck(mlist, config_dict):
continue
text = config_dict[oldvar]
text = text.decode('utf-8', 'replace')
- for oldph, newph in convert_placeholders.items():
+ for oldph, newph in convert_placeholders:
text = text.replace(oldph, newph)
default_value, default_text = defaults.get(newvar, (None, None))
if not text and not (default_value or default_text):
diff --git a/src/mailman/utilities/tests/test_import.py b/src/mailman/utilities/tests/test_import.py
index e6eb9344c..be09a3256 100644
--- a/src/mailman/utilities/tests/test_import.py
+++ b/src/mailman/utilities/tests/test_import.py
@@ -501,8 +501,9 @@ class TestConvertToURI(unittest.TestCase):
))
loader = getUtility(ITemplateLoader)
text = loader.get(template_uri)
- self.assertEqual(text, expected_text,
- 'Old variables were not converted for %s' % newvar)
+ self.assertEqual(
+ text, expected_text,
+ 'Old variables were not converted for %s' % newvar)
def test_keep_default(self):
# If the value was not changed from MM2.1's default, don't import it.
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 000000000..3a9eb91d4
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,8 @@
+[tox]
+envlist = py27
+recreate = True
+
+[testenv]
+commands = python -m nose2 -v
+#sitepackages = True
+usedevelop = True