Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel
e445c4d11e draft html rendering 2026-02-17 09:27:39 +01:00
Raphael Michel
162205ffaf Mail: Handle all rendering in mail.py, return values for log 2026-02-16 13:40:17 +01:00
18 changed files with 193 additions and 181 deletions

View File

@@ -22,7 +22,7 @@
import logging
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
from typing import TypeVar, Union
import bleach
import css_inline
@@ -31,6 +31,7 @@ from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils.safestring import mark_safe
from django.utils.translation import get_language, gettext_lazy as _
from pretix.base.models import Event
@@ -39,7 +40,9 @@ from pretix.base.templatetags.rich_text import (
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
markdown_compile_email, truelink_callback,
)
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
from pretix.helpers.format import (
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
@@ -83,8 +86,8 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
position=None, context=None) -> str:
def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
subject: str, order=None, position=None, context=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -140,27 +143,37 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def compile_markdown(self, plaintext, context=None):
return markdown_compile_email(plaintext, context=context)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
apply_format_map = not isinstance(plain_body, FormattedString)
body_md = self.compile_markdown(plain_body, context)
if context:
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
if apply_format_map:
body_md = format_map(
body_md,
def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
subject: str, order=None, position=None, context=None) -> str:
if isinstance(content, FormattedString):
# Raw string that is already formatted but not markdown-rendered
body_content_html = self.compile_markdown(content, context)
elif isinstance(content, PlainHtmlAlternativeString):
# HTML already rendered by Django templates
body_content_html = content.html
else:
# Raw string that is not yet formatted or markdown-rendered
body_content_html = self.compile_markdown(content, context)
if context:
linker = bleach.Linker(
url_re=URL_RE,
email_re=EMAIL_RE,
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
body_content_html = format_map(
body_content_html,
context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker
)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
'body': body_md,
'body': mark_safe(body_content_html),
'subject': str(subject),
'color': settings.PRETIX_PRIMARY_COLOR,
'rtl': get_language() in settings.LANGUAGES_RTL or get_language().split('-')[0] in settings.LANGUAGES_RTL,

View File

@@ -33,8 +33,7 @@ from pretix.base.invoicing.transmission import (
transmission_types,
)
from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.services.mail import mail, render_mail
from pretix.helpers.format import format_map
from pretix.base.services.mail import mail
@transmission_types.new()
@@ -134,9 +133,7 @@ class EmailTransmissionProvider(TransmissionProvider):
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
# 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(
outgoing_mail = mail(
[recipient],
subject,
template,
@@ -151,19 +148,10 @@ class EmailTransmissionProvider(TransmissionProvider):
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': [],
}
)
if outgoing_mail:
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data=outgoing_mail.log_data()
)

View File

@@ -220,3 +220,20 @@ class OutgoingMail(models.Model):
error_log_action_type = 'pretix.email.error'
log_target = None
return log_target, error_log_action_type
def log_data(self):
return {
"subject": self.subject,
"message": self.body_plain,
"to": self.to,
"cc": self.cc,
"bcc": self.bcc,
"invoices": [i.pk for i in self.should_attach_invoices.all()],
"attach_tickets": self.should_attach_tickets,
"attach_ical": self.should_attach_ical,
"attach_other_files": self.should_attach_other_files,
"attach_cached_files": [cf.filename for cf in self.should_attach_cached_files.all()],
"position": self.orderposition.positionid if self.orderposition else None,
}

View File

@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import FormattedString, format_map
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -1167,7 +1166,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 mail, render_mail
from pretix.base.services.mail import mail
if not self.email and not (position and position.attendee_email):
return
@@ -1177,32 +1176,20 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email:
recipient = position.attendee_email
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
outgoing_mail = 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 [],
}
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
def resend_link(self, user=None, auth=None):
with language(self.locale, self.event.settings.region):
@@ -2900,17 +2887,14 @@ 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 mail, render_mail
from pretix.base.services.mail import mail
if not self.attendee_email:
return
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
@@ -2919,21 +2903,13 @@ class OrderPosition(AbstractPosition):
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': [],
}
)
if outgoing_mail:
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
def resend_link(self, user=None, auth=None):

View File

@@ -34,10 +34,9 @@ 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 mail, render_mail
from pretix.base.services.mail import mail
from pretix.helpers import OF_SELF
from ...helpers.format import format_map
from ...helpers.names import build_name
from .base import LoggedModel
from .event import Event, SubEvent
@@ -272,9 +271,7 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region):
recipient = self.email
email_content = render_mail(template, context)
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event,
self.locale,
@@ -284,18 +281,13 @@ class WaitingListEntry(LoggedModel):
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 [],
}
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
@staticmethod
def clean_itemvar(event, item, variation):

View File

@@ -1295,6 +1295,7 @@ class ManualPayment(BasePaymentProvider):
def format_map(self, order, payment):
return {
# Possible placeholder injection, we should make sure to never include user-controlled variables here
'order': order.code,
'amount': payment.amount,
'currency': self.event.currency,

View File

@@ -45,7 +45,6 @@ from pretix.base.services.tax import split_fee_for_taxes
from pretix.base.templatetags.money import money_filter
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__)
@@ -55,7 +54,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
mail(
wle.email,
format_map(subject, email_context),
str(subject),
message,
email_context,
wle.event,
@@ -73,9 +72,8 @@ 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)
order.send_mail(
real_subject, message, email_context,
subject, message, email_context,
'pretix.event.order.email.event_canceled',
user,
)
@@ -85,14 +83,13 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = format_map(subject, email_context)
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
event=order.event,
refund_amount=refund_amount,
position_or_address=p,
order=order, position=p)
order.send_mail(
real_subject, message, email_context,
subject, message, email_context,
'pretix.event.order.email.event_canceled',
position=p,
user=user

View File

@@ -58,6 +58,7 @@ from django.core.mail.message import SafeMIMEText
from django.db import connection, transaction
from django.db.models import Q
from django.dispatch import receiver
from django.template import Context
from django.template.loader import get_template
from django.utils.html import escape
from django.utils.timezone import now, override
@@ -149,13 +150,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
return subject
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], template: Union[str, LazyI18nString],
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None,
sensitive: bool=False):
sensitive: bool=False) -> Optional[OutgoingMail]:
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -261,17 +262,22 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
_autoextend_context(context, order)
# Build raw content
content_plain = render_mail(template, context, placeholder_mode=None)
content = render_mail(template, context, placeholder_mode=None)
if settings_holder:
signature = str(settings_holder.settings.get('mail_text_signature'))
else:
signature = ""
# Build full plain-text body
if not isinstance(content_plain, FormattedString):
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
if isinstance(content, FormattedString):
# Already formatted by render_mail() from format_values
body_plain = content
elif isinstance(content, PlainHtmlAlternativeString):
# Already formatted by render_mail() form a django template
body_plain = content.plain
else:
body_plain = content_plain
# Not yet formatted
body_plain = format_map(content, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
# Build subject
@@ -298,19 +304,19 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
try:
if 'context' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
body_html = renderer.render(content, signature, raw_subject, order, position, context)
elif 'position' in inspect.signature(renderer.render).parameters:
# Backwards compatibility
warnings.warn('Email renderer called without context argument because context argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
body_html = renderer.render(content, signature, raw_subject, order, position)
else:
# Backwards compatibility
warnings.warn('Email renderer called without position argument because position argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order)
body_html = renderer.render(content, signature, raw_subject, order)
except:
logger.exception('Could not render HTML body')
body_html = None
@@ -335,14 +341,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
should_attach_other_files=attach_other_files or [],
sensitive=sensitive,
)
m._prefetched_objects_cache = {}
if invoices and not position:
m.should_attach_invoices.add(*invoices)
# Hack: For logging, we'll later make a `should_attach_invoices.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = invoices
else:
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = Invoice.objects.none()
if attach_cached_files:
cf_list = []
for cf in attach_cached_files:
if not isinstance(cf, CachedFile):
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf))
else:
m.should_attach_cached_files.add(cf)
cf = CachedFile.objects.get(pk=cf)
m.should_attach_cached_files.add(cf)
cf_list.append(cf)
# Hack: For logging, we'll later make a `should_attach_cached_files.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = cf_list
else:
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = CachedFile.objects.none()
send_task = mail_send_task.si(
outgoing_mail=m.id
@@ -364,6 +382,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
lambda: chain(*task_chain).apply_async()
)
return m
class CustomEmail(EmailMultiAlternatives):
def _create_mime_attachment(self, content, mimetype):
@@ -796,15 +816,22 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
body = str(template)
if context and placeholder_mode:
body = format_map(body, context, mode=placeholder_mode)
return body
else:
tpl = get_template(template)
context = {
# Known bug, should behave differently for plain and HTML but we'll fix after security release
plain_context = Context({
k: v.plain if isinstance(v, PlainHtmlAlternativeString) else v
for k, v in context.items()
} | {"to_html": False}, autoescape=False)
html_context = Context({
k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
for k, v in context.items()
}
body = FormattedString(tpl.render(context))
return body
} | {"to_html": True}, autoescape=True)
return PlainHtmlAlternativeString(
plain=tpl.template.render(plain_context),
html=tpl.template.render(html_context),
)
def replace_images_with_cid_paths(body_html):

View File

@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list])
mail(
outgoing_mail = mail(
r['email'],
subject,
LazyI18nString(message),
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
data={
'recipient': r['email'],
'name': r.get('name'),
'subject': subject,
'message': message,
'subject': outgoing_mail.subject,
'message': outgoing_mail.body_plain,
},
save=False
))

View File

@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
le.save(update_fields=['data', 'shredded'])
else:
shred_log_fields(le, banlist=[
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc',
])

View File

@@ -8,6 +8,9 @@
<h1>{% trans "Not found" %}</h1>
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
<p>{{ exception }}</p>
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
</p>
{% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<p>

View File

@@ -24,7 +24,9 @@
{% if log.display %}
<br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }}
{% endif %}
{% if log.parsed_data.recipient %}
{% if log.parsed_data.to %}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.to|join:", " }}
{% elif log.parsed_data.recipient %} {# legacy #}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
{% endif %}
</p>

View File

@@ -2413,9 +2413,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
@@ -2477,9 +2477,9 @@ class OrderPositionSendMail(OrderSendMail):
with language(position.order.locale, self.request.event.settings.region):
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(position.order.event, escape(email_subject), highlight=True))

View File

@@ -38,13 +38,10 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, Event, InvoiceAddress, Order, User,
)
from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
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
def _chunks(lst, n):
@@ -64,7 +61,6 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
user = User.objects.get(pk=user) if user else None
subject = LazyI18nString(subject)
message = LazyI18nString(message)
attachments_for_log = [cf.filename for cf in CachedFile.objects.filter(pk__in=attachments)] if attachments else []
def _send_to_order(o):
send_to_order = recipients in ('both', 'orders')
@@ -122,7 +118,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
mail(
outgoing_mail = mail(
p.attendee_email,
subject,
message,
@@ -135,25 +131,17 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
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,
}
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data=outgoing_mail.log_data(),
)
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(
outgoing_mail = mail(
o.email,
subject,
message,
@@ -165,19 +153,12 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
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,
}
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data=outgoing_mail.log_data(),
)
for chunk in _chunks(objects, 1000):
orders = Order.objects.filter(pk__in=chunk, event=event)

View File

@@ -1,14 +1,14 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Resend order link" %}{% endblock %}
{% block title %}{% trans "Resend order links" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
<meta name="robots" content="noindex, nofollow">
{% endblock %}
{% block content %}
<h2>
{% trans "Resend order link" %}
{% trans "Resend order links" %}
</h2>
<p>
{% blocktrans trimmed %}

View File

@@ -1510,10 +1510,7 @@ class OrderChangeMixin:
'max_count': iao.max_count,
'iao': iao,
'items': [i for i in items if not i.require_voucher],
'items_missing': {
k: v for k, v in current_addon_products_missing.items()
if v and k[0].category_id == iao.addon_category_id
},
'items_missing': {k: v for k, v in current_addon_products_missing.items() if v},
})
return positions

View File

@@ -42,7 +42,6 @@ import pytest
from django.conf import settings
from django.core import mail as djmail
from django.test import override_settings
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled
@@ -332,13 +331,14 @@ def test_placeholder_html_rendering_from_template(env):
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
# Known bug for now: These should not have HTML for the plain body, but we'll fix this safter the security release
assert escape('Event name: <strong>event & co. kg</strong> {currency}') in djmail.outbox[0].body
assert '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body
assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body
assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body
# todo: assert '&lt;' not in djmail.outbox[0].body
# todo: assert '&amp;' not in djmail.outbox[0].body
assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
assert 'Event: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
assert '**Meta**: *Beep*' in djmail.outbox[0].body
assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body
assert '<a ' not in djmail.outbox[0].body
assert '&lt;' not in djmail.outbox[0].body
assert '&amp;' not in djmail.outbox[0].body
assert 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body
assert 'EUR' not in djmail.outbox[0].body
html = _extract_html(djmail.outbox[0])
@@ -346,11 +346,13 @@ def test_placeholder_html_rendering_from_template(env):
assert '<strong>event' not in html
assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}' in html
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
assert '<strong>Meta</strong>: <em>Beep</em>' in html
assert '**Meta**: <em>Beep</em>' in html
assert 'Unevaluated placeholder: {currency}' in html
assert 'EUR' not in html
assert 'Event website: [&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}](https://example.org/dummy)' in html
# Links are from raw HTML and therefore trusted, rel and target is not added automatically
assert re.search(
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
r'Event: <a href="https://example.com/dummy" style="[^"]+">'
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html
)

View File

@@ -1,13 +1,29 @@
{% load i18n %}
This is a test file for sending mails.
Django variables will get evaluated:
Event name: {{ event }}
pretix variables will not in a template rendering:
Unevaluated placeholder: {currency}
We can use advanced Django things:
{% get_current_language as LANGUAGE_CODE %}
The language code used for rendering this email is {{ LANGUAGE_CODE }}.
Custom content is rendered safely without HTML/Markdown/parameter injection
unless the parameter is marked as "HTML-safe":
Payment info:
{{ payment_info }}
**Meta**: {{ meta_Test }}
Event website: [{{event}}](https://example.org/{{event_slug}})
Markdown will not be evaluated when coming from a template file!
Event website: [{{event}}](https://example.org/{{event_slug}})
Event: {% if to_html %}<a href="https://example.com/{{event_slug}}">{% endif %}{{ event }}{% if to_html %}</a>{% endif %}