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.
This commit is contained in:
Magnus Hagander
2020-10-29 15:27:56 +01:00
parent 25180ee829
commit adf5eb2a34
5 changed files with 59 additions and 10 deletions

View 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

View File

@ -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...

View File

@ -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,
),
]

View 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)

View 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()