Source code for pretalx.mail.models.queued_mail

# SPDX-FileCopyrightText: 2017-present Tobias Kunze
# SPDX-License-Identifier: AGPL-3.0-only WITH LicenseRef-Pretalx-AGPL-3.0-Terms
#
# This file contains Apache-2.0 licensed contributions copyrighted by the following contributors:
# SPDX-FileContributor: Florian Moesch

import warnings
from smtplib import SMTPResponseException

from django.db import models
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy
from django_scopes import ScopedManager

from pretalx.common.models.mixins import PretalxModel
from pretalx.common.urls import EventUrls
from pretalx.mail.enums import QueuedMailStates
from pretalx.mail.rules import can_edit_mail
from pretalx.person.models.user import User
from pretalx.submission.rules import orga_can_change_submissions


class QueuedMailQuerySet(models.QuerySet):
    def prefetch_users(self, event):
        return self.prefetch_related(
            models.Prefetch("to_users", queryset=User.objects.with_speaker_code(event))
        )

    def with_computed_state(self):
        return self.annotate(
            computed_state=models.Case(
                models.When(
                    state=QueuedMailStates.DRAFT,
                    error_data__isnull=False,
                    then=models.Value("failed"),
                ),
                default=models.F("state"),
                output_field=models.CharField(),
            )
        )


class QueuedMailManager(models.Manager.from_queryset(QueuedMailQuerySet)):
    pass


[docs] class QueuedMail(PretalxModel): """Emails in pretalx are rarely sent directly, hence the name QueuedMail. This mechanism allows organisers to make sure they send out the right content, and to include personal changes in emails. :param sent: ``None`` if the mail has not been sent yet. :param to_users: All known users to whom this email is addressed. :param to: A comma-separated list of email addresses to whom this email is addressed. Does not contain any email addresses known to belong to users. """ log_prefix = "pretalx.mail" event = models.ForeignKey( to="event.Event", on_delete=models.PROTECT, related_name="queued_mails", null=True, blank=True, ) template = models.ForeignKey( to="mail.MailTemplate", related_name="mails", on_delete=models.SET_NULL, null=True, blank=True, ) to = models.CharField( max_length=1000, verbose_name=_("To"), help_text=_("One email address or several addresses separated by commas."), null=True, blank=True, ) to_users = models.ManyToManyField(to="person.User", related_name="mails") reply_to = models.CharField( max_length=1000, null=True, blank=True, verbose_name=_("Reply-To"), help_text=_("By default, the organiser address is used as Reply-To."), ) cc = models.CharField( max_length=1000, null=True, blank=True, verbose_name=_("CC"), help_text=_("One email address or several addresses separated by commas."), ) bcc = models.CharField( max_length=1000, null=True, blank=True, verbose_name=_("BCC"), help_text=_("One email address or several addresses separated by commas."), ) subject = models.CharField( max_length=200, verbose_name=pgettext_lazy("email subject", "Subject") ) text = models.TextField(verbose_name=_("Text")) # Set at send-time when the rendered HTML diverges from what # re-rendering ``text`` would produce; cleared on organiser edit. text_html = models.TextField(null=True, blank=True) sent = models.DateTimeField(null=True, blank=True, verbose_name=_("Sent at")) state = models.CharField( max_length=10, choices=QueuedMailStates.choices, default=QueuedMailStates.DRAFT, db_index=True, ) error_data = models.JSONField(null=True, blank=True, default=None) error_timestamp = models.DateTimeField(null=True, blank=True) locale = models.CharField(max_length=32, null=True, blank=True) attachments = models.JSONField(default=None, null=True, blank=True) submissions = models.ManyToManyField( to="submission.Submission", related_name="mails" ) objects = ScopedManager(event="event", _manager_class=QueuedMailManager) class Meta: rules_permissions = { "list": orga_can_change_submissions, "view": orga_can_change_submissions, "create": orga_can_change_submissions, "update": can_edit_mail & orga_can_change_submissions, "delete": orga_can_change_submissions, "send": orga_can_change_submissions, } class urls(EventUrls): base = edit = "{self.event.orga_urls.mail}{self.pk}/" delete = "{base}delete" send = "{base}send" copy = "{base}copy" def __str__(self): """Help with debugging.""" return f"QueuedMail(to={self.to}, subject={self.subject}, state={self.state})" @property def has_error(self): return self.state == QueuedMailStates.DRAFT and self.error_data is not None @property def body_html(self): # Not cached: ``MailDetailForm.save`` clears ``text_html`` on # organiser edits, and ``body_html`` must re-render against the # mutated state on the same instance. from pretalx.mail.domain.render import ( # noqa: PLC0415 -- thin method delivery_html_body, ) return delivery_html_body(self) @cached_property def prefixed_subject(self): from pretalx.mail.domain.render import ( # noqa: PLC0415 -- thin method get_prefixed_subject, ) event = getattr(self, "event", None) if not event: return self.subject return get_prefixed_subject(event, self.subject) def mark_sent(self): self.state = QueuedMailStates.SENT self.sent = now() self.error_data = None self.error_timestamp = None self.save(update_fields=["state", "sent", "error_data", "error_timestamp"]) mark_sent.alters_data = True def mark_failed(self, exception): self.state = QueuedMailStates.DRAFT error_data = {"error": str(exception), "type": type(exception).__name__} if isinstance(exception, SMTPResponseException): smtp_message = exception.smtp_error if isinstance(smtp_message, bytes): smtp_message = smtp_message.decode("utf-8", errors="replace") error_data["smtp_code"] = exception.smtp_code error_data["error"] = smtp_message self.error_data = error_data self.error_timestamp = now() self.save(update_fields=["state", "error_data", "error_timestamp"]) mark_failed.alters_data = True
[docs] @warnings.deprecated( "QueuedMail.send is deprecated; use send_draft / send_transient " "from pretalx.mail.domain.send." ) def send(self, requestor=None, orga: bool = True): """Deprecated; kept as a compatibility shim for third-party plugins. TODO: remove after v2026.2.0. Use the explicit dispatch helpers in :mod:`pretalx.mail.domain.send` instead.""" from pretalx.mail.domain.send import ( # noqa: PLC0415 -- thin method send_draft, send_transient, ) if self.pk: send_draft(self, requestor=requestor, orga=orga) else: send_transient(self)
send.alters_data = True