mirror of
https://github.com/pretix/pretix.git
synced 2026-05-21 17:54:08 +00:00
tests, comments
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
# Generated by Django 4.2.17 on 2025-09-04 12:35
|
# Generated by Django 4.2.26 on 2026-01-22 13:44
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.mail
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
@@ -24,6 +26,8 @@ class Migration(migrations.Migration):
|
|||||||
("status", models.CharField(default="queued", max_length=200)),
|
("status", models.CharField(default="queued", max_length=200)),
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
("sent", models.DateTimeField(blank=True, null=True)),
|
("sent", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("inflight_since", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("retry_after", models.DateTimeField(blank=True, null=True)),
|
||||||
("error", models.TextField(null=True)),
|
("error", models.TextField(null=True)),
|
||||||
("error_detail", models.TextField(null=True)),
|
("error_detail", models.TextField(null=True)),
|
||||||
("subject", models.TextField()),
|
("subject", models.TextField()),
|
||||||
@@ -42,7 +46,7 @@ class Migration(migrations.Migration):
|
|||||||
"customer",
|
"customer",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||||
related_name="outgoing_mails",
|
related_name="outgoing_mails",
|
||||||
to="pretixbase.customer",
|
to="pretixbase.customer",
|
||||||
),
|
),
|
||||||
@@ -51,7 +55,7 @@ class Migration(migrations.Migration):
|
|||||||
"event",
|
"event",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||||
related_name="outgoing_mails",
|
related_name="outgoing_mails",
|
||||||
to="pretixbase.event",
|
to="pretixbase.event",
|
||||||
),
|
),
|
||||||
@@ -60,7 +64,7 @@ class Migration(migrations.Migration):
|
|||||||
"order",
|
"order",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||||
related_name="outgoing_mails",
|
related_name="outgoing_mails",
|
||||||
to="pretixbase.order",
|
to="pretixbase.order",
|
||||||
),
|
),
|
||||||
@@ -69,7 +73,7 @@ class Migration(migrations.Migration):
|
|||||||
"orderposition",
|
"orderposition",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||||
related_name="outgoing_mails",
|
related_name="outgoing_mails",
|
||||||
to="pretixbase.orderposition",
|
to="pretixbase.orderposition",
|
||||||
),
|
),
|
||||||
@@ -99,7 +103,7 @@ class Migration(migrations.Migration):
|
|||||||
"user",
|
"user",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="outgoing_mails",
|
related_name="outgoing_mails",
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
|
def CASCADE_IF_QUEUED(collector, field, sub_objs, using):
|
||||||
|
# If the email is still queued and the thing it is related to vanishes, the email can vanish as well
|
||||||
|
print(sub_objs)
|
||||||
|
cascade_objs = [
|
||||||
|
o for o in sub_objs if o.status == OutgoingMail.STATUS_QUEUED
|
||||||
|
]
|
||||||
|
if cascade_objs:
|
||||||
|
models.CASCADE(collector, field, cascade_objs, using)
|
||||||
|
|
||||||
|
# In all other cases, set to NULL to keep the email on record
|
||||||
|
models.SET_NULL(collector, field, [o for o in sub_objs if o not in cascade_objs], using)
|
||||||
|
|
||||||
|
|
||||||
class OutgoingMail(models.Model):
|
class OutgoingMail(models.Model):
|
||||||
STATUS_QUEUED = "queued"
|
STATUS_QUEUED = "queued"
|
||||||
STATUS_INFLIGHT = "inflight"
|
STATUS_INFLIGHT = "inflight"
|
||||||
@@ -42,9 +55,23 @@ class OutgoingMail(models.Model):
|
|||||||
status = models.CharField(max_length=200, choices=STATUS_CHOICES, default=STATUS_QUEUED)
|
status = models.CharField(max_length=200, choices=STATUS_CHOICES, default=STATUS_QUEUED)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
sent = models.DateTimeField(null=True, blank=True)
|
sent = models.DateTimeField(null=True, blank=True)
|
||||||
|
inflight_since = models.DateTimeField(null=True, blank=True)
|
||||||
|
retry_after = models.DateTimeField(null=True, blank=True)
|
||||||
error = models.TextField(null=True, blank=True)
|
error = models.TextField(null=True, blank=True)
|
||||||
error_detail = models.TextField(null=True, blank=True)
|
error_detail = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
# There is a conflict here between the different purposes of the model. As a system administrator,
|
||||||
|
# one wants *all* emails to be persisted as long as possible to debug issues. This means that if
|
||||||
|
# e.g. the event or order is deleted, we want SET_NULL behavior. However, in that case, the email
|
||||||
|
# would be an "orphan" forever and there's no way to remove the personal information.
|
||||||
|
# We try to find a middle-ground with the following behaviour:
|
||||||
|
# - The email is always deleted if the entire organize or user is deleted
|
||||||
|
# - The email is always deleted if it has not yet been sent
|
||||||
|
# - The email is kept in all other cases
|
||||||
|
# This is only an acceptable trade-off since emails are stored for a short period only, and because
|
||||||
|
# orders and customers are never deleted during normal operation. If we ever make this a long-term
|
||||||
|
# storage / email archive, we'd need to find another way to make sure personal information is removed
|
||||||
|
# if personal information of orders etc is removed.
|
||||||
organizer = models.ForeignKey(
|
organizer = models.ForeignKey(
|
||||||
'pretixbase.Organizer',
|
'pretixbase.Organizer',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@@ -53,31 +80,31 @@ class OutgoingMail(models.Model):
|
|||||||
)
|
)
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
'pretixbase.Event',
|
'pretixbase.Event',
|
||||||
on_delete=models.SET_NULL, # todo think, only for non-queued!
|
on_delete=CASCADE_IF_QUEUED,
|
||||||
related_name='outgoing_mails',
|
related_name='outgoing_mails',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
)
|
)
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
'pretixbase.Order',
|
'pretixbase.Order',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=CASCADE_IF_QUEUED,
|
||||||
related_name='outgoing_mails',
|
related_name='outgoing_mails',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
)
|
)
|
||||||
orderposition = models.ForeignKey(
|
orderposition = models.ForeignKey(
|
||||||
'pretixbase.OrderPosition',
|
'pretixbase.OrderPosition',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=CASCADE_IF_QUEUED,
|
||||||
related_name='outgoing_mails',
|
related_name='outgoing_mails',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
)
|
)
|
||||||
customer = models.ForeignKey(
|
customer = models.ForeignKey(
|
||||||
'pretixbase.Customer',
|
'pretixbase.Customer',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=CASCADE_IF_QUEUED,
|
||||||
related_name='outgoing_mails',
|
related_name='outgoing_mails',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
'pretixbase.User',
|
'pretixbase.User',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.CASCADE,
|
||||||
related_name='outgoing_mails',
|
related_name='outgoing_mails',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
)
|
)
|
||||||
@@ -91,17 +118,25 @@ class OutgoingMail(models.Model):
|
|||||||
cc = models.JSONField(default=list)
|
cc = models.JSONField(default=list)
|
||||||
bcc = models.JSONField(default=list)
|
bcc = models.JSONField(default=list)
|
||||||
|
|
||||||
|
# We don't store the actual invoices, tickets or calendar invites, so if the email is re-sent at a later time, a
|
||||||
|
# newer version of the files might be used. We accept that risk to save on storage and also because the new
|
||||||
|
# version might actually be more useful.
|
||||||
should_attach_invoices = models.ManyToManyField(
|
should_attach_invoices = models.ManyToManyField(
|
||||||
'pretixbase.Invoice',
|
'pretixbase.Invoice',
|
||||||
related_name='outgoing_mails'
|
related_name='outgoing_mails'
|
||||||
)
|
)
|
||||||
should_attach_tickets = models.BooleanField(default=False)
|
should_attach_tickets = models.BooleanField(default=False)
|
||||||
should_attach_ical = models.BooleanField(default=False)
|
should_attach_ical = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# We need to make sure cached files are kept as l
|
||||||
should_attach_cached_files = models.ManyToManyField(
|
should_attach_cached_files = models.ManyToManyField(
|
||||||
'pretixbase.CachedFile',
|
'pretixbase.CachedFile',
|
||||||
related_name='outgoing_mails',
|
related_name='outgoing_mails',
|
||||||
) # todo: prevent deletion?
|
) # todo: prevent deletion?
|
||||||
should_attach_other_files = models.JSONField(default=list) # todo_ prevent deletion?
|
|
||||||
|
# This is used to send files stored in settings. In most cases, these aren't short-lived and should still be there
|
||||||
|
# if the email is sent. Otherwise, they will be skipped. We accept that risk.
|
||||||
|
should_attach_other_files = models.JSONField(default=list)
|
||||||
|
|
||||||
actual_attachments = models.JSONField(default=list)
|
actual_attachments = models.JSONField(default=list)
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
import warnings
|
import warnings
|
||||||
|
from datetime import timedelta
|
||||||
from email.mime.image import MIMEImage
|
from email.mime.image import MIMEImage
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
from typing import Any, Dict, Optional, Sequence, Union
|
from typing import Any, Dict, Optional, Sequence, Union
|
||||||
@@ -354,15 +355,20 @@ class CustomEmail(EmailMultiAlternatives):
|
|||||||
@app.task(base=TransactionAwareTask, bind=True, acks_late=True)
|
@app.task(base=TransactionAwareTask, bind=True, acks_late=True)
|
||||||
def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
|
try:
|
||||||
|
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
|
||||||
|
except OutgoingMail.DoesNotExist:
|
||||||
|
logger.info(f"Ignoring job for non existing email {outgoing_mail}")
|
||||||
|
return False
|
||||||
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
|
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
|
||||||
logger.info("Ignoring job for inflight email")
|
logger.info(f"Ignoring job for inflight email {outgoing_mail.pk}")
|
||||||
return False
|
return False
|
||||||
elif outgoing_mail.status in (OutgoingMail.STATUS_SENT, OutgoingMail.STATUS_FAILED):
|
elif outgoing_mail.status in (OutgoingMail.STATUS_SENT, OutgoingMail.STATUS_FAILED):
|
||||||
logger.info(f"Ignoring job for email in final state {outgoing_mail.status}")
|
logger.info(f"Ignoring job for email {outgoing_mail.pk} in final state {outgoing_mail.status}")
|
||||||
return False
|
return False
|
||||||
outgoing_mail.status = OutgoingMail.STATUS_INFLIGHT
|
outgoing_mail.status = OutgoingMail.STATUS_INFLIGHT
|
||||||
outgoing_mail.save(update_fields=["status"])
|
outgoing_mail.inflight_since = now()
|
||||||
|
outgoing_mail.save(update_fields=["status", "inflight_since"])
|
||||||
|
|
||||||
email = CustomEmail(
|
email = CustomEmail(
|
||||||
subject=outgoing_mail.subject,
|
subject=outgoing_mail.subject,
|
||||||
@@ -405,7 +411,7 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
except MaxRetriesExceededError:
|
except MaxRetriesExceededError:
|
||||||
# Well then, something is really wrong, let's send it without attachment before we
|
# Well then, something is really wrong, let's send it without attachment before we
|
||||||
# don't sent at all
|
# don't sent at all
|
||||||
logger.exception('Could not attach tickets to email')
|
logger.exception(f'Could not attach tickets to email {outgoing_mail.pk}')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
||||||
@@ -464,7 +470,7 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
)
|
)
|
||||||
invoices_attached.append(inv)
|
invoices_attached.append(inv)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('Could not attach invoice to email')
|
logger.exception(f'Could not attach invoice to email {outgoing_mail.pk}')
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if inv.transmission_type == "email":
|
if inv.transmission_type == "email":
|
||||||
@@ -494,7 +500,7 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
ftype
|
ftype
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
logger.exception('Could not attach file to email')
|
logger.exception(f'Could not attach file to email {outgoing_mail.pk}')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for cf in outgoing_mail.should_attach_cached_files.all():
|
for cf in outgoing_mail.should_attach_cached_files.all():
|
||||||
@@ -506,7 +512,7 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
cf.type,
|
cf.type,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
logger.exception('Could not attach file to email')
|
logger.exception(f'Could not attach file to email {outgoing_mail.pk}')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if outgoing_mail.event:
|
if outgoing_mail.event:
|
||||||
@@ -532,7 +538,7 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
try:
|
try:
|
||||||
backend.send_messages([email])
|
backend.send_messages([email])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception('Error sending email')
|
logger.exception(f'Error sending email {outgoing_mail.pk}')
|
||||||
retry_strategy = _retry_strategy(e)
|
retry_strategy = _retry_strategy(e)
|
||||||
err, err_detail = _format_error(e)
|
err, err_detail = _format_error(e)
|
||||||
|
|
||||||
@@ -554,19 +560,23 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
retry_after = min(30 + cnt * 10, 1800)
|
retry_after = min(30 + cnt * 10, 1800)
|
||||||
|
|
||||||
outgoing_mail.status = OutgoingMail.STATUS_AWAWITING_RETRY
|
outgoing_mail.status = OutgoingMail.STATUS_AWAWITING_RETRY
|
||||||
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent"])
|
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
|
||||||
|
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after"])
|
||||||
self.retry(max_retries=max_retries, countdown=retry_after) # throws RetryException, ends function flow
|
self.retry(max_retries=max_retries, countdown=retry_after) # throws RetryException, ends function flow
|
||||||
elif retry_strategy in ("microsoft_concurrency", "quick"):
|
elif retry_strategy in ("microsoft_concurrency", "quick"):
|
||||||
max_retries = 5
|
max_retries = 5
|
||||||
retry_after = [10, 30, 60, 300, 900, 900][self.request.retries]
|
retry_after = [10, 30, 60, 300, 900, 900][self.request.retries]
|
||||||
outgoing_mail.status = OutgoingMail.STATUS_AWAWITING_RETRY
|
outgoing_mail.status = OutgoingMail.STATUS_AWAWITING_RETRY
|
||||||
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent"])
|
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
|
||||||
|
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after"])
|
||||||
self.retry(max_retries=max_retries, countdown=retry_after) # throws RetryException, ends function flow
|
self.retry(max_retries=max_retries, countdown=retry_after) # throws RetryException, ends function flow
|
||||||
|
|
||||||
elif retry_strategy == "slow":
|
elif retry_strategy == "slow":
|
||||||
|
retry_after = [60, 300, 600, 1200, 1800, 1800][self.request.retries]
|
||||||
outgoing_mail.status = OutgoingMail.STATUS_AWAWITING_RETRY
|
outgoing_mail.status = OutgoingMail.STATUS_AWAWITING_RETRY
|
||||||
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent"])
|
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
|
||||||
self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800, 1800][self.request.retries]) # throws RetryException, ends function flow
|
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after"])
|
||||||
|
self.retry(max_retries=5, countdown=retry_after) # throws RetryException, ends function flow
|
||||||
|
|
||||||
except MaxRetriesExceededError:
|
except MaxRetriesExceededError:
|
||||||
for i in invoices_to_mark_transmitted:
|
for i in invoices_to_mark_transmitted:
|
||||||
@@ -586,12 +596,18 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
'invoices': [],
|
'invoices': [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
outgoing_mail.status = OutgoingMail.STATUS_FAILED
|
||||||
|
outgoing_mail.sent = now()
|
||||||
|
outgoing_mail.retry_after = None
|
||||||
|
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after"])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If we reach this, it's a non-retryable error
|
# If we reach this, it's a non-retryable error
|
||||||
outgoing_mail.status = OutgoingMail.STATUS_FAILED
|
outgoing_mail.status = OutgoingMail.STATUS_FAILED
|
||||||
outgoing_mail.sent = now()
|
outgoing_mail.sent = now()
|
||||||
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent"])
|
outgoing_mail.retry_after = None
|
||||||
|
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after"])
|
||||||
for i in invoices_to_mark_transmitted:
|
for i in invoices_to_mark_transmitted:
|
||||||
i.set_transmission_failed(provider="email_pdf", data={
|
i.set_transmission_failed(provider="email_pdf", data={
|
||||||
"reason": "exception",
|
"reason": "exception",
|
||||||
@@ -615,7 +631,8 @@ def mail_send_task(self, *args, outgoing_mail: int) -> bool:
|
|||||||
outgoing_mail.error_detail = None
|
outgoing_mail.error_detail = None
|
||||||
outgoing_mail.actual_attachments = actual_attachments
|
outgoing_mail.actual_attachments = actual_attachments
|
||||||
outgoing_mail.sent = now()
|
outgoing_mail.sent = now()
|
||||||
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "actual_attachments"])
|
outgoing_mail.retry_after = None
|
||||||
|
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "actual_attachments", "retry_after"])
|
||||||
for i in invoices_to_mark_transmitted:
|
for i in invoices_to_mark_transmitted:
|
||||||
if i.transmission_status != Invoice.TRANSMISSION_STATUS_COMPLETED:
|
if i.transmission_status != Invoice.TRANSMISSION_STATUS_COMPLETED:
|
||||||
i.transmission_date = now()
|
i.transmission_date = now()
|
||||||
|
|||||||
@@ -29,3 +29,8 @@ class FailingEmailBackend(EmailBackend):
|
|||||||
raise smtplib.SMTPRecipientsRefused({
|
raise smtplib.SMTPRecipientsRefused({
|
||||||
'recipient@example.org': (450, b'Recipient unknown')
|
'recipient@example.org': (450, b'Recipient unknown')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PermanentlyFailingEmailBackend(EmailBackend):
|
||||||
|
def send_messages(self, email_messages):
|
||||||
|
raise smtplib.SMTPNotSupportedError()
|
||||||
|
|||||||
@@ -39,14 +39,15 @@ from email.mime.text import MIMEText
|
|||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import mail as djmail
|
from django.core import mail as djmail
|
||||||
|
from django.test import override_settings
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import scope
|
from django_scopes import scope
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.base.email import get_email_context
|
from pretix.base.email import get_email_context
|
||||||
from pretix.base.models import Event, Organizer, User
|
from pretix.base.models import Event, Organizer, OutgoingMail, User
|
||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail, mail_send_task
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -162,6 +163,101 @@ def test_send_mail_with_user_locale(env):
|
|||||||
assert 'The language code used for rendering this email is de.' in djmail.outbox[0].body
|
assert 'The language code used for rendering this email is de.' in djmail.outbox[0].body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_queue_state_sent(env):
|
||||||
|
m = OutgoingMail.objects.create(
|
||||||
|
to=['recipient@example.com'],
|
||||||
|
subject='Test',
|
||||||
|
body_plain='Test',
|
||||||
|
sender='sender@example.com',
|
||||||
|
headers={},
|
||||||
|
)
|
||||||
|
assert m.status == OutgoingMail.STATUS_QUEUED
|
||||||
|
mail_send_task.apply(kwargs={
|
||||||
|
'outgoing_mail': m.pk,
|
||||||
|
}, max_retries=0)
|
||||||
|
m.refresh_from_db()
|
||||||
|
assert m.status == OutgoingMail.STATUS_SENT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(EMAIL_BACKEND='pretix.testutils.mail.PermanentlyFailingEmailBackend')
|
||||||
|
def test_queue_state_permanent_failure(env):
|
||||||
|
m = OutgoingMail.objects.create(
|
||||||
|
to=['recipient@example.com'],
|
||||||
|
subject='Test',
|
||||||
|
body_plain='Test',
|
||||||
|
sender='sender@example.com',
|
||||||
|
headers={},
|
||||||
|
)
|
||||||
|
assert m.status == OutgoingMail.STATUS_QUEUED
|
||||||
|
mail_send_task.apply(kwargs={
|
||||||
|
'outgoing_mail': m.pk,
|
||||||
|
}, max_retries=0)
|
||||||
|
m.refresh_from_db()
|
||||||
|
assert m.status == OutgoingMail.STATUS_FAILED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(EMAIL_BACKEND='pretix.testutils.mail.FailingEmailBackend')
|
||||||
|
def test_queue_state_retry_failure(env, monkeypatch):
|
||||||
|
def retry(*args, **kwargs):
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
monkeypatch.setattr('celery.app.task.Task.retry', retry, raising=True)
|
||||||
|
m = OutgoingMail.objects.create(
|
||||||
|
to=['recipient@example.com'],
|
||||||
|
subject='Test',
|
||||||
|
body_plain='Test',
|
||||||
|
sender='sender@example.com',
|
||||||
|
headers={},
|
||||||
|
)
|
||||||
|
assert m.status == OutgoingMail.STATUS_QUEUED
|
||||||
|
mail_send_task.apply(kwargs={
|
||||||
|
'outgoing_mail': m.pk,
|
||||||
|
}, max_retries=0)
|
||||||
|
m.refresh_from_db()
|
||||||
|
assert m.status == OutgoingMail.STATUS_AWAWITING_RETRY
|
||||||
|
assert m.retry_after > now()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_queue_state_foreign_key_handling():
|
||||||
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||||
|
event = Event.objects.create(
|
||||||
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
|
date_from=now()
|
||||||
|
)
|
||||||
|
|
||||||
|
mail_queued = OutgoingMail.objects.create(
|
||||||
|
organizer=o,
|
||||||
|
event=event,
|
||||||
|
to=['recipient@example.com'],
|
||||||
|
subject='Test',
|
||||||
|
body_plain='Test',
|
||||||
|
sender='sender@example.com',
|
||||||
|
headers={},
|
||||||
|
)
|
||||||
|
mail_sent = OutgoingMail.objects.create(
|
||||||
|
organizer=o,
|
||||||
|
event=event,
|
||||||
|
to=['recipient@example.com'],
|
||||||
|
subject='Test',
|
||||||
|
body_plain='Test',
|
||||||
|
sender='sender@example.com',
|
||||||
|
headers={},
|
||||||
|
status=OutgoingMail.STATUS_SENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
event.delete()
|
||||||
|
|
||||||
|
assert not OutgoingMail.objects.filter(pk=mail_queued.pk).exists()
|
||||||
|
assert OutgoingMail.objects.get(pk=mail_sent.pk).event is None
|
||||||
|
|
||||||
|
o.delete()
|
||||||
|
assert not OutgoingMail.objects.filter(pk=mail_sent.pk).exists()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_sendmail_placeholder(env):
|
def test_sendmail_placeholder(env):
|
||||||
djmail.outbox = []
|
djmail.outbox = []
|
||||||
|
|||||||
Reference in New Issue
Block a user