summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2015-12-22 17:05:17 -0500
committerBarry Warsaw2015-12-22 17:05:17 -0500
commit85521fac12e3c67e10fbc21345e4694f3ab22fe0 (patch)
treedbb9db4e36698b6c8f78954548cd17a95607352b /src
parent8d74a4f3b19928a9c311096aa5b39893ae3fe701 (diff)
downloadmailman-85521fac12e3c67e10fbc21345e4694f3ab22fe0.tar.gz
mailman-85521fac12e3c67e10fbc21345e4694f3ab22fe0.tar.zst
mailman-85521fac12e3c67e10fbc21345e4694f3ab22fe0.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/digests.py2
-rw-r--r--src/mailman/commands/tests/test_digests.py2
-rw-r--r--src/mailman/docs/NEWS.rst10
-rw-r--r--src/mailman/rest/docs/lists.rst109
-rw-r--r--src/mailman/rest/lists.py47
-rw-r--r--src/mailman/rest/tests/test_lists.py77
6 files changed, 238 insertions, 9 deletions
diff --git a/src/mailman/app/digests.py b/src/mailman/app/digests.py
index 2190fdf98..6ef37a391 100644
--- a/src/mailman/app/digests.py
+++ b/src/mailman/app/digests.py
@@ -73,7 +73,7 @@ def bump_digest_number_and_volume(mlist):
-def maybe_send_digest_now(mlist, force=False):
+def maybe_send_digest_now(mlist, *, force=False):
"""Send this mailing list's digest now.
If there are any messages in this mailing list's digest, the
diff --git a/src/mailman/commands/tests/test_digests.py b/src/mailman/commands/tests/test_digests.py
index 607949f6e..2106f5089 100644
--- a/src/mailman/commands/tests/test_digests.py
+++ b/src/mailman/commands/tests/test_digests.py
@@ -377,7 +377,7 @@ Subject: message 3
items = get_queue_messages('virgin')
self.assertEqual(len(items), 0)
- def test_bump_after_send(self):
+ def test_bump_before_send(self):
self._mlist.digest_volume_frequency = DigestFrequency.monthly
self._mlist.volume = 7
self._mlist.next_digest_number = 4
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 3e8870589..e8ac58c68 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -123,6 +123,12 @@ REST
* Expose ``digest_send_periodic``, ``digest_volume_frequency``, and
``digests_enabled`` (renamed from ``digestable``) to the REST API.
(Closes: #159)
+ * Expose the "bump digest" and "send digest" functionality though the REST
+ API via the ``<api>/lists/<list-id>/digest`` end-point. GETting this
+ resource returns the ``next_digest_number`` and ``volume`` as the same
+ values accessible through the list's configuraiton resource. POSTing to
+ the resource with either ``send=True``, ``bump=True``, or both invokes the
+ given action.
Other
-----
@@ -132,9 +138,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
+ * Rework the digest machinery, and add a new ``digests`` subcommand, which
can be used from the command line or cron to immediately send out any
- partially collected digests.
+ partially collected digests, or bump the digest and volume numbers.
* The mailing list "data directory" has been renamed. Instead of using the
fqdn listname, the subdirectory inside ``[paths]list_data_dir`` now uses
the List-ID.
diff --git a/src/mailman/rest/docs/lists.rst b/src/mailman/rest/docs/lists.rst
index 1006a8f0c..49f2d454e 100644
--- a/src/mailman/rest/docs/lists.rst
+++ b/src/mailman/rest/docs/lists.rst
@@ -288,3 +288,112 @@ You can change the state of a subset of the list archivers.
mail-archive: False
mhonarc: False
prototype: False
+
+
+List digests
+============
+
+A list collects messages and prepares a digest which can be periodically sent
+to all members who elect to receive digests. Digests are usually sent
+whenever their size has reached a threshold, but you can force a digest to be
+sent immediately via the REST API.
+
+Let's create a mailing list that has a digest recipient.
+
+ >>> from mailman.interfaces.member import DeliveryMode
+ >>> from mailman.testing.helpers import subscribe
+ >>> emu = create_list('emu@example.com')
+ >>> emu.send_welcome_message = False
+ >>> anne = subscribe(emu, 'Anne')
+ >>> anne.preferences.delivery_mode = DeliveryMode.plaintext_digests
+
+The mailing list has a fairly high size threshold so that sending a single
+message through the list won't trigger an automatic digest. The threshold is
+the maximum digest size in kibibytes (1024 bytes).
+
+ >>> emu.digest_size_threshold = 100
+ >>> transaction.commit()
+
+We send a message through the mailing list to start collecting for a digest.
+
+ >>> from mailman.runners.digest import DigestRunner
+ >>> from mailman.testing.helpers import make_testable_runner
+ >>> msg = message_from_string("""\
+ ... From: anne@example.com
+ ... To: emu@example.com
+ ... Subject: Message #1
+ ...
+ ... """)
+ >>> config.handlers['to-digest'].process(emu, msg, {})
+ >>> runner = make_testable_runner(DigestRunner, 'digest')
+ >>> runner.run()
+
+No digest was sent because it didn't reach the size threshold.
+
+ >>> from mailman.testing.helpers import get_queue_messages
+ >>> len(get_queue_messages('virgin'))
+ 0
+
+By POSTing to the list's digest end-point with the ``send`` parameter set, we
+can force the digest to be sent.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest', {
+ ... 'send': True,
+ ... })
+ content-length: 0
+ date: ...
+
+Once the runner does its thing, the digest message will be sent.
+
+ >>> runner.run()
+ >>> items = get_queue_messages('virgin')
+ >>> len(items)
+ 1
+ >>> print(items[0].msg)
+ From: emu-request@example.com
+ Subject: Emu Digest, Vol 1, Issue 1
+ To: emu@example.com
+ ...
+ From: anne@example.com
+ Subject: Message #1
+ To: emu@example.com
+ ...
+ End of Emu Digest, Vol 1, Issue 1
+ *********************************
+ <BLANKLINE>
+
+Digests also have a volume number and digest number which can be bumped, also
+by POSTing to the REST API. Bumping the digest for this list will increment
+the digest volume and reset the digest number to 1. We have to fake that the
+last digest was sent a couple of days ago.
+
+ >>> from datetime import timedelta
+ >>> from mailman.interfaces.digests import DigestFrequency
+ >>> emu.digest_volume_frequency = DigestFrequency.daily
+ >>> emu.digest_last_sent_at -= timedelta(days=2)
+ >>> transaction.commit()
+
+Before bumping, we can get the next digest volume and number. Doing a GET on
+the digest resource is just a shorthand for getting some interesting
+information about the digest. Note that ``volume`` and ``next_digest_number``
+can also be retrieved from the list's configuration resource.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest')
+ http_etag: ...
+ next_digest_number: 2
+ volume: 1
+
+Let's bump the digest.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest', {
+ ... 'bump': True,
+ ... })
+ content-length: 0
+ date: ...
+
+And now the next digest to be sent will have a new volume number.
+
+ >>> dump_json('http://localhost:9001/3.0/lists/emu.example.com/digest')
+ http_etag: ...
+ next_digest_number: 1
+ volume: 2
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 467e71508..e73cadf08 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -28,6 +28,8 @@ __all__ = [
from lazr.config import as_boolean
+from mailman.app.digests import (
+ bump_digest_number_and_volume, maybe_send_digest_now)
from mailman.app.lifecycle import create_list, remove_list
from mailman.config import config
from mailman.interfaces.domain import BadDomainSpecificationError
@@ -39,8 +41,8 @@ from mailman.interfaces.styles import IStyleManager
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.rest.listconf import ListConfiguration
from mailman.rest.helpers import (
- CollectionMixin, GetterSetter, NotFound, bad_request, child, created,
- etag, no_content, not_found, okay)
+ CollectionMixin, GetterSetter, NotFound, accepted, bad_request, child,
+ created, etag, no_content, not_found, okay)
from mailman.rest.members import AMember, MemberCollection
from mailman.rest.post_moderation import HeldMessages
from mailman.rest.sub_moderation import SubscriptionRequests
@@ -192,6 +194,12 @@ class AList(_ListBase):
return NotFound(), []
return ListArchivers(self._mlist)
+ @child()
+ def digest(self, request, segments):
+ if self._mlist is None:
+ return NotFound(), []
+ return ListDigest(self._mlist)
+
class AllLists(_ListBase):
@@ -310,6 +318,41 @@ class ListArchivers:
+class ListDigest:
+ """Simple resource representing actions on a list's digest."""
+
+ def __init__(self, mlist):
+ self._mlist = mlist
+
+ def on_get(self, request, response):
+ resource = dict(
+ next_digest_number=self._mlist.next_digest_number,
+ volume=self._mlist.volume,
+ )
+ okay(response, etag(resource))
+
+ def on_post(self, request, response):
+ try:
+ validator = Validator(
+ send=as_boolean,
+ bump=as_boolean,
+ _optional=('send', 'bump'))
+ values = validator(request)
+ except ValueError as error:
+ bad_request(response, str(error))
+ return
+ if len(values) == 0:
+ # There's nothing to do, but that's okay.
+ okay(response)
+ return
+ if values.get('bump', False):
+ bump_digest_number_and_volume(self._mlist)
+ if values.get('send', False):
+ maybe_send_digest_now(self._mlist, force=True)
+ accepted(response)
+
+
+
class Styles:
"""Simple resource representing all list styles."""
diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py
index 546e431d3..4782bc011 100644
--- a/src/mailman/rest/tests/test_lists.py
+++ b/src/mailman/rest/tests/test_lists.py
@@ -19,6 +19,7 @@
__all__ = [
'TestListArchivers',
+ 'TestListDigests',
'TestListPagination',
'TestLists',
'TestListsMissing',
@@ -27,15 +28,22 @@ __all__ = [
import unittest
+from datetime import timedelta
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.database.transaction import transaction
+from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.mailinglist import IAcceptableAliasSet
+from mailman.interfaces.member import DeliveryMode
from mailman.interfaces.usermanager import IUserManager
from mailman.model.mailinglist import AcceptableAlias
-from mailman.testing.helpers import call_api
+from mailman.runners.digest import DigestRunner
+from mailman.testing.helpers import (
+ call_api, get_queue_messages, make_testable_runner,
+ specialized_message_from_string as mfs)
from mailman.testing.layers import RESTLayer
+from mailman.utilities.datetime import now as right_now
from urllib.error import HTTPError
from zope.component import getUtility
@@ -318,7 +326,7 @@ class TestListPagination(unittest.TestCase):
def test_zeroth_page(self):
# Page numbers start at one.
with self.assertRaises(HTTPError) as cm:
- resource, response = call_api(
+ call_api(
'http://localhost:9001/3.0/domains/example.com/lists'
'?count=1&page=0')
self.assertEqual(cm.exception.code, 400)
@@ -326,7 +334,7 @@ class TestListPagination(unittest.TestCase):
def test_negative_page(self):
# Negative pages are not allowed.
with self.assertRaises(HTTPError) as cm:
- resource, response = call_api(
+ call_api(
'http://localhost:9001/3.0/domains/example.com/lists'
'?count=1&page=-1')
self.assertEqual(cm.exception.code, 400)
@@ -340,3 +348,66 @@ class TestListPagination(unittest.TestCase):
self.assertEqual(resource['total_size'], 6)
self.assertEqual(resource['start'], 6)
self.assertNotIn('entries', resource)
+
+
+
+class TestListDigests(unittest.TestCase):
+ """Test /lists/<list-id>/digest"""
+
+ layer = RESTLayer
+
+ def setUp(self):
+ with transaction():
+ self._mlist = create_list('ant@example.com')
+ self._mlist.send_welcome_message = False
+ anne = getUtility(IUserManager).create_address('anne@example.com')
+ self._mlist.subscribe(anne)
+ anne.preferences.delivery_mode = DeliveryMode.plaintext_digests
+
+ def test_post_nothing_to_do(self):
+ resource, response = call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/digest', {})
+ self.assertEqual(response.status, 200)
+
+ def test_post_something_to_do(self):
+ resource, response = call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/digest', dict(
+ bump=True))
+ self.assertEqual(response.status, 202)
+
+ def test_post_bad_request(self):
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/digest', dict(
+ bogus=True))
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason, b'Unexpected parameters: bogus')
+
+ def test_bump_before_send(self):
+ with transaction():
+ self._mlist.digest_volume_frequency = DigestFrequency.monthly
+ self._mlist.volume = 7
+ self._mlist.next_digest_number = 4
+ self._mlist.digest_last_sent_at = right_now() + timedelta(
+ days=-32)
+ msg = mfs("""\
+To: ant@example.com
+From: anne@example.com
+Subject: message 1
+
+""")
+ config.handlers['to-digest'].process(self._mlist, msg, {})
+ resource, response = call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/digest', dict(
+ send=True,
+ bump=True))
+ self.assertEqual(response.status, 202)
+ make_testable_runner(DigestRunner, 'digest').run()
+ # The volume is 8 and the digest number is 2 because a digest was sent
+ # after the volume/number was bumped.
+ self.assertEqual(self._mlist.volume, 8)
+ self.assertEqual(self._mlist.next_digest_number, 2)
+ self.assertEqual(self._mlist.digest_last_sent_at, right_now())
+ items = get_queue_messages('virgin')
+ self.assertEqual(len(items), 1)
+ self.assertEqual(items[0].msg['subject'], 'Ant Digest, Vol 8, Issue 1')