Compare commits

..

19 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
Raphael Michel
f24429a7c5 Fix tests on Python <3.11 2026-02-16 13:40:00 +01:00
Raphael Michel
29ed07ccce Merge branch 'pajowu/security-plaintext-placeholder' into 'master'
SECURITY: Prevent placeholder injection in plaintext emails

See merge request pretix/pretix!21
2026-02-16 10:59:44 +01:00
Nate Horst
dd0cd7ab0b Translations: Update Thai
Currently translated at 36.0% (2237 of 6207 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/th/

powered by weblate
2026-02-16 10:44:21 +01:00
Nate Horst
d7df906995 Translations: Update Thai
Currently translated at 36.0% (2237 of 6207 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/th/

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
839f4b4657 Translations: Update Dutch (Belgium)
Currently translated at 0.1% (12 of 6207 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
74f7e1f61c Translations: Add Dutch (Belgium) 2026-02-16 10:44:21 +01:00
Yasunobu YesNo Kawaguchi
47919afab0 Translations: Update Japanese
Currently translated at 100.0% (256 of 256 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ja/

powered by weblate
2026-02-16 10:44:21 +01:00
Yasunobu YesNo Kawaguchi
819daa99f7 Translations: Update Japanese
Currently translated at 100.0% (6207 of 6207 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
8512e79d68 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6207 of 6207 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_Informal/

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
52672ae25b Translations: Update Dutch
Currently translated at 100.0% (6207 of 6207 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2026-02-16 10:44:21 +01:00
Raphael Michel
ad752dc617 Fix placeholder injection with django templates 2026-02-13 13:36:12 +01:00
Raphael Michel
43c6c33bd8 SafeFormatter: Ignore conversion spec 2026-02-13 12:35:49 +01:00
Raphael Michel
88c9f8c047 Remove duplicate rendering of plain content without variables 2026-02-13 12:30:01 +01:00
Raphael Michel
2d2663f15f Mark strings as formatted to prevent double-formatting 2026-02-13 12:28:32 +01:00
Kara Engelhardt
ae6014708b SECURITY: Prevent placeholder injcetion in plaintext emails 2026-02-13 12:28:32 +01:00
Richard Schreiber
d1686df07c Move request.GET.items to ctx (#5889) 2026-02-12 12:05:08 +01:00
Richard Schreiber
4d60d7bfbc Fix widget quantity prefill (#5886) 2026-02-12 12:04:11 +01:00
24 changed files with 33506 additions and 279 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 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,25 +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:
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
)
body_md = format_map(
body_md,
context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker
)
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 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,31 +1176,20 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email:
recipient = position.attendee_email
email_content = render_mail(template, context)
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):
@@ -2899,16 +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)
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,
@@ -2917,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
@@ -81,7 +82,9 @@ from pretix.base.signals import (
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.format import (
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
@@ -147,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.
@@ -218,6 +221,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if email == INVALID_ADDRESS:
return
if isinstance(template, FormattedString):
raise TypeError("Cannot pass an already formatted body template")
if no_order_links and not plain_text_only:
raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
@@ -251,23 +257,33 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if event and attach_tickets and not event.settings.mail_attach_tickets:
attach_tickets = False
with language(locale):
with language(locale), override(timezone):
if isinstance(context, dict) and order:
_autoextend_context(context, order)
# Build raw content
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
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 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:
# 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)
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
# Build subject
subject = str(subject).format_map(TolerantDict(context))
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
if settings_holder:
subject = prefix_subject(settings_holder, subject)
@@ -286,26 +302,24 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
else:
renderer = ClassicMailRenderer(None, organizer)
with override(timezone):
content_plain = render_mail(template, context, placeholder_mode=None)
try:
if 'context' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, 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)
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)
except:
logger.exception('Could not render HTML body')
body_html = None
try:
if 'context' in inspect.signature(renderer.render).parameters:
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, 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, signature, raw_subject, order)
except:
logger.exception('Could not render HTML body')
body_html = None
m = OutgoingMail.objects.create(
organizer=organizer,
@@ -327,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
@@ -356,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):
@@ -788,10 +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)
body = tpl.render(context)
return body
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()
} | {"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):
@@ -936,7 +976,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += "\r\n\r\n-- \r\n"
if signature:
signature = signature.format(event=event.name if event else '')
signature = format_map(signature, {"event": event.name if event else ''})
body_plain += signature
body_plain += "\r\n\r\n-- \r\n"
@@ -948,7 +988,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
orderurl=build_absolute_uri(
order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,

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

@@ -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

@@ -22,6 +22,7 @@
import logging
from string import Formatter
from django.core.exceptions import SuspiciousOperation
from django.utils.html import conditional_escape
logger = logging.getLogger(__name__)
@@ -37,6 +38,17 @@ class PlainHtmlAlternativeString:
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
class FormattedString(str):
"""
A str subclass that has been specifically marked as "already formatted" for email rendering
purposes to avoid duplicate formatting.
"""
__slots__ = ()
def __str__(self):
return self
class SafeFormatter(Formatter):
"""
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
@@ -77,8 +89,19 @@ class SafeFormatter(Formatter):
# Ignore format_spec
return super().format_field(self._prepare_value(value), '')
def convert_field(self, value, conversion):
# Ignore any conversions
if conversion is None:
return value
else:
return str(value)
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString:
if isinstance(template, FormattedString):
raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.")
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
return FormattedString(
SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
)

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-06 07:41+0000\n"
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
"PO-Revision-Date: 2026-02-12 20:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
"Language: ja\n"
@@ -2544,7 +2544,7 @@ msgstr "終了日"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:12
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:91
msgid "Product"
msgstr "品"
msgstr "品"
#: pretix/base/exporters/orderlist.py:652 pretix/base/models/vouchers.py:315
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:5
@@ -4048,7 +4048,7 @@ msgstr "複数の一致する製品が見つかりました。"
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1257
#: pretix/base/models/vouchers.py:266 pretix/base/models/waitinglist.py:100
msgid "Product variation"
msgstr "品バリエーション"
msgstr "品バリエーション"
#: pretix/base/modelimport_orders.py:161
msgid "The variation can be specified by its internal ID or full name."
@@ -9144,8 +9144,8 @@ msgid ""
"The voucher code used for one of the items in your cart has already been too "
"often. We adjusted the price of the item in your cart."
msgstr ""
"カートの商品の一つに使用されているバウチャーコードはに使用回数を超えていま"
"す。カート内の商品価格を調整しました。"
"カート内のアイテムに使用されバウチャーコードは、すでに使用回数の上限に達し"
"ています。カート内のアイテムの価格を調整しました。"
#: pretix/base/services/orders.py:182
msgid ""
@@ -24181,7 +24181,7 @@ msgstr "高度な検索"
#: pretix/control/templates/pretixcontrol/orders/index.html:102
#, python-format
msgid "List filtered by answers to question \"%(question)s\"."
msgstr "質問 \"%(question)s\" への回答で絞り込まれたリスト"
msgstr "質問%(question)sへの回答で絞り込まれたリストです。"
#: pretix/control/templates/pretixcontrol/orders/index.html:107
msgid "Remove filter"
@@ -34229,8 +34229,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
"とができます。"
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
"できます。"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:514
#: pretix/presale/templates/pretixpresale/fragment_modals.html:48
@@ -34243,7 +34243,7 @@ msgstr "確保が更新されました"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:532
msgid "Overview of your ordered products."
msgstr "注文した製品の概要"
msgstr "注文した製品の概要"
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:50
msgid "Continue with order process"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-02-06 07:41+0000\n"
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
"PO-Revision-Date: 2026-02-12 20:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
"Language: ja\n"
@@ -456,11 +456,11 @@ msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Product"
msgstr "品"
msgstr "品"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
msgid "Product variation"
msgstr "品バリエーション"
msgstr "品バリエーション"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
msgid "Gate"
@@ -726,8 +726,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
"とができます。"
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
"できます。"
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"

View File

@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
"PO-Revision-Date: 2026-02-12 00:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
@@ -7993,11 +7993,11 @@ msgid ""
"12345 Any City\n"
"Atlantis"
msgstr ""
"Piet Peeters\n"
"Piet Janssen\n"
"Voorbeeldbedrijf\n"
"Sesamstraat 42\n"
"12345 Ergens\n"
"Niemandsland"
"1234 AB Amsterdam\n"
"Nederland"
#: pretix/base/pdf.py:198
msgid "Attendee street"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
"PO-Revision-Date: 2026-02-12 00:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
@@ -8002,9 +8002,9 @@ msgid ""
"Atlantis"
msgstr ""
"Piet Janssen\n"
"Voorbeeld bedrijf\n"
"Voorbeeldbedrijf\n"
"Sesamstraat 42\n"
"1234AB Amsterdam\n"
"1234 AB Amsterdam\n"
"Nederland"
#: pretix/base/pdf.py:198
@@ -21786,9 +21786,9 @@ msgid ""
"copy all products, categories, quotas, and questions as well as general "
"event settings."
msgstr ""
"Wil je de instellingen van een ander evenement kopiëren? We zullen alle "
"producten, categorieën, quota en vragen overnemen, samen met de algemene "
"evenementsinstellingen."
"Wil je de configuratie van een ander evenement kopiëren? We kopiëren alle "
"producten, categorieën, quota en vragen, evenals de algemene "
"evenementinstellingen."
#: pretix/control/templates/pretixcontrol/events/create_copy.html:13
msgid ""
@@ -21796,25 +21796,25 @@ msgid ""
"need to change some settings manually, e.g. date and time settings and texts "
"that contain the event name."
msgstr ""
"Controleer alle instellingen! Je moet waarschijnlijk nog steeds wat "
"instellingen handmatig aanpassen, bijvoorbeeld datum- en tijdsinstellingen, "
"en teksten die de evenementsnaam bevatten."
"Controleer alle instellingen zorgvuldig. Waarschijnlijk moet je nog enkele "
"instellingen handmatig wijzigen, bijvoorbeeld de datum- en tijdinstellingen "
"en teksten die de naam van het evenement bevatten."
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:7
msgid "Event type"
msgstr "Evenementssoort"
msgstr "Type evenement"
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:13
msgid "Singular event or non-event shop"
msgstr "Enkel evenement of winkel die niet voor een evenement is"
msgstr "Eenmalig evenement of geen evenement"
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:15
msgid ""
"An event with individual configuration. If you create more events later, you "
"can copy the event to save yourself some work."
msgstr ""
"Een evenement met eigen instellingen. Als je later meer evenementen aanmaakt "
"kan je de instellingen van dit evenement kopiëren."
"Een evenement met eigen instellingen. Als je later meer evenementen "
"aanmaakt, kan je de instellingen van dit evenement kopiëren."
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:21
msgid ""
@@ -21834,8 +21834,8 @@ msgid ""
"A series of events that share the same configuration. They can still be "
"different in their dates, locations, prices, and capacities."
msgstr ""
"Een reeks evenementen die dezelfde instellingen delen. De evenementen in de "
"reeks kunnen verschillen in hun datums, prijzen en capaciteiten."
"Een reeks evenementen met dezelfde configuratie. Ze kunnen nog steeds "
"verschillen qua data, locaties, prijzen en capaciteiten."
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:40
msgid ""
@@ -21861,9 +21861,8 @@ msgid ""
"The list below shows all events you have administrative access to. Click on "
"the event name to access event details."
msgstr ""
"De lijst hieronder toont alle evenementen waar je administratieve toegang "
"toe hebt. Klik op de evenementsnaam om de details van het evenement te "
"openen."
"De onderstaande lijst toont alle evenementen waar je beheerderstoegang toe "
"hebt. Klik op de naam van het evenement om de details ervan te bekijken."
#: pretix/control/templates/pretixcontrol/events/index.html:12
#: pretix/control/templates/pretixcontrol/organizers/detail.html:18
@@ -21923,7 +21922,7 @@ msgstr "Aantallen bijgewerkt op %(date)s"
#: pretix/control/templates/pretixcontrol/fragment_quota_box_paid.html:3
#, python-format
msgid "Currently available: %(num)s"
msgstr "Op dit moment beschikbaar: %(num)s"
msgstr "Nu beschikbaar: %(num)s"
#: pretix/control/templates/pretixcontrol/global_license.html:8
msgid ""
@@ -21973,7 +21972,7 @@ msgstr "Installatiegegevens"
#: pretix/control/templates/pretixcontrol/global_license.html:34
msgid "Installed plugins"
msgstr "Geïnstalleerde plugins"
msgstr "Geïnstalleerde plug-ins"
#: pretix/control/templates/pretixcontrol/global_license.html:40
msgid "Public information"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-05 14:34+0000\n"
"PO-Revision-Date: 2026-02-14 06:10+0000\n"
"Last-Translator: Nate Horst <nate@agcthailand.org>\n"
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/>"
"\n"
@@ -1000,7 +1000,7 @@ msgstr "โดเมนอีเมลที่ใช้สั่งซื้อ
#: pretix/plugins/reports/exporters.py:900
#: pretix/plugins/ticketoutputpdf/exporters.py:96
msgid "Order code"
msgstr "รหัสการสั่งซื้อ"
msgstr "รหัสการลงทะเบียน"
#: pretix/base/datasync/sourcefields.py:380
msgid "Event and order code"
@@ -7190,7 +7190,7 @@ msgstr "สินค้าที่ซื้อแล้ว"
#: pretix/base/services/placeholders.py:424
#: pretix/base/templates/pretixbase/email/order_details.html:151
msgid "View order details"
msgstr "ดูรายละเอียดคำสั่งซื้อ"
msgstr "ดูรายละเอียดการลงทะเบียน"
#: pretix/base/notifications.py:234
#, python-brace-format
@@ -11567,7 +11567,7 @@ msgstr ""
#: pretix/base/settings.py:2856 pretix/base/settings.py:2872
#, python-brace-format
msgid "Your ticket is ready for download: {code}"
msgstr ""
msgstr "ตั๋วของคุณพร้อมสำหรับการดาวน์โหลดแล้ว: {code}"
#: pretix/base/settings.py:2860
#, python-brace-format
@@ -12477,7 +12477,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/base.html:222
#: pretix/presale/templates/pretixpresale/organizers/base.html:100
msgid "Contact"
msgstr ""
msgstr "ติดต่อ"
#: pretix/base/templates/pretixbase/email/shred_completed.txt:2
#, python-format
@@ -12548,21 +12548,23 @@ msgstr ""
#: pretix/base/templates/source.html:5 pretix/base/templates/source.html:9
msgid "Source code"
msgstr ""
msgstr "ซอร์สโค้ด (Source code)"
#: pretix/base/templates/source.html:10
msgid ""
"This site is powered by free software. If you want to read the license terms "
"or obtain the source code, follow these links or instructions:"
msgstr ""
"เว็บไซต์นี้ขับเคลื่อนโดยซอฟต์แวร์เสรี หากคุณต้องการอ่านเงื่อนไขการอนุญาตใช้งานหรือรับซอร์สโค้ด โปร"
"ดไปที่ลิงก์หรือคำแนะนำต่อไปนี้:"
#: pretix/base/ticketoutput.py:182
msgid "Enable ticket format"
msgstr ""
msgstr "เปิดใช้งานรูปแบบตั๋ว"
#: pretix/base/ticketoutput.py:200
msgid "Download ticket"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว"
#: pretix/base/timeframes.py:49
msgctxt "reporting_timeframe"
@@ -19827,7 +19829,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:7
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:40
msgid "Ticket download"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว"
#: pretix/control/templates/pretixcontrol/event/tickets.html:11
msgid "Download settings"
@@ -21760,27 +21762,27 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:410
msgid "Change products"
msgstr ""
msgstr "เปลี่ยนรายการสินค้า"
#: pretix/control/templates/pretixcontrol/order/index.html:415
#: pretix/presale/templates/pretixpresale/event/order.html:197
msgid "Ordered items"
msgstr ""
msgstr "รายการสินค้าที่สั่งซื้อ"
#: pretix/control/templates/pretixcontrol/order/index.html:434
#, python-format
msgid "Denied scan: %(date)s"
msgstr ""
msgstr "การสแกนถูกปฏิเสธ: %(date)s"
#: pretix/control/templates/pretixcontrol/order/index.html:439
#, python-format
msgid "Exit scan: %(date)s"
msgstr ""
msgstr "สแกนขาออก: %(date)s"
#: pretix/control/templates/pretixcontrol/order/index.html:446
#, python-format
msgid "Entry scan: %(date)s"
msgstr ""
msgstr "สแกนขาเข้า: %(date)s"
#: pretix/control/templates/pretixcontrol/order/index.html:465
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:55
@@ -21922,7 +21924,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:90
#: pretix/presale/templates/pretixpresale/event/order.html:318
msgid "ZIP code and city"
msgstr ""
msgstr "รหัสไปรษณีย์ และชื่อเมือง"
#: pretix/control/templates/pretixcontrol/order/index.html:1047
msgid "Valid EU VAT ID"
@@ -30726,46 +30728,46 @@ msgstr ""
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:65
msgid "Download tickets (PDF)"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว (PDF)"
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:66
msgid "Download ticket (PDF)"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว (PDF)"
#: pretix/plugins/ticketoutputpdf/views.py:62
msgid "Default ticket layout"
msgstr ""
msgstr "รูปแบบตั๋วมาตรฐาน"
#: pretix/plugins/ticketoutputpdf/views.py:119
msgid "The new ticket layout has been created."
msgstr ""
msgstr "สร้างรูปแบบตั๋วใหม่เรียบร้อยแล้ว"
#: pretix/plugins/ticketoutputpdf/views.py:168
#: pretix/plugins/ticketoutputpdf/views.py:198
#: pretix/plugins/ticketoutputpdf/views.py:246
msgid "The requested layout does not exist."
msgstr ""
msgstr "ไม่พบรูปแบบตั๋วที่ร้องขอ"
#: pretix/plugins/ticketoutputpdf/views.py:210
msgid "The selected ticket layout been deleted."
msgstr ""
msgstr "รูปแบบตั๋วที่เลือกถูกลบออกแล้ว"
#: pretix/plugins/ticketoutputpdf/views.py:250
#, python-brace-format
msgid "Ticket PDF layout: {}"
msgstr ""
msgstr "รูปแบบไฟล์ PDF ของตั๋ว: {}"
#: pretix/plugins/webcheckin/apps.py:30 pretix/plugins/webcheckin/apps.py:33
msgid "Web-based check-in"
msgstr ""
msgstr "การเช็คอินผ่านเว็บ"
#: pretix/plugins/webcheckin/apps.py:38
msgid "Turn your browser into a check-in device to perform access control."
msgstr ""
msgstr "เปลี่ยนเบราว์เซอร์ของคุณให้เป็นอุปกรณ์เช็คอินเพื่อควบคุมการเข้างาน"
#: pretix/plugins/webcheckin/apps.py:40 pretix/plugins/webcheckin/signals.py:36
msgid "Web Check-in"
msgstr ""
msgstr "เว็บเช็คอิน (Web Check-in)"
#: pretix/presale/checkoutflow.py:117
msgctxt "checkoutflow"
@@ -31188,7 +31190,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/base.html:242
#: pretix/presale/templates/pretixpresale/organizers/base.html:120
msgid "Imprint"
msgstr ""
msgstr "ข้อมูลทางกฎหมาย (Imprint)"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:12
msgid ""
@@ -32075,24 +32077,25 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:76
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:82
msgid "Please have your ticket ready when entering the event."
msgstr ""
msgstr "โปรดเตรียมตั๋วของคุณให้พร้อมเมื่อเข้าสู่งาน"
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:85
msgid "Download your tickets using the buttons below."
msgstr ""
msgstr "ดาวน์โหลดตั๋วของคุณโดยใช้ปุ่มด้านล่างนี้"
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:94
#, python-format
msgid "You will be able to download your tickets here starting on %(date)s."
msgstr ""
"คุณจะสามารถดาวน์โหลดตั๋วของคุณได้ที่นี่ ตั้งแต่วันที่ %(date)s เป็นต้นไป"
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:7
msgid "Where does the event happen?"
msgstr ""
msgstr "กิจกรรมจัดขึ้นที่ไหน?"
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:17
msgid "When does the event happen?"
msgstr ""
msgstr "กิจกรรมจัดขึ้นเมื่อไหร่?"
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:26
#, python-format
@@ -32491,7 +32494,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html:287
msgid "Your information"
msgstr ""
msgstr "ข้อมูลของคุณ"
#: pretix/presale/templates/pretixpresale/event/order.html:290
msgid "Change your information"

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

@@ -252,7 +252,7 @@ Vue.component('availbox', {
variation: Object
},
mounted: function() {
if (this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
if (!this.$root.cart_exists && this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
this.$refs.quantity.value = 1;
if (this.order_max === 1) {
this.$refs.quantity.checked = true;

View File

@@ -32,8 +32,10 @@
# 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 datetime
import os
import re
from decimal import Decimal
from email.mime.text import MIMEText
import pytest
@@ -42,11 +44,13 @@ 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 django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.models import Event, Organizer, OutgoingMail, User
from pretix.base.models import (
Event, InvoiceAddress, Order, Organizer, OutgoingMail, User,
)
from pretix.base.services.mail import mail, mail_send_task
@@ -68,6 +72,45 @@ def env():
yield event, user, o
@pytest.fixture
@scopes_disabled()
def item(env):
return env[0].items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
@scopes_disabled()
def order(env, item):
event, _, _ = env
o = Order.objects.create(
code="FOO",
event=event,
email="dummy@dummy.test",
status=Order.STATUS_PENDING,
secret="k24fiuwvu8kxz3y1",
sales_channel=event.organizer.sales_channels.get(identifier="web"),
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
total=23,
locale="en",
)
o.positions.create(
order=o,
item=item,
variation=None,
price=Decimal("23"),
attendee_email="peter@example.org",
attendee_name_parts={"given_name": "Peter", "family_name": "Miller"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL",
)
InvoiceAddress.objects.create(
order=o,
name_parts={"given_name": "Peter", "family_name": "Miller"},
)
return o
@pytest.mark.django_db
def test_send_mail_with_prefix(env):
djmail.outbox = []
@@ -279,7 +322,7 @@ def _extract_html(mail):
def test_placeholder_html_rendering_from_template(env):
djmail.outbox = []
event, user, organizer = env
event.name = "<strong>event & co. kg</strong>"
event.name = "<strong>event & co. kg</strong> {currency}"
event.save()
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context(
event=event,
@@ -288,25 +331,29 @@ def test_placeholder_html_rendering_from_template(env):
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
assert 'Event name: <strong>event & co. kg</strong>' 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>](https://example.org/dummy)' in djmail.outbox[0].body
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' 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])
assert '<strong>event' not in html
assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt;' 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">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
html
)
assert re.search(
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'Event: <a href="https://example.com/dummy" style="[^"]+">'
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html
)
@@ -324,7 +371,7 @@ def test_placeholder_html_rendering_from_string(env):
})
djmail.outbox = []
event, user, organizer = env
event.name = "<strong>event & co. kg</strong>"
event.name = "<strong>event & co. kg</strong> {currency}"
event.save()
ctx = get_email_context(
event=event,
@@ -335,9 +382,9 @@ def test_placeholder_html_rendering_from_string(env):
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body
assert 'Other website: [<strong>event & co. kg</strong> {currency}](https://example.com)' 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 'URL: https://google.com' in djmail.outbox[0].body
@@ -352,11 +399,13 @@ def test_placeholder_html_rendering_from_string(env):
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
assert '<strong>Meta</strong>: <em>Beep</em>' in html
assert re.search(
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html
)
assert re.search(
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">'
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html
)
assert re.search(
@@ -377,3 +426,141 @@ def test_placeholder_html_rendering_from_string(env):
r'style="[^"]+" target="_blank">Link &amp; Text</a>',
html
)
@pytest.mark.django_db
def test_nested_placeholder_inclusion_full_process(env, order):
# Test that it is not possible to sneak in a placeholder like {url_cancel} inside a user-controlled
# placeholder value like {invoice_company}
event, user, organizer = env
position = order.positions.get()
order.invoice_address.company = "{url_cancel} Corp"
order.invoice_address.save()
event.settings.mail_text_resend_link = LazyI18nString({"en": "Ticket for {invoice_company}"})
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": "Ticket for {invoice_company}"})
djmail.outbox = []
position.resend_link()
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [position.attendee_email]
assert "Ticket for {url_cancel} Corp" == djmail.outbox[0].subject
assert "/cancel" not in djmail.outbox[0].body
assert "/order" not in djmail.outbox[0].body
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain):
assert "Ticket for {url_cancel} Corp" in part
assert "/order/" not in part
assert "/cancel" not in part
@pytest.mark.django_db
def test_nested_placeholder_inclusion_mail_service(env):
# test that it is not possible to have placeholders within the values of placeholders when
# the mail() function is called directly
template = LazyI18nString("Event name: {event}")
djmail.outbox = []
event, user, organizer = env
event.name = "event & {currency} co. kg"
event.slug = "event-co-ag-slug"
event.save()
mail(
"dummy@dummy.dummy",
"{event} Test subject",
template,
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
),
event,
)
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "event & {currency} co. kg" in part or "event &amp; {currency} co. kg" in part
assert "EUR" not in part
@pytest.mark.django_db
@pytest.mark.parametrize("tpl", [
"Event: {event.__class__}",
"Event: {{event.__class__}}",
"Event: {{{event.__class__}}}",
])
def test_variable_inclusion_from_string_full_process(env, tpl, order):
# Test that it is not possible to use placeholders that leak system information in templates
# when run through system processes
event, user, organizer = env
event.name = "event & co. kg"
event.save()
position = order.positions.get()
event.settings.mail_text_resend_link = LazyI18nString({"en": tpl})
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": tpl})
position.resend_link()
assert len(djmail.outbox) == 1
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "{event.__class__}" in part
assert "LazyI18nString" not in part
@pytest.mark.django_db
@pytest.mark.parametrize("tpl", [
"Event: {event.__class__}",
"Event: {{event.__class__}}",
"Event: {{{event.__class__}}}",
])
def test_variable_inclusion_from_string_mail_service(env, tpl):
# Test that it is not possible to use placeholders that leak system information in templates
# when run through mail() directly
event, user, organizer = env
event.name = "event & co. kg"
event.save()
djmail.outbox = []
mail(
"dummy@dummy.dummy",
tpl,
LazyI18nString(tpl),
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456\n" + tpl,
),
event,
)
assert len(djmail.outbox) == 1
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "{event.__class__}" in part
assert "LazyI18nString" not in part
@pytest.mark.django_db
def test_escaped_braces_mail_services(env):
# Test that braces can be escaped by doubling
template = LazyI18nString("Event name: -{{currency}}-")
djmail.outbox = []
event, user, organizer = env
event.name = "event & co. kg"
event.save()
mail(
"dummy@dummy.dummy",
"-{{currency}}- Test subject",
template,
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
),
event,
)
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "EUR" not in part
assert "-{currency}-" in part

View File

@@ -29,6 +29,8 @@ def test_format_map():
assert format_map("Foo {baz}", {"bar": 3}) == "Foo {baz}"
assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}"
assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {bar!r}", {"bar": '3'}) == "Foo 3"
assert format_map("Foo {bar!a}", {"bar": '3'}) == "Foo 3"
assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3"

View File

@@ -1,13 +1,29 @@
{% load i18n %}
This is a test file for sending mails.
Event name: {event}
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}
{{ payment_info }}
**Meta**: {meta_Test}
**Meta**: {{ meta_Test }}
Event website: [{event}](https://example.org/{event_slug})
Other website: [{event}]({meta_Website})
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 %}