Model-based mail queuing

This commit is contained in:
Raphael Michel
2026-01-30 10:43:02 +01:00
committed by GitHub
parent 1492ec51bf
commit c40e34af57
51 changed files with 2415 additions and 1030 deletions

View File

@@ -39,14 +39,15 @@ from email.mime.text import MIMEText
import pytest
from django.conf import settings
from django.core import mail as djmail
from django.test import override_settings
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import scope
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.models import Event, Organizer, User
from pretix.base.services.mail import mail
from pretix.base.models import Event, Organizer, OutgoingMail, User
from pretix.base.services.mail import mail, mail_send_task
@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
@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_AWAITING_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
def test_sendmail_placeholder(env):
djmail.outbox = []

View File

@@ -31,7 +31,7 @@ from django_scopes import scope
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
OrderPayment, OrderPosition, Organizer, QuestionAnswer,
OrderPayment, OrderPosition, Organizer, OutgoingMail, QuestionAnswer,
)
from pretix.base.services.invoices import generate_invoice, invoice_pdf_task
from pretix.base.services.tickets import generate
@@ -111,6 +111,15 @@ def test_email_shredder(event, order):
'new_email': 'foo@bar.com',
}
)
m = OutgoingMail.objects.create(
event=event,
order=order,
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
s = EmailAddressShredder(event)
f = list(s.generate_files())
@@ -129,6 +138,7 @@ def test_email_shredder(event, order):
assert 'Foo' not in l1.data
l2.refresh_from_db()
assert '@' not in l2.data
assert not OutgoingMail.objects.filter(pk=m.pk).exists()
@pytest.mark.django_db

View File

@@ -24,12 +24,13 @@ from smtplib import SMTPResponseException
import pytest
import responses
from django.conf import settings
from django.db import transaction
from django.test.utils import override_settings
from django_scopes import scopes_disabled
from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, Organizer, Team, User
from pretix.base.models import Event, Organizer, OutgoingMail, Team, User
@pytest.fixture
@@ -453,3 +454,111 @@ class OrganizerTest(SoupTest):
self.event1.refresh_from_db()
assert 'pretix.plugins.banktransfer' not in self.event1.get_plugins()
assert 'pretix.plugins.stripe' in self.event1.get_plugins()
def test_outgoing_mails_list_and_detail(self):
m1 = OutgoingMail.objects.create(
organizer=self.orga1,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
m2 = OutgoingMail.objects.create(
organizer=self.orga2,
to=['wrongrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
resp = self.client.get('/control/organizer/%s/outgoingmails' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
assert b"wrongrecipient@example.com" not in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmails?status=queued' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmails?status=sent' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" not in resp.content
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
resp = self.client.get('/control/organizer/%s/outgoingmails?query=RIGHTrecipient@example.com' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmails?query=wrongrecipient@example.com' % self.orga1.slug)
assert resp.status_code == 200
assert b"rightrecipient@example.com" not in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmail/%d/' % (self.orga1.slug, m1.pk))
assert resp.status_code == 200
assert b"rightrecipient@example.com" in resp.content
resp = self.client.get('/control/organizer/%s/outgoingmail/%d/' % (self.orga1.slug, m2.pk))
assert resp.status_code == 404
def test_outgoing_mails_retry(self):
m1 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_SENT,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
m2 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_FAILED,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
resp = self.client.post(
'/control/organizer/%s/outgoingmail/bulk_action' % self.orga1.slug,
data={
"action": "retry",
"outgoingmail": [m1.pk, m2.pk]
}
)
assert resp.status_code == 302
m1.refresh_from_db()
m2.refresh_from_db()
assert m1.status == OutgoingMail.STATUS_SENT
assert m2.status in (OutgoingMail.STATUS_SENT, OutgoingMail.STATUS_QUEUED)
def test_outgoing_mails_abort(self):
m1 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_SENT,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
m2 = OutgoingMail.objects.create(
organizer=self.orga1,
status=OutgoingMail.STATUS_QUEUED,
to=['rightrecipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
headers={},
)
resp = self.client.post(
'/control/organizer/%s/outgoingmail/bulk_action' % self.orga1.slug,
data={
"action": "abort",
"__ALL": "on",
}
)
assert resp.status_code == 302
m1.refresh_from_db()
m2.refresh_from_db()
assert m1.status == OutgoingMail.STATUS_SENT
assert m2.status == OutgoingMail.STATUS_ABORTED

View File

@@ -192,6 +192,9 @@ organizer_urls = [
'organizer/abc/team/1/edit',
'organizer/abc/team/1/delete',
'organizer/abc/team/add',
'organizer/abc/outgoingmails',
'organizer/abc/outgoingmail/bulk_action',
'organizer/abc/outgoingmail/1/',
'organizer/abc/devices',
'organizer/abc/device/add',
'organizer/abc/device/bulk_edit',
@@ -528,6 +531,9 @@ organizer_permission_urls = [
("can_change_organizer_settings", "organizer/dummy/settings/plugins/pretix.plugins.sendmail/events", 200),
("can_change_organizer_settings", "organizer/dummy/settings/email", 200),
("can_change_organizer_settings", "organizer/dummy/settings/email/setup", 200),
("can_change_organizer_settings", "organizer/dummy/outgoingmails", 200),
("can_change_organizer_settings", "organizer/dummy/outgoingmail/1/", 404),
("can_change_organizer_settings", "organizer/dummy/outgoingmail/bulk_action", 405),
("can_change_organizer_settings", "organizer/dummy/devices", 200),
("can_change_organizer_settings", "organizer/dummy/devices/select2", 200),
("can_change_organizer_settings", "organizer/dummy/device/add", 200),