Add support for staggering outgoing emails
authorMagnus Hagander <magnus@hagander.net>
Thu, 29 Oct 2020 14:27:56 +0000 (15:27 +0100)
committerMagnus Hagander <magnus@hagander.net>
Thu, 29 Oct 2020 14:27:56 +0000 (15:27 +0100)
Sent email can be assigned a "stagger type", for which he system will
maintain a "last sent" information. When the email is sent, it will be
delayed to be at least "stagger" time after the last one sent of the
same type. If no email of this type has been sent before, the email is
of course sent immediately.

pgweb/mailqueue/admin.py
pgweb/mailqueue/management/commands/send_queued_mail.py
pgweb/mailqueue/migrations/0002_stagger_sending.py [new file with mode: 0644]
pgweb/mailqueue/models.py
pgweb/mailqueue/util.py

index 77d07ee12cbcbe063ad94bafd7a1966cac405970..99129923016d11956beb575771be4977f9d6b620 100644 (file)
@@ -9,6 +9,7 @@ from .models import QueuedMail
 class QueuedMailAdmin(admin.ModelAdmin):
     model = QueuedMail
     readonly_fields = ('parsed_content', )
+    list_display = ('pk', 'sender', 'receiver', 'sendat')
 
     def parsed_content(self, obj):
         # We only try to parse the *first* piece, because we assume
index 361b19f00634a98217fa8aecb27c86fd3a33cc3c..66b3df001beae54c98057be54028d298435460af 100755 (executable)
@@ -9,6 +9,7 @@ from django.core.management.base import BaseCommand, CommandError
 from django.db import connection
 from django.conf import settings
 
+import datetime
 import smtplib
 
 from pgweb.mailqueue.models import QueuedMail
@@ -26,7 +27,7 @@ class Command(BaseCommand):
         if not curs.fetchall()[0][0]:
             raise CommandError("Failed to get advisory lock, existing send_queued_mail process stuck?")
 
-        for m in QueuedMail.objects.all():
+        for m in QueuedMail.objects.filter(sendat__lte=datetime.datetime.now()):
             # Yes, we do a new connection for each run. Just because we can.
             # If it fails we'll throw an exception and just come back on the
             # next cron job. And local delivery should never fail...
diff --git a/pgweb/mailqueue/migrations/0002_stagger_sending.py b/pgweb/mailqueue/migrations/0002_stagger_sending.py
new file mode 100644 (file)
index 0000000..a892441
--- /dev/null
@@ -0,0 +1,28 @@
+# Generated by Django 2.2.11 on 2020-10-29 14:16
+
+import datetime
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('mailqueue', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='LastSent',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('type', models.CharField(max_length=10, unique=True)),
+                ('lastsent', models.DateTimeField()),
+            ],
+        ),
+        migrations.AddField(
+            model_name='queuedmail',
+            name='sendat',
+            field=models.DateTimeField(default=datetime.datetime(2000, 1, 1, 0, 0)),
+            preserve_default=False,
+        ),
+    ]
index becfb7363e0838e90ee61fbd07ce8ac55f5a33b8..9417276547e9643df44ceb7fa2b7bd71f8fcd3eb 100644 (file)
@@ -10,6 +10,12 @@ class QueuedMail(models.Model):
     # Flag if the message is "user generated", so we can treat those
     # separately from an antispam and delivery perspective.
     usergenerated = models.BooleanField(null=False, blank=False, default=False)
+    sendat = models.DateTimeField(null=False, blank=False)
 
     def __str__(self):
         return "%s: %s -> %s" % (self.pk, self.sender, self.receiver)
+
+
+class LastSent(models.Model):
+    type = models.CharField(max_length=10, null=False, blank=False, unique=True)
+    lastsent = models.DateTimeField(null=False, blank=False)
index 5d635098c84f32c329318acd430d1afd503028a6..2ef25b3d21eb73931d4eecfa40f93d25e6977be5 100644 (file)
@@ -1,3 +1,6 @@
+from django.db import transaction
+
+from datetime import datetime
 from email.mime.text import MIMEText
 from email.mime.multipart import MIMEMultipart
 from email.mime.nonmultipart import MIMENonMultipart
@@ -6,7 +9,7 @@ from email.utils import make_msgid
 from email import encoders, charset
 from email.header import Header
 
-from .models import QueuedMail
+from .models import QueuedMail, LastSent
 
 
 def _encoded_email_header(name, email):
@@ -22,7 +25,7 @@ _utf8_charset.header_encoding = charset.QP
 _utf8_charset.body_encoding = charset.QP
 
 
-def send_simple_mail(sender, receiver, subject, msgtxt, attachments=None, usergenerated=False, cc=None, replyto=None, sendername=None, receivername=None, messageid=None, suppress_auto_replies=True, is_auto_reply=False, htmlbody=None, headers={}):
+def send_simple_mail(sender, receiver, subject, msgtxt, attachments=None, usergenerated=False, cc=None, replyto=None, sendername=None, receivername=None, messageid=None, suppress_auto_replies=True, is_auto_reply=False, htmlbody=None, headers={}, staggertype=None, stagger=None):
     # attachment format, each is a tuple of (name, mimetype,contents)
     # content should be *binary* and not base64 encoded, since we need to
     # use the base64 routines from the email library to get a properly
@@ -79,10 +82,20 @@ def send_simple_mail(sender, receiver, subject, msgtxt, attachments=None, userge
             encoders.encode_base64(part)
             msg.attach(part)
 
-    # Just write it to the queue, so it will be transactionally rolled back
-    QueuedMail(sender=sender, receiver=receiver, fullmsg=msg.as_string(), usergenerated=usergenerated).save()
-    if cc:
-        # Write a second copy for the cc, wihch will be delivered
-        # directly to the recipient. (The sender doesn't parse the
-        # message content to extract cc fields).
-        QueuedMail(sender=sender, receiver=cc, fullmsg=msg.as_string(), usergenerated=usergenerated).save()
+    with transaction.atomic():
+        if staggertype and stagger:
+            # Don't send a second one too close after another one of this class.
+            ls, created = LastSent.objects.get_or_create(type=staggertype, defaults={'lastsent': datetime.now()})
+
+            sendat = ls.lastsent = ls.lastsent + stagger
+            ls.save(update_fields=['lastsent'])
+        else:
+            sendat = datetime.now()
+
+        # Just write it to the queue, so it will be transactionally rolled back
+        QueuedMail(sender=sender, receiver=receiver, fullmsg=msg.as_string(), usergenerated=usergenerated, sendat=sendat).save()
+        if cc:
+            # Write a second copy for the cc, wihch will be delivered
+            # directly to the recipient. (The sender doesn't parse the
+            # message content to extract cc fields).
+            QueuedMail(sender=sender, receiver=cc, fullmsg=msg.as_string(), usergenerated=usergenerated, sendat=sendat).save()