diff --git a/CHANGES.rst b/CHANGES.rst index df96fbf..3b70e1b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ Change log * Add fix for email address that is a NoneType * Stop testing on unsupported Python (<3.7) and Django (<2.2) versions * Start testing on Python 3.11 and Django 4.1/4.2 +* Add setting to limit the amount of retries for deferred messages + (``MAILER_EMAIL_MAX_RETRIES``), defaults to ``None`` (unlimited) + (See Issue `#161 `_) 2.2 - 2022-03-11 ---------------- diff --git a/docs/usage.rst b/docs/usage.rst index 1b7d9af..721ea03 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -185,6 +185,12 @@ value more suitable for you. This value, which defaults to ``None``, will be pas `Django's bulk_create method `_ as the ``batch_size`` parameter. +To limit the amount of times a deferred message is retried, you can set +``MAILER_EMAIL_MAX_RETRIES`` to an integer value. The default is ``None``, which means +that the message will be retried indefinitely. If you set this to a value of ``0``, +the message will not be retried at all, any number greater than ``0`` will be the +maximum number of retries (excluding the initial attempt). + Using the DontSendEntry table ============================= diff --git a/src/mailer/admin.py b/src/mailer/admin.py index fb1b1a1..c13dd59 100644 --- a/src/mailer/admin.py +++ b/src/mailer/admin.py @@ -22,7 +22,7 @@ def plain_text_body(self, instance): class MessageAdmin(MessageAdminMixin, admin.ModelAdmin): - list_display = ["id", show_to, "subject", "when_added", "priority"] + list_display = ["id", show_to, "subject", "when_added", "priority", "retry_count"] readonly_fields = ['plain_text_body'] date_hierarchy = "when_added" diff --git a/src/mailer/migrations/0006_message_retry_count.py b/src/mailer/migrations/0006_message_retry_count.py new file mode 100644 index 0000000..12af9ec --- /dev/null +++ b/src/mailer/migrations/0006_message_retry_count.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-05-25 13:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0005_id_bigautofield'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='retry_count', + field=models.IntegerField(default=0), + ), + ] diff --git a/src/mailer/models.py b/src/mailer/models.py index a57d63a..5469db0 100644 --- a/src/mailer/models.py +++ b/src/mailer/models.py @@ -7,6 +7,8 @@ import six +from django.conf import settings + try: from django.utils.encoding import python_2_unicode_compatible except ImportError: @@ -89,7 +91,10 @@ def deferred(self): return self.filter(priority=PRIORITY_DEFERRED) def retry_deferred(self, new_priority=PRIORITY_MEDIUM): - return self.deferred().update(priority=new_priority) + qs = self.deferred() + if getattr(settings, 'MAILER_EMAIL_MAX_RETRIES', None) is not None: + qs = qs.filter(retry_count__lt=settings.MAILER_EMAIL_MAX_RETRIES) + return qs.update(priority=new_priority, retry_count=models.F('retry_count') + 1) base64_encode = base64.encodebytes if hasattr(base64, 'encodebytes') else base64.encodestring @@ -133,6 +138,7 @@ class Message(BigAutoModel): message_data = models.TextField() when_added = models.DateTimeField(default=datetime_now) priority = models.PositiveSmallIntegerField(choices=PRIORITIES, default=PRIORITY_MEDIUM) + retry_count = models.IntegerField(default=0) objects = MessageManager() diff --git a/tests/test_mailer.py b/tests/test_mailer.py index 60ab2a9..e887288 100644 --- a/tests/test_mailer.py +++ b/tests/test_mailer.py @@ -88,6 +88,54 @@ def test_retry_deferred(self): engine.send_all() self.assertEqual(len(mail.outbox), 1) self.assertEqual(Message.objects.count(), 0) + self.assertEqual(MessageLog.objects.count(), 2) + + def test_max_retry_deferred(self): + with self.settings(MAILER_EMAIL_BACKEND="tests.FailingMailerEmailBackend", MAILER_EMAIL_MAX_RETRIES=2): # noqa + mailer.send_mail("Subject", "Body", "sender@examle.com", ["recipient@example.com"]) + engine.send_all() + # First try fails, message is deferred + self.assertEqual(Message.objects.count(), 1) + self.assertEqual(MessageLog.objects.count(), 1) + self.assertEqual(Message.objects.deferred().count(), 1) + for n in range(4): + with self.subTest(tries=n): + # Re-que the deferred message + Message.objects.retry_deferred() + # Retry count is updated, unless the max is reached + self.assertEqual( + Message.objects.values_list('retry_count', flat=True).get(), + n + 1 if n < 2 else 2, + msg="Expected retry_count to be at most 2, got %d" % n + ) + # Send all messages + engine.send_all() + # Message is retried (log entry is added), unless the max is reached + self.assertEqual( + MessageLog.objects.count(), 1 + (n + 1) if n < 2 else 3, + msg="Expected at most 3 attempts (log entries), got %d" % n + ) + # Message remain deferred + self.assertEqual(Message.objects.deferred().count(), 1) + + def test_max_retry_zero(self): + with self.settings(MAILER_EMAIL_BACKEND="tests.FailingMailerEmailBackend", MAILER_EMAIL_MAX_RETRIES=0): # noqa + mailer.send_mail("Subject", "Body", "sender@examle.com", ["recipient@example.com"]) + engine.send_all() + # First try fails, message is deferred + self.assertEqual(Message.objects.count(), 1) + self.assertEqual(MessageLog.objects.count(), 1) + self.assertEqual(Message.objects.deferred().count(), 1) + # Re-que the deferred message + Message.objects.retry_deferred() + # Retry count remains at 0, the message is not retried + self.assertEqual(Message.objects.values_list('retry_count', flat=True).get(), 0) + # Send all messages + engine.send_all() + # Message is not retried (log entry is not added) + self.assertEqual(MessageLog.objects.count(), 1) + # Message remain deferred + self.assertEqual(Message.objects.deferred().count(), 1) def test_purge_old_entries(self):