Commit bb45766f authored by Barry Warsaw's avatar Barry Warsaw

Add a send-digests subcommand to send list digests right now.

* Add a `mailman send-digests` subcommand which replaces the functionality of
  the MM2.1 senddigests.py cronjob.

* Use mlist.data_path where appropriate instead of crafting it from
  config.LIST_DATA_DIR.  This makes it more consistent to switch to using the
  list-id as the data subdirectory.

* Refactor the to_digest handler so that we can implement
  maybe_send_digest_now() for the internal API.

* Fix some typos in subcommand --help summaries.
parent d7bc81a6
# Copyright (C) 1998-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/>.
import os
import sys
import optparse
from mailman import MailList
from mailman.core.i18n import _
from mailman.initialize import initialize
from mailman.version import MAILMAN_VERSION
# Work around known problems with some RedHat cron daemons
import signal
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
def parseargs():
parser = optparse.OptionParser(version=MAILMAN_VERSION,
usage=_("""\
%prog [options]
Dispatch digests for lists w/pending messages and digest_send_periodic
set."""))
parser.add_option('-l', '--listname',
type='string', default=[], action='append',
dest='listnames', help=_("""\
Send the digest for the given list only, otherwise the digests for all
lists are sent out. Multiple -l options may be given."""))
parser.add_option('-C', '--config',
help=_('Alternative configuration file to use'))
opts, args = parser.parse_args()
if args:
parser.print_help()
print >> sys.stderr, _('Unexpected arguments')
sys.exit(1)
return opts, args, parser
def main():
opts, args, parser = parseargs()
initialize(opts.config)
for listname in set(opts.listnames or config.list_manager.names):
mlist = MailList.MailList(listname, lock=False)
if mlist.digest_send_periodic:
mlist.Lock()
try:
try:
mlist.send_digest_now()
mlist.Save()
# We are unable to predict what exception may occur in digest
# processing and we don't want to lose the other digests, so
# we catch everything.
except Exception as errmsg:
print >> sys.stderr, \
'List: %s: problem processing %s:\n%s' % \
(listname,
os.path.join(mlist.data_path, 'digest.mbox'),
errmsg)
finally:
mlist.Unlock()
if __name__ == '__main__':
main()
......@@ -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
......@@ -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, '[email protected]')
create_list, '[email protected]')
def test_remove_list_error(self):
# An error occurs while deleting the list's data directory.
mlist = create_list('[email protected]')
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)
......@@ -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')
......
......@@ -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'
......
......@@ -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'
......
# 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)
This diff is collapsed.
......@@ -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
......@@ -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"
......
......@@ -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)
......@@ -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?""")
......
......@@ -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
......
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment