diff options
| -rw-r--r-- | .bzrignore | 1 | ||||
| -rw-r--r-- | MANIFEST.in | 8 | ||||
| -rw-r--r-- | src/mailman/app/docs/pipelines.rst | 19 | ||||
| -rw-r--r-- | src/mailman/commands/cli_conf.py | 27 | ||||
| -rw-r--r-- | src/mailman/commands/docs/conf.rst | 34 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 27 | ||||
| -rw-r--r-- | src/mailman/docs/START.rst | 56 | ||||
| -rw-r--r-- | src/mailman/handlers/cook_headers.py | 2 | ||||
| -rw-r--r-- | src/mailman/handlers/rfc_2369.py | 37 | ||||
| -rw-r--r-- | src/mailman/mta/docs/bulk.rst | 2 | ||||
| -rw-r--r-- | src/mailman/rest/docs/helpers.rst | 4 | ||||
| -rw-r--r-- | src/mailman/rest/docs/preferences.rst | 2 | ||||
| -rw-r--r-- | src/mailman/rest/helpers.py | 18 | ||||
| -rw-r--r-- | src/mailman/runners/docs/outgoing.rst | 12 | ||||
| -rw-r--r-- | src/mailman/testing/mta.py | 5 | ||||
| -rw-r--r-- | src/mailman/utilities/importer.py | 18 | ||||
| -rw-r--r-- | src/mailman/utilities/tests/test_import.py | 5 | ||||
| -rw-r--r-- | tox.ini | 8 |
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 |
