diff options
| author | Barry Warsaw | 2015-12-20 12:44:06 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2015-12-20 12:44:06 -0500 |
| commit | bb45766f91ddce5e68ddf0a1fe6fe67d2f93dd60 (patch) | |
| tree | 1eb70a7836fc5e59968c635f65715de91c766c09 /src/mailman | |
| parent | d7bc81a6ab97cd00c70da901cb1d04f80f896685 (diff) | |
| download | mailman-bb45766f91ddce5e68ddf0a1fe6fe67d2f93dd60.tar.gz mailman-bb45766f91ddce5e68ddf0a1fe6fe67d2f93dd60.tar.zst mailman-bb45766f91ddce5e68ddf0a1fe6fe67d2f93dd60.zip | |
Diffstat (limited to 'src/mailman')
| -rw-r--r-- | src/mailman/app/lifecycle.py | 12 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_lifecycle.py | 10 | ||||
| -rw-r--r-- | src/mailman/commands/cli_control.py | 2 | ||||
| -rw-r--r-- | src/mailman/commands/cli_help.py | 2 | ||||
| -rw-r--r-- | src/mailman/commands/cli_lists.py | 4 | ||||
| -rw-r--r-- | src/mailman/commands/cli_send_digests.py | 69 | ||||
| -rw-r--r-- | src/mailman/commands/tests/test_send_digests.py | 287 | ||||
| -rw-r--r-- | src/mailman/database/alembic/versions/70af5a4e5790_digests.py | 2 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 3 | ||||
| -rw-r--r-- | src/mailman/handlers/to_digest.py | 78 | ||||
| -rw-r--r-- | src/mailman/interfaces/mailinglist.py | 3 | ||||
| -rw-r--r-- | src/mailman/model/mailinglist.py | 2 | ||||
| -rw-r--r-- | src/mailman/runners/docs/digester.rst | 2 |
13 files changed, 433 insertions, 43 deletions
diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py index 140ccab21..2120c12c6 100644 --- a/src/mailman/app/lifecycle.py +++ b/src/mailman/app/lifecycle.py @@ -94,14 +94,12 @@ def create_list(fqdn_listname, owners=None, style_name=None): def remove_list(mlist): """Remove the list and all associated artifacts and subscriptions.""" - fqdn_listname = mlist.fqdn_listname + # Remove the list's data directory, if it exists. + try: + shutil.rmtree(mlist.data_path) + except FileNotFoundError: + pass # Delete the mailing list from the database. getUtility(IListManager).delete(mlist) # Do the MTA-specific list deletion tasks call_name(config.mta.incoming).delete(mlist) - # Remove the list directory, if it exists. - try: - shutil.rmtree(os.path.join(config.LIST_DATA_DIR, fqdn_listname)) - except OSError as error: - if error.errno != errno.ENOENT: - raise diff --git a/src/mailman/app/tests/test_lifecycle.py b/src/mailman/app/tests/test_lifecycle.py index 62cbbf768..df2bf5233 100644 --- a/src/mailman/app/tests/test_lifecycle.py +++ b/src/mailman/app/tests/test_lifecycle.py @@ -26,7 +26,6 @@ import os import shutil import unittest -from mailman.config import config from mailman.interfaces.address import InvalidEmailAddressError from mailman.interfaces.domain import BadDomainSpecificationError from mailman.app.lifecycle import create_list, remove_list @@ -47,13 +46,12 @@ class TestLifecycle(unittest.TestCase): def test_unregistered_domain(self): # Creating a list with an unregistered domain raises an exception. self.assertRaises(BadDomainSpecificationError, - create_list, 'test@nodomain.example.org') + create_list, 'test@nodomain.example.org') def test_remove_list_error(self): # An error occurs while deleting the list's data directory. mlist = create_list('test@example.com') - data_dir = os.path.join(config.LIST_DATA_DIR, mlist.fqdn_listname) - os.chmod(data_dir, 0) - self.addCleanup(shutil.rmtree, data_dir) + os.chmod(mlist.data_path, 0) + self.addCleanup(shutil.rmtree, mlist.data_path) self.assertRaises(OSError, remove_list, mlist) - os.chmod(data_dir, 0o777) + os.chmod(mlist.data_path, 0o777) diff --git a/src/mailman/commands/cli_control.py b/src/mailman/commands/cli_control.py index 5bd9ef1e3..df1cc373b 100644 --- a/src/mailman/commands/cli_control.py +++ b/src/mailman/commands/cli_control.py @@ -205,7 +205,7 @@ class Stop(SignalCommand): class Reopen(SignalCommand): - """Signal the Mailman processes to re-open their log files..""" + """Signal the Mailman processes to re-open their log files.""" name = 'reopen' message = _('Reopening the Mailman runners') diff --git a/src/mailman/commands/cli_help.py b/src/mailman/commands/cli_help.py index eec47b0f0..df745244e 100644 --- a/src/mailman/commands/cli_help.py +++ b/src/mailman/commands/cli_help.py @@ -30,7 +30,7 @@ from zope.interface import implementer @implementer(ICLISubCommand) class Help: # Lowercase, to match argparse's default --help text. - """show this help message and exit""" + """Show this help message and exit.""" name = 'help' diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py index 3d5fcd634..cbc797e5b 100644 --- a/src/mailman/commands/cli_lists.py +++ b/src/mailman/commands/cli_lists.py @@ -122,7 +122,7 @@ class Lists: @implementer(ICLISubCommand) class Create: - """Create a mailing list""" + """Create a mailing list.""" name = 'create' @@ -238,7 +238,7 @@ class Create: @implementer(ICLISubCommand) class Remove: - """Remove a mailing list""" + """Remove a mailing list.""" name = 'remove' diff --git a/src/mailman/commands/cli_send_digests.py b/src/mailman/commands/cli_send_digests.py new file mode 100644 index 000000000..842054982 --- /dev/null +++ b/src/mailman/commands/cli_send_digests.py @@ -0,0 +1,69 @@ +# Copyright (C) 2015 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/>. + +"""The `send_digests` subcommand.""" + +__all__ = [ + 'Send', + ] + + +import sys + +from mailman.core.i18n import _ +from mailman.handlers.to_digest import maybe_send_digest_now +from mailman.interfaces.command import ICLISubCommand +from mailman.interfaces.listmanager import IListManager +from zope.component import getUtility +from zope.interface import implementer + + + +@implementer(ICLISubCommand) +class Send: + """Send some mailing list digests right now.""" + + name = 'send-digests' + + def add(self, parser, command_parser): + """See `ICLISubCommand`.""" + + command_parser.add_argument( + '-l', '--list', + default=[], dest='lists', metavar='list', action='append', + help=_("""Send the digests for this mailing list. Multiple --list + options can be given. The argument can either be a List-ID + or a fully qualified list name. Without this option, the + digests for all mailing lists will be sent if possible.""")) + + def process(self, args): + """See `ICLISubCommand`.""" + if not args.lists: + # Send the digests for every list. + maybe_send_digest_now(force=True) + return + list_manager = getUtility(IListManager) + for list_spec in args.lists: + # We'll accept list-ids or fqdn list names. + if '@' in list_spec: + mlist = list_manager.get(list_spec) + else: + mlist = list_manager.get_by_list_id(list_spec) + if mlist is None: + print(_('No such list found: $list_spec'), file=sys.stderr) + continue + maybe_send_digest_now(mlist, force=True) diff --git a/src/mailman/commands/tests/test_send_digests.py b/src/mailman/commands/tests/test_send_digests.py new file mode 100644 index 000000000..937922d7c --- /dev/null +++ b/src/mailman/commands/tests/test_send_digests.py @@ -0,0 +1,287 @@ +# Copyright (C) 2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test the send-digests subcommand.""" + +__all__ = [ + 'TestSendDigests', + ] + + +import os +import unittest + +from io import StringIO +from mailman.app.lifecycle import create_list +from mailman.commands.cli_send_digests import Send +from mailman.config import config +from mailman.interfaces.member import DeliveryMode +from mailman.runners.digest import DigestRunner +from mailman.testing.helpers import ( + get_queue_messages, make_testable_runner, + specialized_message_from_string as mfs, subscribe) +from mailman.testing.layers import ConfigLayer +from unittest.mock import patch + + + +class FakeArgs: + def __init__(self): + self.lists = [] + + + +class TestSendDigests(unittest.TestCase): + """Test the send-digests subcommand.""" + + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('ant@example.com') + self._mlist.digests_enabled = True + self._mlist.digest_size_threshold = 100000 + self._mlist.send_welcome_message = False + self._command = Send() + self._handler = config.handlers['to-digest'] + self._runner = make_testable_runner(DigestRunner, 'digest') + # The mailing list needs at least one digest recipient. + member = subscribe(self._mlist, 'Anne') + member.preferences.delivery_mode = DeliveryMode.plaintext_digests + + def test_send_one_digest_by_list_id(self): + msg = mfs("""\ +To: ant@example.com +From: anne@example.com +Subject: message 1 + +""") + self._handler.process(self._mlist, msg, {}) + del msg['subject'] + msg['subject'] = 'message 2' + self._handler.process(self._mlist, msg, {}) + # There are no digests already being sent, but the ant mailing list + # does have a digest mbox collecting messages. + items = get_queue_messages('digest') + self.assertEqual(len(items), 0) + mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') + self.assertGreater(os.path.getsize(mailbox_path), 0) + args = FakeArgs() + args.lists.append('ant.example.com') + self._command.process(args) + self._runner.run() + # Now, there's no digest mbox and there's a plaintext digest in the + # outgoing queue. + self.assertFalse(os.path.exists(mailbox_path)) + items = get_queue_messages('virgin') + self.assertEqual(len(items), 1) + digest_contents = str(items[0].msg) + self.assertIn('Subject: message 1', digest_contents) + self.assertIn('Subject: message 2', digest_contents) + + def test_send_one_digest_by_fqdn_listname(self): + msg = mfs("""\ +To: ant@example.com +From: anne@example.com +Subject: message 1 + +""") + self._handler.process(self._mlist, msg, {}) + del msg['subject'] + msg['subject'] = 'message 2' + self._handler.process(self._mlist, msg, {}) + # There are no digests already being sent, but the ant mailing list + # does have a digest mbox collecting messages. + items = get_queue_messages('digest') + self.assertEqual(len(items), 0) + mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') + self.assertGreater(os.path.getsize(mailbox_path), 0) + args = FakeArgs() + args.lists.append('ant@example.com') + self._command.process(args) + self._runner.run() + # Now, there's no digest mbox and there's a plaintext digest in the + # outgoing queue. + self.assertFalse(os.path.exists(mailbox_path)) + items = get_queue_messages('virgin') + self.assertEqual(len(items), 1) + digest_contents = str(items[0].msg) + self.assertIn('Subject: message 1', digest_contents) + self.assertIn('Subject: message 2', digest_contents) + + def test_send_one_digest_to_missing_list_id(self): + msg = mfs("""\ +To: ant@example.com +From: anne@example.com +Subject: message 1 + +""") + self._handler.process(self._mlist, msg, {}) + del msg['subject'] + msg['subject'] = 'message 2' + self._handler.process(self._mlist, msg, {}) + # There are no digests already being sent, but the ant mailing list + # does have a digest mbox collecting messages. + items = get_queue_messages('digest') + self.assertEqual(len(items), 0) + mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') + self.assertGreater(os.path.getsize(mailbox_path), 0) + args = FakeArgs() + args.lists.append('bee.example.com') + stderr = StringIO() + with patch('mailman.commands.cli_send_digests.sys.stderr', stderr): + self._command.process(args) + self._runner.run() + # The warning was printed to stderr. + self.assertEqual(stderr.getvalue(), + 'No such list found: bee.example.com\n') + # And no digest was prepared. + self.assertGreater(os.path.getsize(mailbox_path), 0) + items = get_queue_messages('virgin') + self.assertEqual(len(items), 0) + + def test_send_one_digest_to_missing_fqdn_listname(self): + msg = mfs("""\ +To: ant@example.com +From: anne@example.com +Subject: message 1 + +""") + self._handler.process(self._mlist, msg, {}) + del msg['subject'] + msg['subject'] = 'message 2' + self._handler.process(self._mlist, msg, {}) + # There are no digests already being sent, but the ant mailing list + # does have a digest mbox collecting messages. + items = get_queue_messages('digest') + self.assertEqual(len(items), 0) + mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') + self.assertGreater(os.path.getsize(mailbox_path), 0) + args = FakeArgs() + args.lists.append('bee@example.com') + stderr = StringIO() + with patch('mailman.commands.cli_send_digests.sys.stderr', stderr): + self._command.process(args) + self._runner.run() + # The warning was printed to stderr. + self.assertEqual(stderr.getvalue(), + 'No such list found: bee@example.com\n') + # And no digest was prepared. + self.assertGreater(os.path.getsize(mailbox_path), 0) + items = get_queue_messages('virgin') + self.assertEqual(len(items), 0) + + def test_send_digest_to_one_missing_and_one_existing_list(self): + msg = mfs("""\ +To: ant@example.com +From: anne@example.com +Subject: message 1 + +""") + self._handler.process(self._mlist, msg, {}) + del msg['subject'] + msg['subject'] = 'message 2' + self._handler.process(self._mlist, msg, {}) + # There are no digests already being sent, but the ant mailing list + # does have a digest mbox collecting messages. + items = get_queue_messages('digest') + self.assertEqual(len(items), 0) + mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') + self.assertGreater(os.path.getsize(mailbox_path), 0) + args = FakeArgs() + args.lists.extend(('ant.example.com', 'bee.example.com')) + stderr = StringIO() + with patch('mailman.commands.cli_send_digests.sys.stderr', stderr): + self._command.process(args) + self._runner.run() + # The warning was printed to stderr. + self.assertEqual(stderr.getvalue(), + 'No such list found: bee.example.com\n') + # But ant's digest was still prepared. + self.assertFalse(os.path.exists(mailbox_path)) + items = get_queue_messages('virgin') + self.assertEqual(len(items), 1) + digest_contents = str(items[0].msg) + self.assertIn('Subject: message 1', digest_contents) + self.assertIn('Subject: message 2', digest_contents) + + def test_send_digests_for_two_lists(self): + # Populate ant's digest. + msg = mfs("""\ +To: ant@example.com +From: anne@example.com +Subject: message 1 + +""") + self._handler.process(self._mlist, msg, {}) + del msg['subject'] + msg['subject'] = 'message 2' + self._handler.process(self._mlist, msg, {}) + # Create the second list. + bee = create_list('bee@example.com') + bee.digests_enabled = True + bee.digest_size_threshold = 100000 + bee.send_welcome_message = False + member = subscribe(bee, 'Bart') + member.preferences.delivery_mode = DeliveryMode.plaintext_digests + # Populate bee's digest. + msg = mfs("""\ +To: bee@example.com +From: bart@example.com +Subject: message 3 + +""") + self._handler.process(bee, msg, {}) + del msg['subject'] + msg['subject'] = 'message 4' + self._handler.process(bee, msg, {}) + # There are no digests for either list already being sent, but the + # mailing lists do have a digest mbox collecting messages. + ant_mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') + self.assertGreater(os.path.getsize(ant_mailbox_path), 0) + # Check bee's digest. + bee_mailbox_path = os.path.join(bee.data_path, 'digest.mmdf') + self.assertGreater(os.path.getsize(bee_mailbox_path), 0) + # Both. + items = get_queue_messages('digest') + self.assertEqual(len(items), 0) + # Process both list's digests. + args = FakeArgs() + args.lists.extend(('ant.example.com', 'bee@example.com')) + self._command.process(args) + self._runner.run() + # Now, neither list has a digest mbox and but there are plaintext + # digest in the outgoing queue for both. + self.assertFalse(os.path.exists(ant_mailbox_path)) + self.assertFalse(os.path.exists(bee_mailbox_path)) + items = get_queue_messages('virgin') + self.assertEqual(len(items), 2) + # Figure out which digest is going to ant and which to bee. + if items[0].msg['to'] == 'ant@example.com': + ant = items[0].msg + bee = items[1].msg + else: + assert items[0].msg['to'] == 'bee@example.com' + ant = items[1].msg + bee = items[0].msg + # Check ant's digest. + digest_contents = str(ant) + self.assertIn('Subject: message 1', digest_contents) + self.assertIn('Subject: message 2', digest_contents) + # Check bee's digest. + digest_contents = str(bee) + self.assertIn('Subject: message 3', digest_contents) + self.assertIn('Subject: message 4', digest_contents) diff --git a/src/mailman/database/alembic/versions/70af5a4e5790_digests.py b/src/mailman/database/alembic/versions/70af5a4e5790_digests.py index 5df46b584..2b53202d9 100644 --- a/src/mailman/database/alembic/versions/70af5a4e5790_digests.py +++ b/src/mailman/database/alembic/versions/70af5a4e5790_digests.py @@ -25,3 +25,5 @@ def downgrade(): batch_op.alter_column('digests_enabled', new_column_name='digestable') # The data for this column is lost, it's not used anyway. batch_op.add_column(sa.Column('nondigestable', sa.Boolean)) + +# XXX - move list.data_path diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index 2cc72862b..6e098a8c0 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -132,6 +132,9 @@ Other ``list_url`` or permalink. Given by Aurélien Bompard. * Large performance improvement in ``SubscriptionService.find_members()``. Given by Aurélien Bompard. + * Rework the digest machinery, and add a new `send-digests` subcommand, which + can be used from the command line or cron to immediately send out any + partially collected digests. 3.0.0 -- "Show Don't Tell" diff --git a/src/mailman/handlers/to_digest.py b/src/mailman/handlers/to_digest.py index 89e1acaea..d9c707e12 100644 --- a/src/mailman/handlers/to_digest.py +++ b/src/mailman/handlers/to_digest.py @@ -19,6 +19,8 @@ __all__ = [ 'ToDigest', + 'bump_digest_number_and_volume', + 'maybe_send_digest_now', ] @@ -29,8 +31,10 @@ from mailman.core.i18n import _ from mailman.email.message import Message from mailman.interfaces.digests import DigestFrequency from mailman.interfaces.handler import IHandler +from mailman.interfaces.listmanager import IListManager from mailman.utilities.datetime import now as right_now from mailman.utilities.mailbox import Mailbox +from zope.component import getUtility from zope.interface import implementer @@ -53,29 +57,7 @@ class ToDigest: # Lock the mailbox and append the message. with Mailbox(mailbox_path, create=True) as mbox: mbox.add(msg) - # Calculate the current size of the mailbox file. This will not tell - # us exactly how big the resulting MIME and rfc1153 digest will - # actually be, but it's the most easily available metric to decide - # whether the size threshold has been reached. - size = os.path.getsize(mailbox_path) - if size >= mlist.digest_size_threshold * 1024.0: - # The digest is ready to send. Because we don't want to hold up - # this process with crafting the digest, we're going to move the - # digest file to a safe place, then craft a fake message for the - # DigestRunner as a trigger for it to build and send the digest. - mailbox_dest = os.path.join( - mlist.data_path, - 'digest.{0.volume}.{0.next_digest_number}.mmdf'.format(mlist)) - volume = mlist.volume - digest_number = mlist.next_digest_number - bump_digest_number_and_volume(mlist) - os.rename(mailbox_path, mailbox_dest) - config.switchboards['digest'].enqueue( - Message(), - listid=mlist.list_id, - digest_path=mailbox_dest, - volume=volume, - digest_number=digest_number) + maybe_send_digest_now(mlist) @@ -117,3 +99,53 @@ def bump_digest_number_and_volume(mlist): # Just bump the digest number. mlist.next_digest_number += 1 mlist.digest_last_sent_at = now + + + +def maybe_send_digest_now(mlist=None, force=False): + """Send this mailing list's digest now. + + If there are any messages in this mailing list's digest, the + digest is sent immediately, regardless of whether the size + threshold has been met. When called through the subcommand + `mailman send_digest` the value of .digest_send_periodic is + consulted. + + :param mlist: The mailing list whose digest should be sent. If this is + None, all mailing lists with non-zero sized digests will have theirs + sent immediately. + :type mlist: IMailingList or None + :param force: Should the digest be sent even if the size threshold hasn't + been met? + :type force: boolean + """ + if mlist is None: + digestable_lists = getUtility(IListManager).mailing_lists + else: + digestable_lists = [mlist] + for mailing_list in digestable_lists: + mailbox_path = os.path.join(mailing_list.data_path, 'digest.mmdf') + # Calculate the current size of the mailbox file. This will not tell + # us exactly how big the resulting MIME and rfc1153 digest will + # actually be, but it's the most easily available metric to decide + # whether the size threshold has been reached. + size = os.path.getsize(mailbox_path) + if (size >= mlist.digest_size_threshold * 1024.0 or + (force and size > 0)): + # Send the digest. Because we don't want to hold up this process + # with crafting the digest, we're going to move the digest file to + # a safe place, then craft a fake message for the DigestRunner as + # a trigger for it to build and send the digest. + mailbox_dest = os.path.join( + mlist.data_path, + 'digest.{0.volume}.{0.next_digest_number}.mmdf'.format(mlist)) + volume = mlist.volume + digest_number = mlist.next_digest_number + bump_digest_number_and_volume(mlist) + os.rename(mailbox_path, mailbox_dest) + config.switchboards['digest'].enqueue( + Message(), + listid=mlist.list_id, + digest_path=mailbox_dest, + volume=volume, + digest_number=digest_number) diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index 75fdce327..2d2062d9c 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -316,7 +316,8 @@ class IMailingList(Interface): being collected.""") digest_send_periodic = Attribute( - "Should a digest be sent daily even when the size threshold isn't met?") + """Should a digest be sent by the `mailman send_digest` command even + when the size threshold hasn't yet been met?""") digest_volume_frequency = Attribute( """How often should a new digest volume be started?""") diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 9ee52629f..44dd9998b 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -261,7 +261,7 @@ class MailingList(Model): @property def data_path(self): """See `IMailingList`.""" - return os.path.join(config.LIST_DATA_DIR, self.fqdn_listname) + return os.path.join(config.LIST_DATA_DIR, self.list_id) # IMailingListAddresses diff --git a/src/mailman/runners/docs/digester.rst b/src/mailman/runners/docs/digester.rst index 9ff604340..00deb0d4a 100644 --- a/src/mailman/runners/docs/digester.rst +++ b/src/mailman/runners/docs/digester.rst @@ -56,7 +56,7 @@ But the message metadata has a reference to the digest file. >>> dump_msgdata(entry.msgdata) _parsemsg : False digest_number: 1 - digest_path : .../lists/test@example.com/digest.1.1.mmdf + digest_path : .../lists/test.example.com/digest.1.1.mmdf listid : test.example.com version : 3 volume : 1 |
