mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Model-based mail queuing
This commit is contained in:
@@ -49,7 +49,7 @@ from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.settings import validate_organizer_settings
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
@@ -363,24 +363,21 @@ class TeamInviteSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.context['organizer'].name,
|
||||
'team': instance.team.name,
|
||||
'url': build_global_uri('control:auth.invite', kwargs={
|
||||
'token': instance.token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=get_language_without_region() # TODO: expose?
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.context['organizer'].name,
|
||||
'team': instance.team.name,
|
||||
'url': build_global_uri('control:auth.invite', kwargs={
|
||||
'token': instance.token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=get_language_without_region() # TODO: expose?
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
if 'email' in validated_data:
|
||||
|
||||
@@ -90,7 +90,6 @@ from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
|
||||
regenerate_invoice, transmit_invoice,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _order_placed_email,
|
||||
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
|
||||
@@ -439,8 +438,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except PaymentException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except SendMailException:
|
||||
pass
|
||||
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
return Response(
|
||||
@@ -634,10 +631,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
order = self.get_object()
|
||||
if not order.email:
|
||||
return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
order.resend_link(user=self.request.user, auth=self.request.auth)
|
||||
except SendMailException:
|
||||
return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
order.resend_link(user=self.request.user, auth=self.request.auth)
|
||||
|
||||
return Response(
|
||||
status=status.HTTP_204_NO_CONTENT
|
||||
@@ -1616,8 +1610,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
except SendMailException:
|
||||
pass
|
||||
|
||||
serializer = OrderPaymentSerializer(r, context=serializer.context)
|
||||
|
||||
@@ -1655,8 +1647,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except PaymentException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except SendMailException:
|
||||
pass
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
|
||||
@@ -33,7 +33,7 @@ from pretix.base.invoicing.transmission import (
|
||||
transmission_types,
|
||||
)
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
@@ -133,41 +133,37 @@ class EmailTransmissionProvider(TransmissionProvider):
|
||||
template = invoice.order.event.settings.get('mail_text_order_invoice', as_type=LazyI18nString)
|
||||
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
|
||||
|
||||
try:
|
||||
# Do not set to completed because that is done by the email sending task
|
||||
subject = format_map(subject, context)
|
||||
email_content = render_mail(template, context)
|
||||
mail(
|
||||
[recipient],
|
||||
subject,
|
||||
template,
|
||||
context=context,
|
||||
event=invoice.order.event,
|
||||
locale=invoice.order.locale,
|
||||
order=invoice.order,
|
||||
invoices=[invoice],
|
||||
attach_tickets=False,
|
||||
auto_email=True,
|
||||
attach_ical=False,
|
||||
plain_text_only=True,
|
||||
no_order_links=True,
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
else:
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': None,
|
||||
'recipient': recipient,
|
||||
'invoices': [invoice.pk],
|
||||
'attach_tickets': False,
|
||||
'attach_ical': False,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
# Do not set to completed because that is done by the email sending task
|
||||
subject = format_map(subject, context)
|
||||
email_content = render_mail(template, context)
|
||||
mail(
|
||||
[recipient],
|
||||
subject,
|
||||
template,
|
||||
context=context,
|
||||
event=invoice.order.event,
|
||||
locale=invoice.order.locale,
|
||||
order=invoice.order,
|
||||
invoices=[invoice],
|
||||
attach_tickets=False,
|
||||
auto_email=True,
|
||||
attach_ical=False,
|
||||
plain_text_only=True,
|
||||
no_order_links=True,
|
||||
)
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': None,
|
||||
'recipient': recipient,
|
||||
'invoices': [invoice.pk],
|
||||
'attach_tickets': False,
|
||||
'attach_ical': False,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
|
||||
120
src/pretix/base/migrations/0297_outgoingmail.py
Normal file
120
src/pretix/base/migrations/0297_outgoingmail.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Generated by Django 4.2.26 on 2026-01-22 13:44
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.mail
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0296_invoice_invoice_from_state"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="OutgoingMail",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("guid", models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
("status", models.CharField(default="queued", max_length=200)),
|
||||
("created", models.DateTimeField(auto_now_add=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_detail", models.TextField(null=True)),
|
||||
("sensitive", models.BooleanField(default=False)),
|
||||
("subject", models.TextField()),
|
||||
("body_plain", models.TextField()),
|
||||
("body_html", models.TextField(null=True)),
|
||||
("sender", models.CharField(max_length=500)),
|
||||
("headers", models.JSONField(default=dict)),
|
||||
("to", models.JSONField(default=list)),
|
||||
("cc", models.JSONField(default=list)),
|
||||
("bcc", models.JSONField(default=list)),
|
||||
("recipient_count", models.IntegerField()),
|
||||
("should_attach_tickets", models.BooleanField(default=False)),
|
||||
("should_attach_ical", models.BooleanField(default=False)),
|
||||
("should_attach_other_files", models.JSONField(default=list)),
|
||||
("actual_attachments", models.JSONField(default=list)),
|
||||
(
|
||||
"customer",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||
related_name="outgoing_mails",
|
||||
to="pretixbase.customer",
|
||||
),
|
||||
),
|
||||
(
|
||||
"event",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||
related_name="outgoing_mails",
|
||||
to="pretixbase.event",
|
||||
),
|
||||
),
|
||||
(
|
||||
"order",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||
related_name="outgoing_mails",
|
||||
to="pretixbase.order",
|
||||
),
|
||||
),
|
||||
(
|
||||
"orderposition",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
|
||||
related_name="outgoing_mails",
|
||||
to="pretixbase.orderposition",
|
||||
),
|
||||
),
|
||||
(
|
||||
"organizer",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="outgoing_mails",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
(
|
||||
"should_attach_cached_files",
|
||||
models.ManyToManyField(
|
||||
related_name="outgoing_mails", to="pretixbase.cachedfile"
|
||||
),
|
||||
),
|
||||
(
|
||||
"should_attach_invoices",
|
||||
models.ManyToManyField(
|
||||
related_name="outgoing_mails", to="pretixbase.invoice"
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="outgoing_mails",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ("-created",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -41,6 +41,7 @@ from .items import (
|
||||
itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .mail import OutgoingMail
|
||||
from .media import ReusableMedium
|
||||
from .memberships import Membership, MembershipType
|
||||
from .notifications import NotificationSetting
|
||||
|
||||
@@ -334,27 +334,24 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return self.email
|
||||
|
||||
def send_security_notice(self, messages, email=None):
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
try:
|
||||
with language(self.locale):
|
||||
msg = '- ' + '\n- '.join(str(m) for m in messages)
|
||||
with language(self.locale):
|
||||
msg = '- ' + '\n- '.join(str(m) for m in messages)
|
||||
|
||||
mail(
|
||||
email or self.email,
|
||||
_('Account information changed'),
|
||||
'pretixcontrol/email/security_notice.txt',
|
||||
{
|
||||
'user': self,
|
||||
'messages': msg,
|
||||
'url': build_absolute_uri('control:user.settings')
|
||||
},
|
||||
event=None,
|
||||
user=self,
|
||||
locale=self.locale
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
mail(
|
||||
email or self.email,
|
||||
_('Account information changed'),
|
||||
'pretixcontrol/email/security_notice.txt',
|
||||
{
|
||||
'user': self,
|
||||
'messages': msg,
|
||||
'url': build_absolute_uri('control:user.settings')
|
||||
},
|
||||
event=None,
|
||||
user=self,
|
||||
locale=self.locale
|
||||
)
|
||||
|
||||
def send_confirmation_code(self, session, reason, email=None, state=None):
|
||||
"""
|
||||
|
||||
@@ -293,6 +293,7 @@ class Customer(LoggedModel):
|
||||
locale=self.locale,
|
||||
customer=self,
|
||||
organizer=self.organizer,
|
||||
sensitive=True,
|
||||
)
|
||||
|
||||
def usable_gift_cards(self, used_cards=[]):
|
||||
|
||||
222
src/pretix/base/models/mail.py
Normal file
222
src/pretix/base/models/mail.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import uuid
|
||||
|
||||
from django.core.mail import get_connection
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
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
|
||||
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):
|
||||
STATUS_QUEUED = "queued"
|
||||
STATUS_WITHHELD = "withheld"
|
||||
STATUS_INFLIGHT = "inflight"
|
||||
STATUS_AWAITING_RETRY = "awaiting_retry"
|
||||
STATUS_FAILED = "failed"
|
||||
STATUS_SENT = "sent"
|
||||
STATUS_BOUNCED = "bounced"
|
||||
STATUS_ABORTED = "aborted"
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_QUEUED, _("queued")),
|
||||
(STATUS_INFLIGHT, _("being sent")),
|
||||
(STATUS_AWAITING_RETRY, _("awaiting retry")),
|
||||
(STATUS_WITHHELD, _("withheld")), # for plugin use
|
||||
(STATUS_FAILED, _("failed")),
|
||||
(STATUS_ABORTED, _("aborted")),
|
||||
(STATUS_SENT, _("sent")),
|
||||
(STATUS_BOUNCED, _("bounced")), # for plugin use
|
||||
)
|
||||
STATUS_LIST_ABORTABLE = {
|
||||
STATUS_QUEUED,
|
||||
STATUS_WITHHELD,
|
||||
STATUS_AWAITING_RETRY,
|
||||
}
|
||||
STATUS_LIST_RETRYABLE = {
|
||||
STATUS_FAILED,
|
||||
STATUS_WITHHELD,
|
||||
}
|
||||
|
||||
# The GUID is a globally unique ID for the email added to a header of the email for later tracing
|
||||
# in bug reports etc. We could theoretically also use this as a basis for the Message-ID header, but
|
||||
# we currently don't since we are unsure if some intermediary SMTP servers have opinions on setting
|
||||
# their own Message-ID headers.
|
||||
guid = models.UUIDField(db_index=True, default=uuid.uuid4)
|
||||
|
||||
status = models.CharField(max_length=200, choices=STATUS_CHOICES, default=STATUS_QUEUED)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# sent will be the time the email was sent or the email failed
|
||||
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_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 organizer 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(
|
||||
'pretixbase.Organizer',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='outgoing_mails',
|
||||
null=True, blank=True,
|
||||
)
|
||||
event = models.ForeignKey(
|
||||
'pretixbase.Event',
|
||||
on_delete=CASCADE_IF_QUEUED,
|
||||
related_name='outgoing_mails',
|
||||
null=True, blank=True,
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
'pretixbase.Order',
|
||||
on_delete=CASCADE_IF_QUEUED,
|
||||
related_name='outgoing_mails',
|
||||
null=True, blank=True,
|
||||
)
|
||||
orderposition = models.ForeignKey(
|
||||
'pretixbase.OrderPosition',
|
||||
on_delete=CASCADE_IF_QUEUED,
|
||||
related_name='outgoing_mails',
|
||||
null=True, blank=True,
|
||||
)
|
||||
customer = models.ForeignKey(
|
||||
'pretixbase.Customer',
|
||||
on_delete=CASCADE_IF_QUEUED,
|
||||
related_name='outgoing_mails',
|
||||
null=True, blank=True,
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
'pretixbase.User',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='outgoing_mails',
|
||||
null=True, blank=True,
|
||||
)
|
||||
|
||||
sensitive = models.BooleanField(default=False)
|
||||
subject = models.TextField()
|
||||
body_plain = models.TextField()
|
||||
body_html = models.TextField(null=True)
|
||||
sender = models.CharField(max_length=500)
|
||||
headers = models.JSONField(default=dict)
|
||||
to = models.JSONField(default=list)
|
||||
cc = models.JSONField(default=list)
|
||||
bcc = models.JSONField(default=list)
|
||||
recipient_count = models.IntegerField()
|
||||
|
||||
# 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(
|
||||
'pretixbase.Invoice',
|
||||
related_name='outgoing_mails'
|
||||
)
|
||||
should_attach_tickets = models.BooleanField(default=False)
|
||||
should_attach_ical = models.BooleanField(default=False)
|
||||
|
||||
# clean_cached_files makes sure not to delete these as long as the email is in a retryable state
|
||||
should_attach_cached_files = models.ManyToManyField(
|
||||
'pretixbase.CachedFile',
|
||||
related_name='outgoing_mails',
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# [{name, type size}] of the attachments we actually setn
|
||||
actual_attachments = models.JSONField(default=list)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-created',)
|
||||
|
||||
def get_mail_backend(self):
|
||||
if self.event:
|
||||
return self.event.get_mail_backend()
|
||||
elif self.organizer:
|
||||
return self.organizer.get_mail_backend()
|
||||
else:
|
||||
return get_connection(fail_silently=False)
|
||||
|
||||
def scope_manager(self):
|
||||
if self.organizer:
|
||||
return scope(organizer=self.organizer) # noqa
|
||||
else:
|
||||
return scopes_disabled() # noqa
|
||||
|
||||
@property
|
||||
def is_failed(self):
|
||||
return self.status in (
|
||||
OutgoingMail.STATUS_FAILED,
|
||||
OutgoingMail.STATUS_AWAITING_RETRY,
|
||||
OutgoingMail.STATUS_BOUNCED,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.orderposition_id and not self.order_id:
|
||||
self.order = self.orderposition.order
|
||||
if self.order_id and not self.event_id:
|
||||
self.event = self.order.event
|
||||
if self.event_id and not self.organizer_id:
|
||||
self.organizer = self.event.organizer
|
||||
if self.customer_id and not self.organizer_id:
|
||||
self.organizer = self.customer.organizer
|
||||
self.recipient_count = len(self.to) + len(self.cc) + len(self.bcc)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def log_parameters(self):
|
||||
if self.order:
|
||||
error_log_action_type = 'pretix.event.order.email.error'
|
||||
log_target = self.order
|
||||
elif self.customer:
|
||||
error_log_action_type = 'pretix.customer.email.error'
|
||||
log_target = self.customer
|
||||
elif self.user:
|
||||
error_log_action_type = 'pretix.user.email.error'
|
||||
log_target = self.user
|
||||
else:
|
||||
error_log_action_type = 'pretix.email.error'
|
||||
log_target = None
|
||||
return log_target, error_log_action_type
|
||||
@@ -1167,9 +1167,7 @@ class Order(LockModel, LoggedModel):
|
||||
only be attached for this position and child positions, the link will only point to the
|
||||
position and the attendee email will be used if available.
|
||||
"""
|
||||
from pretix.base.services.mail import (
|
||||
SendMailException, mail, render_mail,
|
||||
)
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
|
||||
if not self.email and not (position and position.attendee_email):
|
||||
return
|
||||
@@ -1179,35 +1177,31 @@ class Order(LockModel, LoggedModel):
|
||||
if position and position.attendee_email:
|
||||
recipient = position.attendee_email
|
||||
|
||||
try:
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
invoices=invoices, attach_tickets=attach_tickets,
|
||||
position=position, auto_email=auto_email, attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
else:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': position.positionid if position else None,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
invoices=invoices, attach_tickets=attach_tickets,
|
||||
position=position, auto_email=auto_email, attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': position.positionid if position else None,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
@@ -2024,40 +2018,30 @@ class OrderPayment(models.Model):
|
||||
transmit_invoice.apply_async(args=(self.order.event_id, invoice.pk, False))
|
||||
|
||||
def _send_paid_mail_attendee(self, position, user):
|
||||
from pretix.base.services.mail import SendMailException
|
||||
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
email_template = self.order.event.settings.mail_text_order_paid_attendee
|
||||
email_subject = self.order.event.settings.mail_subject_order_paid_attendee
|
||||
email_context = get_email_context(event=self.order.event, order=self.order, position=position)
|
||||
try:
|
||||
position.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_paid', user,
|
||||
invoices=[],
|
||||
attach_tickets=True,
|
||||
attach_ical=self.order.event.settings.mail_attach_ical
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order paid email could not be sent')
|
||||
position.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_paid', user,
|
||||
invoices=[],
|
||||
attach_tickets=True,
|
||||
attach_ical=self.order.event.settings.mail_attach_ical
|
||||
)
|
||||
|
||||
def _send_paid_mail(self, invoice, user, mail_text):
|
||||
from pretix.base.services.mail import SendMailException
|
||||
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
email_template = self.order.event.settings.mail_text_order_paid
|
||||
email_subject = self.order.event.settings.mail_subject_order_paid
|
||||
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
|
||||
try:
|
||||
self.order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_paid', user,
|
||||
invoices=[invoice] if invoice else [],
|
||||
attach_tickets=True,
|
||||
attach_ical=self.order.event.settings.mail_attach_ical
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order paid email could not be sent')
|
||||
self.order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_paid', user,
|
||||
invoices=[invoice] if invoice else [],
|
||||
attach_tickets=True,
|
||||
attach_ical=self.order.event.settings.mail_attach_ical
|
||||
)
|
||||
|
||||
@property
|
||||
def refunded_amount(self):
|
||||
@@ -2915,45 +2899,39 @@ class OrderPosition(AbstractPosition):
|
||||
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
|
||||
:param attach_ical: Attach relevant ICS files
|
||||
"""
|
||||
from pretix.base.services.mail import (
|
||||
SendMailException, mail, render_mail,
|
||||
)
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
|
||||
if not self.attendee_email:
|
||||
return
|
||||
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
recipient = self.attendee_email
|
||||
try:
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
|
||||
position=self,
|
||||
invoices=invoices,
|
||||
attach_tickets=attach_tickets,
|
||||
attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files,
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
else:
|
||||
self.order.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
|
||||
position=self,
|
||||
invoices=invoices,
|
||||
attach_tickets=attach_tickets,
|
||||
attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files,
|
||||
)
|
||||
self.order.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User, Voucher
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
from ...helpers.format import format_map
|
||||
@@ -272,34 +272,30 @@ class WaitingListEntry(LoggedModel):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
recipient = self.email
|
||||
|
||||
try:
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event,
|
||||
self.locale,
|
||||
headers=headers,
|
||||
sender=sender,
|
||||
auto_email=auto_email,
|
||||
attach_other_files=attach_other_files,
|
||||
attach_cached_files=attach_cached_files,
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
else:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event,
|
||||
self.locale,
|
||||
headers=headers,
|
||||
sender=sender,
|
||||
auto_email=auto_email,
|
||||
attach_other_files=attach_other_files,
|
||||
attach_cached_files=attach_cached_files,
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_itemvar(event, item, variation):
|
||||
|
||||
@@ -36,7 +36,7 @@ from pretix.base.models import (
|
||||
SubEvent, TaxRule, User, WaitingListEntry,
|
||||
)
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
|
||||
)
|
||||
@@ -53,17 +53,14 @@ logger = logging.getLogger(__name__)
|
||||
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
|
||||
with language(wle.locale, wle.event.settings.region):
|
||||
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
|
||||
try:
|
||||
mail(
|
||||
wle.email,
|
||||
format_map(subject, email_context),
|
||||
message,
|
||||
email_context,
|
||||
wle.event,
|
||||
locale=wle.locale
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Waiting list canceled email could not be sent')
|
||||
mail(
|
||||
wle.email,
|
||||
format_map(subject, email_context),
|
||||
message,
|
||||
email_context,
|
||||
wle.event,
|
||||
locale=wle.locale
|
||||
)
|
||||
|
||||
|
||||
def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent,
|
||||
@@ -77,14 +74,11 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
|
||||
order=order, position_or_address=ia, event=order.event)
|
||||
real_subject = format_map(subject, email_context)
|
||||
try:
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
user,
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order canceled email could not be sent')
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
user,
|
||||
)
|
||||
|
||||
for p in positions:
|
||||
if subevent and p.subevent_id != subevent.id:
|
||||
@@ -97,15 +91,12 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
refund_amount=refund_amount,
|
||||
position_or_address=p,
|
||||
order=order, position=p)
|
||||
try:
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
position=p,
|
||||
user=user
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order canceled email could not be sent to attendee')
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
position=p,
|
||||
user=user
|
||||
)
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
|
||||
@@ -23,11 +23,12 @@ from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import CachedCombinedTicket, CachedTicket
|
||||
from pretix.base.models import CachedCombinedTicket, CachedTicket, OutgoingMail
|
||||
from pretix.base.models.customers import CustomerSSOGrant
|
||||
|
||||
from ..models import CachedFile, CartPosition, InvoiceAddress
|
||||
@@ -49,7 +50,18 @@ def clean_cart_positions(sender, **kwargs):
|
||||
@receiver(signal=periodic_task)
|
||||
@scopes_disabled()
|
||||
def clean_cached_files(sender, **kwargs):
|
||||
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()):
|
||||
has_queued_email = Exists(
|
||||
OutgoingMail.objects.filter(
|
||||
should_attach_cached_files__pk=OuterRef("pk"),
|
||||
status__in=(
|
||||
OutgoingMail.STATUS_QUEUED,
|
||||
OutgoingMail.STATUS_INFLIGHT,
|
||||
OutgoingMail.STATUS_AWAITING_RETRY,
|
||||
OutgoingMail.STATUS_FAILED,
|
||||
),
|
||||
)
|
||||
)
|
||||
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()).exclude(has_queued_email):
|
||||
cf.delete()
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,8 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import uuid
|
||||
|
||||
import css_inline
|
||||
from django.conf import settings
|
||||
from django.template.loader import get_template
|
||||
@@ -26,7 +28,9 @@ from django.utils.timezone import override
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import LogEntry, NotificationSetting, User
|
||||
from pretix.base.models import (
|
||||
LogEntry, NotificationSetting, OutgoingMail, User,
|
||||
)
|
||||
from pretix.base.notifications import Notification, get_all_notification_types
|
||||
from pretix.base.services.mail import mail_send_task
|
||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
||||
@@ -153,16 +157,26 @@ def send_notification_mail(notification: Notification, user: User):
|
||||
tpl_plain = get_template('pretixbase/email/notification.txt')
|
||||
body_plain = tpl_plain.render(ctx)
|
||||
|
||||
mail_send_task.apply_async(kwargs={
|
||||
'to': [user.email],
|
||||
'subject': '[{}] {}: {}'.format(
|
||||
guid = uuid.uuid4()
|
||||
m = OutgoingMail.objects.create(
|
||||
guid=guid,
|
||||
user=user,
|
||||
to=[user.email],
|
||||
subject='[{}] {}: {}'.format(
|
||||
settings.PRETIX_INSTANCE_NAME,
|
||||
notification.event.settings.mail_prefix or notification.event.slug.upper(),
|
||||
notification.title
|
||||
),
|
||||
'body': body_plain,
|
||||
'html': body_html,
|
||||
'sender': settings.MAIL_FROM_NOTIFICATIONS,
|
||||
'headers': {},
|
||||
'user': user.pk
|
||||
body_plain=body_plain,
|
||||
body_html=body_html,
|
||||
sender=settings.MAIL_FROM_NOTIFICATIONS,
|
||||
headers={
|
||||
'X-Auto-Response-Suppress': 'OOF, NRN, AutoReply, RN',
|
||||
'Auto-Submitted': 'auto-generated',
|
||||
'X-Mailer': 'pretix',
|
||||
'X-PX-Correlation': str(guid),
|
||||
},
|
||||
)
|
||||
mail_send_task.apply_async(kwargs={
|
||||
'outgoing_mail': m.pk,
|
||||
})
|
||||
|
||||
@@ -90,7 +90,6 @@ from pretix.base.services.invoices import (
|
||||
from pretix.base.services.locking import (
|
||||
LOCK_TRUST_WINDOW, LockTimeoutException, lock_objects,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.memberships import (
|
||||
create_membership, validate_memberships_in_order,
|
||||
)
|
||||
@@ -438,33 +437,27 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
|
||||
email_attendee_subject = order.event.settings.mail_subject_order_approved_attendee
|
||||
|
||||
email_context = get_email_context(event=order.event, order=order)
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_approved', user,
|
||||
attach_tickets=True,
|
||||
attach_ical=order.event.settings.mail_attach_ical and (
|
||||
not order.event.settings.mail_attach_ical_paid_only or
|
||||
order.total == Decimal('0.00') or
|
||||
order.valid_if_pending
|
||||
),
|
||||
invoices=[invoice] if invoice and transmit_invoice_mail else []
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order approved email could not be sent')
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_approved', user,
|
||||
attach_tickets=True,
|
||||
attach_ical=order.event.settings.mail_attach_ical and (
|
||||
not order.event.settings.mail_attach_ical_paid_only or
|
||||
order.total == Decimal('0.00') or
|
||||
order.valid_if_pending
|
||||
),
|
||||
invoices=[invoice] if invoice and transmit_invoice_mail else []
|
||||
)
|
||||
|
||||
if email_attendees:
|
||||
for p in order.positions.all():
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
email_attendee_context = get_email_context(event=order.event, order=order, position=p)
|
||||
try:
|
||||
p.send_mail(
|
||||
email_attendee_subject, email_attendee_template, email_attendee_context,
|
||||
'pretix.event.order.email.order_approved', user,
|
||||
attach_tickets=True,
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order approved email could not be sent to attendee')
|
||||
p.send_mail(
|
||||
email_attendee_subject, email_attendee_template, email_attendee_context,
|
||||
'pretix.event.order.email.order_approved', user,
|
||||
attach_tickets=True,
|
||||
)
|
||||
|
||||
return order.pk
|
||||
|
||||
@@ -501,13 +494,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
email_template = order.event.settings.mail_text_order_denied
|
||||
email_subject = order.event.settings.mail_subject_order_denied
|
||||
email_context = get_email_context(event=order.event, order=order, comment=comment)
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_denied', user
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order denied email could not be sent')
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_denied', user
|
||||
)
|
||||
|
||||
return order.pk
|
||||
|
||||
@@ -660,14 +650,11 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
email_template = order.event.settings.mail_text_order_canceled
|
||||
email_subject = order.event.settings.mail_subject_order_canceled
|
||||
email_context = get_email_context(event=order.event, order=order, comment=comment or "")
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_canceled', user,
|
||||
invoices=transmit_invoices_mail,
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order canceled email could not be sent')
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_canceled', user,
|
||||
invoices=transmit_invoices_mail,
|
||||
)
|
||||
|
||||
for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)):
|
||||
try:
|
||||
@@ -1108,46 +1095,40 @@ def _order_placed_email(event: Event, order: Order, email_template, subject_temp
|
||||
log_entry: str, invoice, payments: List[OrderPayment], is_free=False):
|
||||
email_context = get_email_context(event=event, order=order, payments=payments)
|
||||
|
||||
try:
|
||||
order.send_mail(
|
||||
subject_template, email_template, email_context,
|
||||
log_entry,
|
||||
invoices=[invoice] if invoice else [],
|
||||
attach_tickets=True,
|
||||
attach_ical=event.settings.mail_attach_ical and (
|
||||
not event.settings.mail_attach_ical_paid_only or
|
||||
is_free or
|
||||
order.valid_if_pending
|
||||
),
|
||||
attach_other_files=[a for a in [
|
||||
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a],
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order received email could not be sent')
|
||||
order.send_mail(
|
||||
subject_template, email_template, email_context,
|
||||
log_entry,
|
||||
invoices=[invoice] if invoice else [],
|
||||
attach_tickets=True,
|
||||
attach_ical=event.settings.mail_attach_ical and (
|
||||
not event.settings.mail_attach_ical_paid_only or
|
||||
is_free or
|
||||
order.valid_if_pending
|
||||
),
|
||||
attach_other_files=[a for a in [
|
||||
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a],
|
||||
)
|
||||
|
||||
|
||||
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, subject_template,
|
||||
log_entry: str, is_free=False):
|
||||
email_context = get_email_context(event=event, order=order, position=position)
|
||||
|
||||
try:
|
||||
position.send_mail(
|
||||
subject_template, email_template, email_context,
|
||||
log_entry,
|
||||
invoices=[],
|
||||
attach_tickets=True,
|
||||
attach_ical=event.settings.mail_attach_ical and (
|
||||
not event.settings.mail_attach_ical_paid_only or
|
||||
is_free or
|
||||
order.valid_if_pending
|
||||
),
|
||||
attach_other_files=[a for a in [
|
||||
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a],
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order received email could not be sent to attendee')
|
||||
position.send_mail(
|
||||
subject_template, email_template, email_context,
|
||||
log_entry,
|
||||
invoices=[],
|
||||
attach_tickets=True,
|
||||
attach_ical=event.settings.mail_attach_ical and (
|
||||
not event.settings.mail_attach_ical_paid_only or
|
||||
is_free or
|
||||
order.valid_if_pending
|
||||
),
|
||||
attach_other_files=[a for a in [
|
||||
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a],
|
||||
)
|
||||
|
||||
|
||||
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
|
||||
@@ -1476,13 +1457,10 @@ def send_expiry_warnings(sender, **kwargs):
|
||||
email_template = settings.mail_text_order_pending_warning
|
||||
email_subject = settings.mail_subject_order_pending_warning
|
||||
|
||||
try:
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.expire_warning_sent'
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Reminder email could not be sent')
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.expire_warning_sent'
|
||||
)
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
@@ -1543,14 +1521,11 @@ def send_download_reminders(sender, **kwargs):
|
||||
email_template = event.settings.mail_text_download_reminder
|
||||
email_subject = event.settings.mail_subject_download_reminder
|
||||
email_context = get_email_context(event=event, order=o)
|
||||
try:
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Reminder email could not be sent')
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True
|
||||
)
|
||||
|
||||
if event.settings.mail_send_download_reminder_attendee:
|
||||
for p in positions:
|
||||
@@ -1564,14 +1539,11 @@ def send_download_reminders(sender, **kwargs):
|
||||
email_template = event.settings.mail_text_download_reminder_attendee
|
||||
email_subject = event.settings.mail_subject_download_reminder_attendee
|
||||
email_context = get_email_context(event=event, order=o, position=p)
|
||||
try:
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True, position=p
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Reminder email could not be sent to attendee')
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.download_reminder_sent',
|
||||
attach_tickets=True, position=p
|
||||
)
|
||||
|
||||
|
||||
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
|
||||
@@ -1579,13 +1551,10 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
|
||||
email_template = order.event.settings.mail_text_order_changed
|
||||
email_context = get_email_context(event=order.event, order=order)
|
||||
email_subject = order.event.settings.mail_subject_order_changed
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True,
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order changed email could not be sent')
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True,
|
||||
)
|
||||
|
||||
|
||||
class OrderChangeManager:
|
||||
|
||||
@@ -48,7 +48,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CachedFile, Event, User, cachedfile_name
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.base.shredder import ShredError
|
||||
from pretix.celery_app import app
|
||||
@@ -171,21 +171,18 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
|
||||
|
||||
if user:
|
||||
with language(user.locale):
|
||||
try:
|
||||
mail(
|
||||
user.email,
|
||||
_('Data shredding completed'),
|
||||
'pretixbase/email/shred_completed.txt',
|
||||
{
|
||||
'user': user,
|
||||
'organizer': event.organizer.name,
|
||||
'event': str(event.name),
|
||||
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
|
||||
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
|
||||
},
|
||||
event=None,
|
||||
user=user,
|
||||
locale=user.locale,
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
mail(
|
||||
user.email,
|
||||
_('Data shredding completed'),
|
||||
'pretixbase/email/shred_completed.txt',
|
||||
{
|
||||
'user': user,
|
||||
'organizer': event.organizer.name,
|
||||
'event': str(event.name),
|
||||
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
|
||||
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
|
||||
},
|
||||
event=None,
|
||||
user=user,
|
||||
locale=user.locale,
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
|
||||
from pretix.base.i18n import LazyLocaleException
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment,
|
||||
OrderPosition, OrderRefund, QuestionAnswer,
|
||||
OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
|
||||
)
|
||||
from pretix.base.services.invoices import invoice_pdf_task
|
||||
from pretix.base.signals import register_data_shredders
|
||||
@@ -329,6 +329,10 @@ class EmailAddressShredder(BaseDataShredder):
|
||||
sleep_time=2,
|
||||
)
|
||||
|
||||
slow_delete(
|
||||
OutgoingMail.objects.filter(event=self.event)
|
||||
)
|
||||
|
||||
for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total):
|
||||
changed = bool(o.email) or bool(o.customer)
|
||||
o.email = None
|
||||
|
||||
@@ -944,32 +944,40 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
|
||||
|
||||
email_filter = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``message``, ``order``, ``user``
|
||||
Arguments: ``message``, ``order``, ``user``, ``outgoing_mail``
|
||||
|
||||
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
|
||||
return a (possibly modified) copy of the message object passed to you.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
|
||||
The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object
|
||||
might have newer information if a previous plugin already modified the email.
|
||||
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
|
||||
it will be ``None``.
|
||||
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
|
||||
well, otherwise it will be ``None``.
|
||||
|
||||
You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting.
|
||||
"""
|
||||
|
||||
global_email_filter = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer``
|
||||
Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer``, ``outgoing_mail``
|
||||
|
||||
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
|
||||
return a (possibly modified) copy of the message object passed to you.
|
||||
|
||||
This signal is called on all events and even if there is no known event. ``sender`` is an event or None.
|
||||
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
|
||||
The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object
|
||||
might have newer information if a previous plugin already modified the email.
|
||||
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
|
||||
it will be ``None``.
|
||||
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
|
||||
well, otherwise it will be ``None``.
|
||||
|
||||
You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -57,8 +57,9 @@ from pretix.base.forms.widgets import (
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
|
||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel,
|
||||
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
|
||||
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.base.timeframes import (
|
||||
@@ -2815,3 +2816,61 @@ class DeviceFilterForm(FilterForm):
|
||||
qs = qs.order_by('-device_id')
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class OutgoingMailFilterForm(FilterForm):
|
||||
orders = {
|
||||
'date': 'd',
|
||||
'-date': '-d',
|
||||
}
|
||||
query = forms.CharField(
|
||||
label=_('Search email address or subject'),
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Search email address or subject'),
|
||||
}),
|
||||
required=False
|
||||
)
|
||||
event = forms.ModelChoiceField(
|
||||
queryset=Event.objects.none(),
|
||||
label=_('Event'),
|
||||
empty_label=_('All events'),
|
||||
required=False,
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=[
|
||||
('', _('All')),
|
||||
*OutgoingMail.STATUS_CHOICES,
|
||||
],
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['event'].queryset = request.organizer.events.all()
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('query'):
|
||||
query = fdata.get('query')
|
||||
qs = qs.filter(
|
||||
Q(to__containsstring=query.lower())
|
||||
| Q(cc__containsstring=query.lower())
|
||||
| Q(bcc__containsstring=query.lower())
|
||||
| Q(subject__icontains=query)
|
||||
)
|
||||
|
||||
if fdata.get('event'):
|
||||
qs = qs.filter(event=fdata['event'])
|
||||
|
||||
if fdata.get('status'):
|
||||
qs = qs.filter(status=fdata['status'])
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(self.get_order_by())
|
||||
else:
|
||||
qs = qs.order_by("-created", "-pk")
|
||||
|
||||
return qs
|
||||
|
||||
@@ -585,6 +585,7 @@ class MailSettingsForm(SettingsForm):
|
||||
help_text=''.join([
|
||||
str(_("All emails will be sent to this address as a Bcc copy.")),
|
||||
str(_("You can specify multiple recipients separated by commas.")),
|
||||
str(_("Sensitive emails like password resets will not be sent in Bcc.")),
|
||||
]),
|
||||
validators=[multimail_validate],
|
||||
required=False,
|
||||
|
||||
@@ -699,6 +699,8 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
||||
'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'),
|
||||
'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'),
|
||||
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
|
||||
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
|
||||
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
|
||||
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
|
||||
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
|
||||
|
||||
@@ -679,6 +679,15 @@ def get_organizer_navigation(request):
|
||||
'active': (url.url_name == 'organizer.datasync.failedjobs'),
|
||||
}])
|
||||
|
||||
nav.append({
|
||||
'label': _('Outgoing emails'),
|
||||
'url': reverse('control:organizer.outgoingmails', kwargs={
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
'active': 'organizer.outgoingmail' in url.url_name,
|
||||
'icon': 'send',
|
||||
})
|
||||
|
||||
merge_in(nav, sorted(
|
||||
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
|
||||
[]),
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load urlreplace %}
|
||||
{% load icon %}
|
||||
{% load compress %}
|
||||
{% load static %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Outgoing email" %}
|
||||
</h1>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Email details" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-md-12">
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "From" context "email" %}</dt>
|
||||
<dd>{{ mail.sender }}</dd>
|
||||
<dt>{% trans "To" context "email" %}</dt>
|
||||
<dd>{{ mail.to|join:", " }}</dd>
|
||||
{% if mail.cc %}
|
||||
<dt>{% trans "Cc" context "email" %}</dt>
|
||||
<dd>{{ mail.cc|join:", " }}</dd>
|
||||
{% endif %}
|
||||
{% if mail.bcc %}
|
||||
<dt>{% trans "Bcc" context "email" %}</dt>
|
||||
<dd>{{ mail.bcc|join:", " }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Subject" %}</dt>
|
||||
<dd>{{ mail.subject }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if mail.status == "queued" %}
|
||||
<span class="label label-info">{% icon "clock-o" %} {% trans "queued" %}</span>
|
||||
{% elif mail.status == "inflight" %}
|
||||
<span class="label label-info">{% icon "send" %} {% trans "being sent" %}</span>
|
||||
{% elif mail.status == "awaiting_retry" %}
|
||||
<span class="label label-warning">{% icon "repeat" %} {% trans "will be retried" %}</span>
|
||||
{% elif mail.status == "failed" %}
|
||||
<span class="label label-danger">{% icon "warning" %} {% trans "failed" %}</span>
|
||||
{% elif mail.status == "bounced" %}
|
||||
<span class="label label-danger">{% icon "exclamation-circle" %} {% trans "bounced" %}</span>
|
||||
{% elif mail.status == "withheld" %}
|
||||
<span class="label label-warning">{% icon "ban" %} {% trans "withheld" %}</span>
|
||||
{% elif mail.status == "aborted" %}
|
||||
<span class="label label-danger">{% icon "ban" %} {% trans "aborted" %}</span>
|
||||
{% elif mail.status == "sent" %}
|
||||
<span class="label label-success">{% icon "check" %} {% trans "sent" %}</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Creation" %}</dt>
|
||||
<dd>{{ mail.created|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% if mail.sent %}
|
||||
<dt>{% trans "Sent" %}</dt>
|
||||
<dd>{{ mail.sent|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% endif %}
|
||||
{% if mail.retry_after and mail.status == "awaiting_retry" %}
|
||||
<dt>{% trans "Next attempt (estimate)" %}</dt>
|
||||
<dd>{{ mail.retry_after|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% endif %}
|
||||
{% if mail.event %}
|
||||
<dt>{% trans "Event" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url "control:event.index" organizer=request.organizer.slug event=mail.event.slug %}">
|
||||
{{ mail.event }}
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if mail.order %}
|
||||
<dt>{% trans "Order" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url "control:event.order" organizer=request.organizer.slug event=mail.event.slug code=mail.order.code %}">
|
||||
{{ mail.order.code }}</a>{% if mail.orderposition %}-
|
||||
{{ mail.orderposition.positionid }}{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if mail.customer %}
|
||||
<dt>{% trans "Customer" %}</dt>
|
||||
<dd>
|
||||
{% icon "user fa-fw" %}
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=mail.customer.identifier %}">
|
||||
{{ mail.customer }}
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
{% if mail.actual_attachments %}
|
||||
<div class="col-lg-5 col-md-12">
|
||||
<strong>{% trans "Attachments" %}</strong><br>
|
||||
<ul class="list-unstyled">
|
||||
{% for a in mail.actual_attachments %}
|
||||
<li>
|
||||
{% if a.type == "text/calendar" %}
|
||||
{% icon "calendar-plus-o fa-fw" %}
|
||||
{% elif a.type == "application/pdf" %}
|
||||
{% icon "file-pdf-o fa-fw" %}
|
||||
{% elif "image/" in a.type %}
|
||||
{% icon "file-image-o fa-fw" %}
|
||||
{% elif "msword" in a.type or "document" in a.type %}
|
||||
{% icon "file-word-o fa-fw" %}
|
||||
{% elif "excel" in a.type or "spreadsheet" in a.type %}
|
||||
{% icon "file-excel-o fa-fw" %}
|
||||
{% elif "powerpoint" in a.type or "presentation" in a.type %}
|
||||
{% icon "file-powerpoint-o fa-fw" %}
|
||||
{% elif "pkpass" in a.type %}
|
||||
{% icon "qrcode fa-fw" %}
|
||||
{% else %}
|
||||
{% icon "file-o fa-fw" %}
|
||||
{% endif %}
|
||||
{{ a.name }}
|
||||
<span class="text-muted">
|
||||
({{ a.size|filesizeformat }})
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
{% if mail.is_failed %}
|
||||
<li role="presentation" class="active">
|
||||
<a href="#tab-error" role="tab" data-toggle="tab">
|
||||
<span class="fa fa-warning"></span>
|
||||
{% trans "Error" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if mail.body_html %}
|
||||
<li role="presentation"
|
||||
{% if not mail.is_failed %}class="active"{% endif %}>
|
||||
<a href="#tab-html" role="tab" data-toggle="tab">
|
||||
<span class="fa fa-eye"></span>
|
||||
{% trans "HTML content" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li role="presentation"
|
||||
{% if not mail.is_failed and not mail.body_html %}class="active"{% endif %}>
|
||||
<a href="#tab-text" role="tab" data-toggle="tab">
|
||||
<span class="fa fa-file-text-o"></span>
|
||||
{% trans "Text content" %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#tab-headers" role="tab" data-toggle="tab">
|
||||
<span class="fa fa-code"></span>
|
||||
{% trans "Headers" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
{% if mail.is_failed %}
|
||||
<div role="tabpanel" class="tab-pane active" id="tab-error">
|
||||
<strong>
|
||||
{{ mail.error }}
|
||||
</strong>
|
||||
<pre>{{ mail.error_detail }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mail.body_html %}
|
||||
<div role="tabpanel"
|
||||
class="tab-pane {% if not mail.is_failed %}active{% endif %}"
|
||||
id="tab-html">
|
||||
{% if mail.sensitive %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% icon "eye-slash fa-4x" %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Sensitive content not shown for security reasons
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ data_url|json_script:"mail_body_html" }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div role="tabpanel"
|
||||
class="tab-pane {% if not mail.is_failed and not mail.body_html %}active{% endif %}"
|
||||
id="tab-text">
|
||||
{% if mail.sensitive %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% icon "eye-slash fa-4x" %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Sensitive content not shown for security reasons
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<pre><code>{{ mail.body_plain }}</code></pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div role="tabpanel"
|
||||
class="tab-pane"
|
||||
id="tab-headers">
|
||||
<pre><code>{% for k, v in mail.headers.items %}{{ k }}: {{ v }}<br>{% endfor %}</code></pre>
|
||||
<p class="text-muted">
|
||||
{% trans "Additional headers will be added by the mail server and are not visible here." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/outgoingmail.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,185 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load urlreplace %}
|
||||
{% load icon %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Outgoing emails" %}
|
||||
</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed with days=days %}
|
||||
This is an overview of all emails sent by your organizer account in the last {{ days }} days.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if mails|length == 0 and not filter_form.filtered %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You haven't sent any emails recently.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Filter" %}</h3>
|
||||
</div>
|
||||
<form class="panel-body filter-form" action="" method="get">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.query %}
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.status %}
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.event %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form action="{% url "control:organizer.outgoingmails.bulk_action" organizer=request.organizer.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in filter_form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
</th>
|
||||
<th>{% trans "Subject" %}</th>
|
||||
<th>{% trans "Recipients" %}</th>
|
||||
<th>{% trans "Context" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Date" %}
|
||||
<a href="?{% url_replace request 'ordering' '-date' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'date' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<tr class="table-select-all warning hidden">
|
||||
<td>
|
||||
<input type="checkbox" name="__ALL" id="__all"
|
||||
data-results-total="{{ page_obj.paginator.count }}">
|
||||
</td>
|
||||
<td colspan="7">
|
||||
<label for="__all">
|
||||
{% trans "Select all results on other pages as well" %}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in mails %}
|
||||
<tr>
|
||||
<td>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" name="outgoingmail"
|
||||
class="batch-select-checkbox"
|
||||
value="{{ m.pk }}"/></label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url "control:organizer.outgoingmail" organizer=request.organizer.slug mail=m.id %}">
|
||||
{{ m.subject }}
|
||||
</a>
|
||||
{% if m.sensitive %}
|
||||
<span class="text-muted">{% icon "eye-slash" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.to|join:", " }}
|
||||
{% if m.cc %}
|
||||
<br><small class="text-muted">{% trans "Cc" context "email" %}: {{ m.cc|join:", " }}</small>
|
||||
{% endif %}
|
||||
{% if m.bcc %}
|
||||
<br><small class="text-muted">{% trans "Bcc" context "email" %}: {{ m.bcc|join:", " }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if m.event %}
|
||||
<div>
|
||||
{% icon "calendar fa-fw" %}
|
||||
<a href="{% url "control:event.index" organizer=request.organizer.slug event=m.event.slug %}">
|
||||
{{ m.event }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if m.order %}
|
||||
<div>
|
||||
{% icon "shopping-cart fa-fw" %}
|
||||
<a href="{% url "control:event.order" organizer=request.organizer.slug event=m.event.slug code=m.order.code %}">
|
||||
{{ m.order.code }}</a>{% if m.orderposition %}-{{ m.orderposition.positionid }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if m.customer %}
|
||||
<div>
|
||||
{% icon "user fa-fw" %}
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=m.customer.identifier %}">
|
||||
{{ m.customer }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if m.status == "queued" %}
|
||||
<span class="label label-info">{% icon "clock-o" %} {% trans "queued" %}</span>
|
||||
{% elif m.status == "inflight" %}
|
||||
<span class="label label-info">{% icon "send" %} {% trans "being sent" %}</span>
|
||||
{% elif m.status == "awaiting_retry" %}
|
||||
<span class="label label-warning">{% icon "repeat" %} {% trans "will be retried" %}</span>
|
||||
{% elif m.status == "failed" %}
|
||||
<span class="label label-danger">{% icon "warning" %} {% trans "failed" %}</span>
|
||||
{% elif m.status == "bounced" %}
|
||||
<span class="label label-danger">{% icon "exclamation-circle" %} {% trans "bounced" %}</span>
|
||||
{% elif m.status == "withheld" %}
|
||||
<span class="label label-warning">{% icon "ban" %} {% trans "withheld" %}</span>
|
||||
{% elif m.status == "aborted" %}
|
||||
<span class="label label-danger">{% icon "ban" %} {% trans "aborted" %}</span>
|
||||
{% elif m.status == "sent" %}
|
||||
<span class="label label-success">{% icon "check" %} {% trans "sent" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.created|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if m.sent %}
|
||||
<br>
|
||||
<small class="text-muted">{% trans "Sent:" %} {{ m.sent|date:"SHORT_DATETIME_FORMAT" }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:organizer.outgoingmail" organizer=request.organizer.slug mail=m.id %}"
|
||||
class="btn btn-default btn-sm">{% icon "eye" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="batch-select-actions">
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="retry">
|
||||
{% icon "repeat" %}
|
||||
{% trans "Retry (if failed or withheld)" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger btn-save" name="action" value="abort">
|
||||
{% icon "ban" %}
|
||||
{% trans "Abort (if queued, awaiting retry or withheld)" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -38,8 +38,9 @@ from django.views.generic.base import RedirectView
|
||||
|
||||
from pretix.control.views import (
|
||||
auth, checkin, dashboards, datasync, discounts, event, geo,
|
||||
global_settings, item, main, modelimport, oauth, orders, organizer, pdf,
|
||||
search, shredder, subevents, typeahead, user, users, vouchers, waitinglist,
|
||||
global_settings, item, mail, main, modelimport, oauth, orders, organizer,
|
||||
pdf, search, shredder, subevents, typeahead, user, users, vouchers,
|
||||
waitinglist,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -240,6 +241,9 @@ urlpatterns = [
|
||||
name='organizer.gate.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/gate/(?P<gate>[^/]+)/delete$', organizer.GateDeleteView.as_view(),
|
||||
name='organizer.gate.delete'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmails$', mail.OutgoingMailListView.as_view(), name='organizer.outgoingmails'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmail/bulk_action$', mail.OutgoingMailBulkAction.as_view(), name='organizer.outgoingmails.bulk_action'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmail/(?P<mail>[0-9]+)/$', mail.OutgoingMailDetailView.as_view(), name='organizer.outgoingmail'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),
|
||||
|
||||
@@ -66,7 +66,6 @@ from pretix.base.forms.auth import (
|
||||
)
|
||||
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.helpers.http import get_client_ip, redirect_to_url
|
||||
from pretix.helpers.security import handle_login_source
|
||||
|
||||
@@ -347,9 +346,6 @@ class Forgot(TemplateView):
|
||||
except User.DoesNotExist:
|
||||
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
|
||||
|
||||
except SendMailException:
|
||||
logger.exception('Sending password reset email to \"' + email + '\" failed.')
|
||||
|
||||
except RepeatedResetDenied:
|
||||
pass
|
||||
|
||||
|
||||
183
src/pretix/control/views/mail.py
Normal file
183
src/pretix/control/views/mail.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ngettext
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
from pretix.base.models import OutgoingMail
|
||||
from pretix.base.services.mail import mail_send_task
|
||||
from pretix.control.forms.filter import OutgoingMailFilterForm
|
||||
from pretix.control.permissions import OrganizerPermissionRequiredMixin
|
||||
from pretix.control.views.organizer import OrganizerDetailViewMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OutgoingMailQueryMixin:
|
||||
|
||||
@cached_property
|
||||
def request_data(self):
|
||||
if self.request.method == "POST":
|
||||
d = self.request.POST
|
||||
else:
|
||||
d = self.request.GET
|
||||
d = d.copy()
|
||||
return d
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return OutgoingMailFilterForm(
|
||||
data=self.request_data,
|
||||
request=self.request,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.outgoing_mails.select_related(
|
||||
'event', 'order', 'orderposition', 'customer'
|
||||
)
|
||||
|
||||
if 'outgoingmail' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('outgoingmail')
|
||||
)
|
||||
elif self.request.method == 'GET' or '__ALL' in self.request_data:
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
else:
|
||||
raise BadRequest("No mails selected")
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class OutgoingMailListView(OutgoingMailQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = OutgoingMail
|
||||
template_name = 'pretixcontrol/organizers/outgoing_mails.html'
|
||||
# Assume "the highest" permission level for now because emails could belog to any event, order, or customer.
|
||||
# We plan to add a special permissoin in the future
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'mails'
|
||||
paginate_by = 100
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['days'] = int(settings.OUTGOING_MAIL_RETENTION / (24 * 3600))
|
||||
return ctx
|
||||
|
||||
|
||||
class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
model = OutgoingMail
|
||||
template_name = 'pretixcontrol/organizers/outgoing_mail.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'mail'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(OutgoingMail, organizer=self.request.organizer, pk=self.kwargs.get('mail'))
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
if 'Content-Security-Policy' in response:
|
||||
h = _parse_csp(response['Content-Security-Policy'])
|
||||
else:
|
||||
h = {}
|
||||
csps = {
|
||||
'frame-src': ['data:'],
|
||||
}
|
||||
_merge_csp(h, csps)
|
||||
response['Content-Security-Policy'] = _render_csp(h)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
if self.object.body_html:
|
||||
ctx['data_url'] = "data:text/html;charset=utf-8;base64," + base64.b64encode(self.object.body_html.encode()).decode()
|
||||
return ctx
|
||||
|
||||
|
||||
class OutgoingMailBulkAction(OutgoingMailQueryMixin, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin, View):
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get('action') == 'retry':
|
||||
ids = set(
|
||||
self.get_queryset().filter(status__in=OutgoingMail.STATUS_LIST_RETRYABLE).values_list("pk", flat=True)
|
||||
)
|
||||
with transaction.atomic():
|
||||
OutgoingMail.objects.filter(pk__in=ids).update(
|
||||
status=OutgoingMail.STATUS_QUEUED,
|
||||
sent=None,
|
||||
)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.outgoingmails.retried', user=self.request.user, data={
|
||||
'mails': list(ids)
|
||||
}, save=False
|
||||
)
|
||||
for i in ids:
|
||||
mail_send_task.apply_async(kwargs={"outgoing_mail": i})
|
||||
|
||||
messages.success(request, ngettext(
|
||||
"A retry of one email was scheduled.",
|
||||
"A retry of {num} emails was scheduled.",
|
||||
len(ids),
|
||||
).format(num=len(ids)))
|
||||
elif request.POST.get('action') == 'abort':
|
||||
ids = set(
|
||||
self.get_queryset().filter(
|
||||
status__in=(OutgoingMail.STATUS_QUEUED, OutgoingMail.STATUS_AWAITING_RETRY)
|
||||
).values_list("pk", flat=True)
|
||||
)
|
||||
with transaction.atomic():
|
||||
OutgoingMail.objects.filter(pk__in=ids).update(
|
||||
status=OutgoingMail.STATUS_ABORTED,
|
||||
sent=None,
|
||||
)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.organizer.outgoingmails.aborted', user=self.request.user, data={
|
||||
'mails': list(ids)
|
||||
}, save=False
|
||||
)
|
||||
for i in ids:
|
||||
mail_send_task.apply_async(kwargs={"outgoing_mail": i})
|
||||
|
||||
messages.success(request, ngettext(
|
||||
"One email was aborted and will not be sent.",
|
||||
"{num} emails were aborted and will not be sent.",
|
||||
len(ids),
|
||||
).format(num=len(ids)))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizer.outgoingmails', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
@@ -98,9 +98,7 @@ from pretix.base.services.invoices import (
|
||||
invoice_qualified, regenerate_invoice, transmit_invoice,
|
||||
)
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import (
|
||||
SendMailException, prefix_subject, render_mail,
|
||||
)
|
||||
from pretix.base.services.mail import prefix_subject, render_mail
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
|
||||
extend_order, mark_order_expired, mark_order_refunded,
|
||||
@@ -1066,10 +1064,6 @@ class OrderPaymentConfirm(OrderView):
|
||||
messages.error(self.request, str(e))
|
||||
except PaymentException as e:
|
||||
messages.error(self.request, str(e))
|
||||
except SendMailException:
|
||||
messages.warning(self.request,
|
||||
_('The payment has been marked as complete, but we were unable to send a '
|
||||
'confirmation mail.'))
|
||||
else:
|
||||
messages.success(self.request, _('The payment has been marked as complete.'))
|
||||
else:
|
||||
@@ -1540,9 +1534,6 @@ class OrderTransition(OrderView):
|
||||
'message': str(e)
|
||||
})
|
||||
messages.error(self.request, str(e))
|
||||
except SendMailException:
|
||||
messages.warning(self.request, _('The order has been marked as paid, but we were unable to send a '
|
||||
'confirmation mail.'))
|
||||
else:
|
||||
messages.success(self.request, _('The payment has been created successfully.'))
|
||||
elif self.order.cancel_allowed() and to == 'c':
|
||||
@@ -1781,15 +1772,11 @@ class OrderResendLink(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
try:
|
||||
if 'position' in kwargs:
|
||||
p = get_object_or_404(self.order.positions, pk=kwargs['position'])
|
||||
p.resend_link(user=self.request.user)
|
||||
else:
|
||||
self.order.resend_link(user=self.request.user)
|
||||
except SendMailException:
|
||||
messages.error(self.request, _('There was an error sending the mail. Please try again later.'))
|
||||
return redirect(self.get_order_url())
|
||||
if 'position' in kwargs:
|
||||
p = get_object_or_404(self.order.positions, pk=kwargs['position'])
|
||||
p.resend_link(user=self.request.user)
|
||||
else:
|
||||
self.order.resend_link(user=self.request.user)
|
||||
|
||||
messages.success(self.request, _('The email has been queued to be sent.'))
|
||||
return redirect(self.get_order_url())
|
||||
@@ -2433,24 +2420,18 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
}
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
else:
|
||||
try:
|
||||
order.send_mail(
|
||||
form.cleaned_data['subject'], email_template,
|
||||
email_context, 'pretix.event.order.email.custom_sent',
|
||||
self.request.user, auto_email=False,
|
||||
attach_tickets=form.cleaned_data.get('attach_tickets', False),
|
||||
invoices=form.cleaned_data.get('attach_invoices', []),
|
||||
attach_other_files=[a for a in [
|
||||
self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a] if form.cleaned_data.get('attach_new_order', False) else [],
|
||||
)
|
||||
messages.success(self.request,
|
||||
_('Your message has been queued and will be sent to {}.'.format(order.email)))
|
||||
except SendMailException:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Failed to send mail to the following user: {}'.format(order.email))
|
||||
)
|
||||
order.send_mail(
|
||||
form.cleaned_data['subject'], email_template,
|
||||
email_context, 'pretix.event.order.email.custom_sent',
|
||||
self.request.user, auto_email=False,
|
||||
attach_tickets=form.cleaned_data.get('attach_tickets', False),
|
||||
invoices=form.cleaned_data.get('attach_invoices', []),
|
||||
attach_other_files=[a for a in [
|
||||
self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a] if form.cleaned_data.get('attach_new_order', False) else [],
|
||||
)
|
||||
messages.success(self.request,
|
||||
_('Your message has been queued and will be sent to {}.'.format(order.email)))
|
||||
return super(OrderSendMail, self).form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
@@ -2503,23 +2484,19 @@ class OrderPositionSendMail(OrderSendMail):
|
||||
}
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
else:
|
||||
try:
|
||||
position.send_mail(
|
||||
form.cleaned_data['subject'],
|
||||
email_template,
|
||||
email_context,
|
||||
'pretix.event.order.position.email.custom_sent',
|
||||
self.request.user,
|
||||
attach_tickets=form.cleaned_data.get('attach_tickets', False),
|
||||
attach_other_files=[a for a in [
|
||||
self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a] if form.cleaned_data.get('attach_new_order', False) else [],
|
||||
)
|
||||
messages.success(self.request,
|
||||
_('Your message has been queued and will be sent to {}.'.format(position.attendee_email)))
|
||||
except SendMailException:
|
||||
messages.error(self.request,
|
||||
_('Failed to send mail to the following user: {}'.format(position.attendee_email)))
|
||||
position.send_mail(
|
||||
form.cleaned_data['subject'],
|
||||
email_template,
|
||||
email_context,
|
||||
'pretix.event.order.position.email.custom_sent',
|
||||
self.request.user,
|
||||
attach_tickets=form.cleaned_data.get('attach_tickets', False),
|
||||
attach_other_files=[a for a in [
|
||||
self.request.event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
|
||||
] if a] if form.cleaned_data.get('attach_new_order', False) else [],
|
||||
)
|
||||
messages.success(self.request,
|
||||
_('Your message has been queued and will be sent to {}.'.format(position.attendee_email)))
|
||||
return super(OrderSendMail, self).form_valid(form)
|
||||
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
from pretix.base.services.export import multiexport, scheduled_organizer_export
|
||||
from pretix.base.services.mail import SendMailException, mail, prefix_subject
|
||||
from pretix.base.services.mail import mail, prefix_subject
|
||||
from pretix.base.signals import register_multievent_data_exporters
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
@@ -1037,24 +1037,21 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
return ctx
|
||||
|
||||
def _send_invite(self, instance):
|
||||
try:
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.request.organizer.name,
|
||||
'team': instance.team.name,
|
||||
'url': build_global_uri('control:auth.invite', kwargs={
|
||||
'token': instance.token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Already logged
|
||||
mail(
|
||||
instance.email,
|
||||
_('pretix account invitation'),
|
||||
'pretixcontrol/email/invitation.txt',
|
||||
{
|
||||
'user': self,
|
||||
'organizer': self.request.organizer.name,
|
||||
'team': instance.team.name,
|
||||
'url': build_global_uri('control:auth.invite', kwargs={
|
||||
'token': instance.token
|
||||
})
|
||||
},
|
||||
event=None,
|
||||
locale=self.request.LANGUAGE_CODE
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -3027,6 +3024,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
locale=self.customer.locale,
|
||||
customer=self.customer,
|
||||
organizer=self.request.organizer,
|
||||
sensitive=True,
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
|
||||
@@ -41,7 +41,6 @@ from hijack import signals
|
||||
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.models import User
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.control.forms.filter import UserFilterForm
|
||||
from pretix.control.forms.users import UserEditForm
|
||||
from pretix.control.permissions import AdministratorPermissionRequiredMixin
|
||||
@@ -139,11 +138,7 @@ class UserResetView(AdministratorPermissionRequiredMixin, RecentAuthenticationRe
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
|
||||
try:
|
||||
self.object.send_password_reset()
|
||||
except SendMailException:
|
||||
messages.error(request, _('There was an error sending the mail. Please try again later.'))
|
||||
return redirect(self.get_success_url())
|
||||
self.object.send_password_reset()
|
||||
|
||||
self.object.log_action('pretix.control.auth.user.forgot_password.mail_sent',
|
||||
user=request.user)
|
||||
|
||||
@@ -25,7 +25,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import (
|
||||
Aggregate, Expression, F, Field, Lookup, OrderBy, Value,
|
||||
Aggregate, Expression, F, Field, JSONField, Lookup, OrderBy, Value,
|
||||
)
|
||||
from django.utils.functional import lazy
|
||||
|
||||
@@ -154,6 +154,19 @@ class NotEqual(Lookup):
|
||||
return '%s <> %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
@JSONField.register_lookup
|
||||
class ContainsString(Lookup):
|
||||
lookup_name = 'containsstring'
|
||||
|
||||
def as_sql(self, compiler, connection):
|
||||
if connection.vendor != "postgresql":
|
||||
raise NotImplementedError("Lookup in JSON Array not supported on this database")
|
||||
lhs, lhs_params = self.process_lhs(compiler, connection)
|
||||
rhs, rhs_params = self.process_rhs(compiler, connection)
|
||||
params = lhs_params + rhs_params
|
||||
return '%s ? %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
class PostgresWindowFrame(Expression):
|
||||
template = "%(frame_type)s BETWEEN %(start)s AND %(end)s"
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from django_countries.fields import Country
|
||||
from geoip2.errors import AddressNotFoundError
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
@@ -159,21 +159,18 @@ def handle_login_source(user, request):
|
||||
})
|
||||
if user.known_login_sources.count() > 1:
|
||||
# Do not send on first login or first login after introduction of this feature:
|
||||
try:
|
||||
with language(user.locale):
|
||||
mail(
|
||||
user.email,
|
||||
_('Login from new source detected'),
|
||||
'pretixcontrol/email/login_notice.txt',
|
||||
{
|
||||
'source': src,
|
||||
'country': Country(str(country)).name if country else _('Unknown country'),
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'url': build_absolute_uri('control:user.settings')
|
||||
},
|
||||
event=None,
|
||||
user=user,
|
||||
locale=user.locale
|
||||
)
|
||||
except SendMailException:
|
||||
pass # Not much we can do
|
||||
with language(user.locale):
|
||||
mail(
|
||||
user.email,
|
||||
_('Login from new source detected'),
|
||||
'pretixcontrol/email/login_notice.txt',
|
||||
{
|
||||
'source': src,
|
||||
'country': Country(str(country)).name if country else _('Unknown country'),
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'url': build_absolute_uri('control:user.settings')
|
||||
},
|
||||
event=None,
|
||||
user=user,
|
||||
locale=user.locale
|
||||
)
|
||||
|
||||
@@ -56,7 +56,6 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import change_payment_provider
|
||||
from pretix.base.services.tasks import TransactionAwareTask
|
||||
from pretix.celery_app import app
|
||||
@@ -72,13 +71,10 @@ def notify_incomplete_payment(o: Order):
|
||||
email_context = get_email_context(event=o.event, order=o, pending_sum=o.pending_sum)
|
||||
email_subject = o.event.settings.mail_subject_order_incomplete_payment
|
||||
|
||||
try:
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.expire_warning_sent'
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Reminder email could not be sent')
|
||||
o.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
'pretix.event.order.email.expire_warning_sent'
|
||||
)
|
||||
|
||||
|
||||
def cancel_old_payments(order):
|
||||
@@ -288,9 +284,6 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, regex_match_to_s
|
||||
except Quota.QuotaExceededException:
|
||||
# payment confirmed but order status could not be set, no longer problem of this plugin
|
||||
cancel_old_payments(order)
|
||||
except SendMailException:
|
||||
# payment confirmed but order status could not be set, no longer problem of this plugin
|
||||
cancel_old_payments(order)
|
||||
else:
|
||||
cancel_old_payments(order)
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ from localflavor.generic.forms import BICFormField, IBANFormField
|
||||
|
||||
from pretix.base.forms.widgets import DatePickerWidget
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.permissions import (
|
||||
@@ -160,11 +159,6 @@ class ActionView(View):
|
||||
p.confirm(user=self.request.user)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
except SendMailException:
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': _('Problem sending email.')
|
||||
})
|
||||
trans.state = BankTransaction.STATE_VALID
|
||||
trans.save()
|
||||
trans.order.payments.filter(
|
||||
|
||||
@@ -57,7 +57,6 @@ from pretix.base.decimal import round_decimal
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.paypal.api import Api
|
||||
@@ -468,9 +467,6 @@ class Paypal(BasePaymentProvider):
|
||||
payment_obj.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
|
||||
except SendMailException:
|
||||
messages.warning(request, _('There was an error sending the confirmation mail.'))
|
||||
return None
|
||||
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
|
||||
@@ -54,7 +54,6 @@ from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.forms.questions import guess_country
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
@@ -821,9 +820,6 @@ class PaypalMethod(BasePaymentProvider):
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
|
||||
except SendMailException:
|
||||
messages.warning(request, _('There was an error sending the confirmation mail.'))
|
||||
finally:
|
||||
if 'payment_paypal_oid' in request.session:
|
||||
del request.session['payment_paypal_oid']
|
||||
|
||||
@@ -38,7 +38,6 @@ from pretix.base.models import (
|
||||
fields,
|
||||
)
|
||||
from pretix.base.models.base import LoggingMixin
|
||||
from pretix.base.services.mail import SendMailException
|
||||
|
||||
|
||||
class ScheduledMail(models.Model):
|
||||
@@ -180,13 +179,10 @@ class ScheduledMail(models.Model):
|
||||
invoice_address=ia,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
try:
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
except SendMailException:
|
||||
... # ¯\_(ツ)_/¯
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
|
||||
if send_to_attendees:
|
||||
if not self.rule.all_products:
|
||||
@@ -195,31 +191,28 @@ class ScheduledMail(models.Model):
|
||||
positions = [p for p in positions if p.subevent_id == self.subevent_id]
|
||||
|
||||
for p in positions:
|
||||
try:
|
||||
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
position=p,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
|
||||
elif not o_sent and o.email:
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
except SendMailException:
|
||||
... # ¯\_(ツ)_/¯
|
||||
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
position=p,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
|
||||
elif not o_sent and o.email:
|
||||
email_ctx = get_email_context(
|
||||
event=e,
|
||||
order=o,
|
||||
invoice_address=ia,
|
||||
event_or_subevent=self.subevent or e,
|
||||
)
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
attach_ical=self.rule.attach_ical,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
|
||||
self.last_successful_order_id = o.pk
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Event, InvoiceAddress, Order, User,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.format import format_map
|
||||
@@ -61,7 +61,6 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
recipients: str, filter_checkins: bool, not_checked_in: bool, checkin_lists: list,
|
||||
attachments: list = None, attach_tickets: bool = False,
|
||||
attach_ical: bool = False) -> None:
|
||||
failures = []
|
||||
user = User.objects.get(pk=user) if user else None
|
||||
subject = LazyI18nString(subject)
|
||||
message = LazyI18nString(message)
|
||||
@@ -121,70 +120,64 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
if subevents_to and p.subevent.date_from >= subevents_to:
|
||||
continue
|
||||
|
||||
try:
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
|
||||
mail(
|
||||
p.attendee_email,
|
||||
subject,
|
||||
message,
|
||||
email_context,
|
||||
event,
|
||||
locale=o.locale,
|
||||
order=o,
|
||||
position=p,
|
||||
attach_tickets=attach_tickets,
|
||||
attach_ical=attach_ical,
|
||||
attach_cached_files=attachments
|
||||
)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent.attendee',
|
||||
user=user,
|
||||
data={
|
||||
'position': p.positionid,
|
||||
'subject': format_map(subject.localize(o.locale), email_context),
|
||||
'message': format_map(message.localize(o.locale), email_context),
|
||||
'recipient': p.attendee_email,
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': attachments_for_log,
|
||||
}
|
||||
)
|
||||
except SendMailException:
|
||||
failures.append(p.attendee_email)
|
||||
|
||||
if send_to_order and o.email:
|
||||
try:
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia)
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
|
||||
mail(
|
||||
o.email,
|
||||
p.attendee_email,
|
||||
subject,
|
||||
message,
|
||||
email_context,
|
||||
event,
|
||||
locale=o.locale,
|
||||
order=o,
|
||||
position=p,
|
||||
attach_tickets=attach_tickets,
|
||||
attach_ical=attach_ical,
|
||||
attach_cached_files=attachments,
|
||||
attach_cached_files=attachments
|
||||
)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
'pretix.plugins.sendmail.order.email.sent.attendee',
|
||||
user=user,
|
||||
data={
|
||||
'position': p.positionid,
|
||||
'subject': format_map(subject.localize(o.locale), email_context),
|
||||
'message': format_map(message.localize(o.locale), email_context),
|
||||
'recipient': o.email,
|
||||
'recipient': p.attendee_email,
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': attachments_for_log,
|
||||
}
|
||||
)
|
||||
except SendMailException:
|
||||
failures.append(o.email)
|
||||
|
||||
if send_to_order and o.email:
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia)
|
||||
mail(
|
||||
o.email,
|
||||
subject,
|
||||
message,
|
||||
email_context,
|
||||
event,
|
||||
locale=o.locale,
|
||||
order=o,
|
||||
attach_tickets=attach_tickets,
|
||||
attach_ical=attach_ical,
|
||||
attach_cached_files=attachments,
|
||||
)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
user=user,
|
||||
data={
|
||||
'subject': format_map(subject.localize(o.locale), email_context),
|
||||
'message': format_map(message.localize(o.locale), email_context),
|
||||
'recipient': o.email,
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': attachments_for_log,
|
||||
}
|
||||
)
|
||||
|
||||
for chunk in _chunks(objects, 1000):
|
||||
orders = Order.objects.filter(pk__in=chunk, event=event)
|
||||
|
||||
@@ -71,7 +71,6 @@ from pretix.base.payment import (
|
||||
BasePaymentProvider, PaymentException, WalletQueries,
|
||||
)
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
@@ -1000,9 +999,6 @@ class StripeMethod(BasePaymentProvider):
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
|
||||
except SendMailException:
|
||||
raise PaymentException(_('There was an error sending the confirmation mail.'))
|
||||
elif intent.status == 'processing':
|
||||
if request:
|
||||
messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the '
|
||||
|
||||
@@ -1654,11 +1654,6 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
order = Order.objects.get(id=value)
|
||||
return self.get_order_url(order)
|
||||
|
||||
def get_error_message(self, exception):
|
||||
if exception.__class__.__name__ == 'SendMailException':
|
||||
return _('There was an error sending the confirmation mail. Please try again later.')
|
||||
return super().get_error_message(exception)
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_step_url(self.request)
|
||||
|
||||
|
||||
@@ -325,6 +325,7 @@ class ResetPasswordView(FormView):
|
||||
locale=customer.locale,
|
||||
customer=customer,
|
||||
organizer=self.request.organizer,
|
||||
sensitive=True,
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
|
||||
@@ -77,7 +77,6 @@ from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
|
||||
invoice_qualified,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _try_auto_refund, cancel_order,
|
||||
change_payment_provider, error_messages,
|
||||
@@ -656,10 +655,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
|
||||
amount=Decimal('0.00'),
|
||||
fee=None
|
||||
)
|
||||
try:
|
||||
p.confirm()
|
||||
except SendMailException:
|
||||
pass
|
||||
p.confirm()
|
||||
else:
|
||||
p._mark_order_paid(
|
||||
payment_refund_sum=self.order.payment_refund_sum
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.utils.functional import cached_property
|
||||
@@ -42,7 +40,7 @@ from django.views import View
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.services.mail import INVALID_ADDRESS, SendMailException, mail
|
||||
from pretix.base.services.mail import INVALID_ADDRESS, mail
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.forms.user import ResendLinkForm
|
||||
@@ -83,13 +81,7 @@ class ResendLinkView(EventViewMixin, TemplateView):
|
||||
subject = self.request.event.settings.mail_subject_resend_all_links
|
||||
template = self.request.event.settings.mail_text_resend_all_links
|
||||
context = get_email_context(event=self.request.event, orders=orders)
|
||||
try:
|
||||
mail(user, subject, template, context, event=self.request.event, locale=self.request.LANGUAGE_CODE)
|
||||
except SendMailException:
|
||||
logger = logging.getLogger('pretix.presale.user')
|
||||
logger.exception('A mail resending order links to {} could not be sent.'.format(user))
|
||||
messages.error(self.request, _('We have trouble sending emails right now, please check back later.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
mail(user, subject, template, context, event=self.request.event, locale=self.request.LANGUAGE_CODE)
|
||||
|
||||
messages.success(self.request, _('If there were any orders by this user, they will receive an email with their order codes.'))
|
||||
return redirect_to_url(eventreverse(self.request.event, 'presale:event.index'))
|
||||
|
||||
@@ -855,6 +855,8 @@ COUNTRIES_OVERRIDE = {
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 25000
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
OUTGOING_MAIL_RETENTION = 14 * 24 * 3600 # 14 days in seonds
|
||||
|
||||
# File sizes are in MiB
|
||||
FILE_UPLOAD_MAX_SIZE_IMAGE = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_image", fallback=10)
|
||||
FILE_UPLOAD_MAX_SIZE_FAVICON = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_favicon", fallback=1)
|
||||
|
||||
43
src/pretix/static/pretixcontrol/js/ui/outgoingmail.js
Normal file
43
src/pretix/static/pretixcontrol/js/ui/outgoingmail.js
Normal file
@@ -0,0 +1,43 @@
|
||||
function is_sandbox_supported() {
|
||||
const iframe = document.createElement('iframe');
|
||||
return 'sandbox' in iframe;
|
||||
}
|
||||
|
||||
function safe_render(url, parent) {
|
||||
// Estimate the height that prevents the user from having to scroll on two levels to see the full email
|
||||
const height = (
|
||||
window.innerHeight - parent.parent().get(0).getBoundingClientRect().top - document.querySelector("footer").getBoundingClientRect().height - 20
|
||||
) + "px";
|
||||
|
||||
const iframe = (
|
||||
// Per the HTML spec, a data: URL in an iframe is treated as its own origin:
|
||||
// https://github.com/whatwg/html/pull/1756
|
||||
// It is unclear, if Firefox complies, and the behaviour around data URLs is quite wild:
|
||||
// https://github.com/whatwg/html/issues/12091
|
||||
// Together with the sandbox attribute disallowing all JavaScript, and the fact
|
||||
// that we sanitize the HTML before we even save it to the database, this should
|
||||
// still be the safest way to render HTML in the context of our backend.
|
||||
$("<iframe>")
|
||||
.height(height)
|
||||
.attr("class", "html-email")
|
||||
.attr("src", url)
|
||||
.attr("sandbox", "allow-popups allow-popups-to-escape-sandbox")
|
||||
.attr("csp", "script-src 'none'; font-src 'none'; connect-src 'none'; form-action 'none'") // respected only by chrome
|
||||
.prop("credentialless", true) // respected only by chrome
|
||||
);
|
||||
|
||||
console.log(parent, iframe);
|
||||
parent.append(iframe);
|
||||
}
|
||||
|
||||
$(function () {
|
||||
const script_element = $("#mail_body_html");
|
||||
if (!script_element.length) return;
|
||||
if (!is_sandbox_supported()) {
|
||||
// Browser is too old for <iframe sandbox>
|
||||
$(script_element.parent()).text("Please switch to a modern browser to view HTML content safely.");
|
||||
return;
|
||||
}
|
||||
|
||||
safe_render(JSON.parse(script_element.html()), script_element.parent());
|
||||
});
|
||||
@@ -91,3 +91,8 @@ div.mail-preview {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
iframe.html-email {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -29,3 +29,8 @@ class FailingEmailBackend(EmailBackend):
|
||||
raise smtplib.SMTPRecipientsRefused({
|
||||
'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
|
||||
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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user