From adf5eb2a3443e05275b1b6c882042c9a1afd34d7 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Thu, 29 Oct 2020 15:27:56 +0100 Subject: [PATCH] Add support for staggering outgoing emails 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 | 1 + .../management/commands/send_queued_mail.py | 3 +- .../migrations/0002_stagger_sending.py | 28 +++++++++++++++++ pgweb/mailqueue/models.py | 6 ++++ pgweb/mailqueue/util.py | 31 +++++++++++++------ 5 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 pgweb/mailqueue/migrations/0002_stagger_sending.py diff --git a/pgweb/mailqueue/admin.py b/pgweb/mailqueue/admin.py index 77d07ee1..99129923 100644 --- a/pgweb/mailqueue/admin.py +++ b/pgweb/mailqueue/admin.py @@ -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 diff --git a/pgweb/mailqueue/management/commands/send_queued_mail.py b/pgweb/mailqueue/management/commands/send_queued_mail.py index 361b19f0..66b3df00 100755 --- a/pgweb/mailqueue/management/commands/send_queued_mail.py +++ b/pgweb/mailqueue/management/commands/send_queued_mail.py @@ -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 index 00000000..a8924411 --- /dev/null +++ b/pgweb/mailqueue/migrations/0002_stagger_sending.py @@ -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, + ), + ] diff --git a/pgweb/mailqueue/models.py b/pgweb/mailqueue/models.py index becfb736..94172765 100644 --- a/pgweb/mailqueue/models.py +++ b/pgweb/mailqueue/models.py @@ -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) diff --git a/pgweb/mailqueue/util.py b/pgweb/mailqueue/util.py index 5d635098..2ef25b3d 100644 --- a/pgweb/mailqueue/util.py +++ b/pgweb/mailqueue/util.py @@ -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() -- 2.39.5