Compare 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
15 changed files with 187 additions and 175 deletions
+31 -18
View File
@@ -22,7 +22,7 @@
import logging import logging
from itertools import groupby from itertools import groupby
from smtplib import SMTPResponseException from smtplib import SMTPResponseException
from typing import TypeVar from typing import TypeVar, Union
import bleach import bleach
import css_inline import css_inline
@@ -31,6 +31,7 @@ from django.core.mail.backends.smtp import EmailBackend
from django.db.models import Count from django.db.models import Count
from django.dispatch import receiver from django.dispatch import receiver
from django.template.loader import get_template 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 django.utils.translation import get_language, gettext_lazy as _
from pretix.base.models import Event 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, DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
markdown_compile_email, truelink_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 from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext get_available_placeholders, PlaceholderContext
@@ -83,8 +86,8 @@ class BaseHTMLMailRenderer:
def __str__(self): def __str__(self):
return self.identifier return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order=None, def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
position=None, context=None) -> str: subject: str, order=None, position=None, context=None) -> str:
""" """
This method should generate the HTML part of the email. This method should generate the HTML part of the email.
@@ -140,27 +143,37 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def compile_markdown(self, plaintext, context=None): def compile_markdown(self, plaintext, context=None):
return markdown_compile_email(plaintext, context=context) return markdown_compile_email(plaintext, context=context)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str: def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
apply_format_map = not isinstance(plain_body, FormattedString) subject: str, order=None, position=None, context=None) -> str:
body_md = self.compile_markdown(plain_body, context) if isinstance(content, FormattedString):
if context: # Raw string that is already formatted but not markdown-rendered
linker = bleach.Linker( body_content_html = self.compile_markdown(content, context)
url_re=URL_RE,
email_re=EMAIL_RE, elif isinstance(content, PlainHtmlAlternativeString):
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback], # HTML already rendered by Django templates
parse_email=True body_content_html = content.html
)
if apply_format_map: else:
body_md = format_map( # Raw string that is not yet formatted or markdown-rendered
body_md, 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, context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML, mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker linkifier=linker
) )
htmlctx = { htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME, 'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL, 'site_url': settings.SITE_URL,
'body': body_md, 'body': mark_safe(body_content_html),
'subject': str(subject), 'subject': str(subject),
'color': settings.PRETIX_PRIMARY_COLOR, 'color': settings.PRETIX_PRIMARY_COLOR,
'rtl': get_language() in settings.LANGUAGES_RTL or get_language().split('-')[0] in settings.LANGUAGES_RTL, 'rtl': get_language() in settings.LANGUAGES_RTL or get_language().split('-')[0] in settings.LANGUAGES_RTL,
+9 -21
View File
@@ -33,8 +33,7 @@ from pretix.base.invoicing.transmission import (
transmission_types, transmission_types,
) )
from pretix.base.models import Invoice, InvoiceAddress from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.services.mail import mail, render_mail from pretix.base.services.mail import mail
from pretix.helpers.format import format_map
@transmission_types.new() @transmission_types.new()
@@ -134,9 +133,7 @@ class EmailTransmissionProvider(TransmissionProvider):
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString) 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 # Do not set to completed because that is done by the email sending task
subject = format_map(subject, context) outgoing_mail = mail(
email_content = render_mail(template, context)
mail(
[recipient], [recipient],
subject, subject,
template, template,
@@ -151,19 +148,10 @@ class EmailTransmissionProvider(TransmissionProvider):
plain_text_only=True, plain_text_only=True,
no_order_links=True, no_order_links=True,
) )
invoice.order.log_action( if outgoing_mail:
'pretix.event.order.email.invoice', invoice.order.log_action(
user=None, 'pretix.event.order.email.invoice',
auth=None, user=None,
data={ auth=None,
'subject': subject, data=outgoing_mail.log_data()
'message': email_content, )
'position': None,
'recipient': recipient,
'invoices': [invoice.pk],
'attach_tickets': False,
'attach_ical': False,
'attach_other_files': [],
'attach_cached_files': [],
}
)
+17
View File
@@ -220,3 +220,20 @@ class OutgoingMail(models.Model):
error_log_action_type = 'pretix.email.error' error_log_action_type = 'pretix.email.error'
log_target = None log_target = None
return log_target, error_log_action_type 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,
}
+18 -42
View File
@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import FormattedString, format_map
from ...helpers.names import build_name from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var from ...testutils.middleware import debugflags_var
from ._transactions import ( 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 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. 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): if not self.email and not (position and position.attendee_email):
return return
@@ -1177,32 +1176,20 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email: if position and position.attendee_email:
recipient = position.attendee_email recipient = position.attendee_email
email_content = render_mail(template, context) outgoing_mail = mail(
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
recipient, subject, template, context, recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender, self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets, invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email, attach_ical=attach_ical, position=position, auto_email=auto_email, attach_ical=attach_ical,
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files, attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
) )
self.log_action( if outgoing_mail:
log_entry_type, self.log_action(
user=user, log_entry_type,
auth=auth, user=user,
data={ auth=auth,
'subject': subject, data=outgoing_mail.log_data(),
'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): def resend_link(self, user=None, auth=None):
with language(self.locale, self.event.settings.region): 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_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files :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: if not self.attendee_email:
return return
with language(self.order.locale, self.order.event.settings.region): with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email recipient = self.attendee_email
email_content = render_mail(template, context) outgoing_mail = mail(
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
recipient, subject, template, context, recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender, self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self, position=self,
@@ -2919,21 +2903,13 @@ class OrderPosition(AbstractPosition):
attach_ical=attach_ical, attach_ical=attach_ical,
attach_other_files=attach_other_files, attach_other_files=attach_other_files,
) )
self.order.log_action( if outgoing_mail:
log_entry_type, self.order.log_action(
user=user, log_entry_type,
auth=auth, user=user,
data={ auth=auth,
'subject': subject, data=outgoing_mail.log_data(),
'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): def resend_link(self, user=None, auth=None):
+9 -17
View File
@@ -34,10 +34,9 @@ from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import User, Voucher 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 pretix.helpers import OF_SELF
from ...helpers.format import format_map
from ...helpers.names import build_name from ...helpers.names import build_name
from .base import LoggedModel from .base import LoggedModel
from .event import Event, SubEvent from .event import Event, SubEvent
@@ -272,9 +271,7 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region): with language(self.locale, self.event.settings.region):
recipient = self.email recipient = self.email
email_content = render_mail(template, context) outgoing_mail = mail(
subject = format_map(subject, context)
mail(
recipient, subject, template, context, recipient, subject, template, context,
self.event, self.event,
self.locale, self.locale,
@@ -284,18 +281,13 @@ class WaitingListEntry(LoggedModel):
attach_other_files=attach_other_files, attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files, attach_cached_files=attach_cached_files,
) )
self.log_action( if outgoing_mail:
log_entry_type, self.log_action(
user=user, log_entry_type,
auth=auth, user=user,
data={ auth=auth,
'subject': subject, data=outgoing_mail.log_data(),
'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 @staticmethod
def clean_itemvar(event, item, variation): def clean_itemvar(event, item, variation):
+1
View File
@@ -1295,6 +1295,7 @@ class ManualPayment(BasePaymentProvider):
def format_map(self, order, payment): def format_map(self, order, payment):
return { return {
# Possible placeholder injection, we should make sure to never include user-controlled variables here
'order': order.code, 'order': order.code,
'amount': payment.amount, 'amount': payment.amount,
'currency': self.event.currency, 'currency': self.event.currency,
+3 -6
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.base.templatetags.money import money_filter
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__) 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) email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
mail( mail(
wle.email, wle.email,
format_map(subject, email_context), str(subject),
message, message,
email_context, email_context,
wle.event, 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, 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) order=order, position_or_address=ia, event=order.event)
real_subject = format_map(subject, email_context)
order.send_mail( order.send_mail(
real_subject, message, email_context, subject, message, email_context,
'pretix.event.order.email.event_canceled', 'pretix.event.order.email.event_canceled',
user, user,
) )
@@ -85,14 +83,13 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
continue continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: 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, email_context = get_email_context(event_or_subevent=p.subevent or order.event,
event=order.event, event=order.event,
refund_amount=refund_amount, refund_amount=refund_amount,
position_or_address=p, position_or_address=p,
order=order, position=p) order=order, position=p)
order.send_mail( order.send_mail(
real_subject, message, email_context, subject, message, email_context,
'pretix.event.order.email.event_canceled', 'pretix.event.order.email.event_canceled',
position=p, position=p,
user=user user=user
+44 -17
View File
@@ -58,6 +58,7 @@ from django.core.mail.message import SafeMIMEText
from django.db import connection, transaction from django.db import connection, transaction
from django.db.models import Q from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.template import Context
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.html import escape from django.utils.html import escape
from django.utils.timezone import now, override from django.utils.timezone import now, override
@@ -149,13 +150,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
return subject 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, 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, 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, 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, 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, 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. 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) _autoextend_context(context, order)
# Build raw content # Build raw content
content_plain = render_mail(template, context, placeholder_mode=None) content = render_mail(template, context, placeholder_mode=None)
if settings_holder: if settings_holder:
signature = str(settings_holder.settings.get('mail_text_signature')) signature = str(settings_holder.settings.get('mail_text_signature'))
else: else:
signature = "" signature = ""
# Build full plain-text body # Build full plain-text body
if not isinstance(content_plain, FormattedString): if isinstance(content, FormattedString):
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN) # 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: 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) body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
# Build subject # Build subject
@@ -298,19 +304,19 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
try: try:
if 'context' in inspect.signature(renderer.render).parameters: 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: elif 'position' in inspect.signature(renderer.render).parameters:
# Backwards compatibility # Backwards compatibility
warnings.warn('Email renderer called without context argument because context argument is not ' warnings.warn('Email renderer called without context argument because context argument is not '
'supported.', 'supported.',
DeprecationWarning) DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order, position) body_html = renderer.render(content, signature, raw_subject, order, position)
else: else:
# Backwards compatibility # Backwards compatibility
warnings.warn('Email renderer called without position argument because position argument is not ' warnings.warn('Email renderer called without position argument because position argument is not '
'supported.', 'supported.',
DeprecationWarning) DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order) body_html = renderer.render(content, signature, raw_subject, order)
except: except:
logger.exception('Could not render HTML body') logger.exception('Could not render HTML body')
body_html = None 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 [], should_attach_other_files=attach_other_files or [],
sensitive=sensitive, sensitive=sensitive,
) )
m._prefetched_objects_cache = {}
if invoices and not position: if invoices and not position:
m.should_attach_invoices.add(*invoices) 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: if attach_cached_files:
cf_list = []
for cf in attach_cached_files: for cf in attach_cached_files:
if not isinstance(cf, CachedFile): if not isinstance(cf, CachedFile):
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf)) cf = CachedFile.objects.get(pk=cf)
else: m.should_attach_cached_files.add(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( send_task = mail_send_task.si(
outgoing_mail=m.id 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() lambda: chain(*task_chain).apply_async()
) )
return m
class CustomEmail(EmailMultiAlternatives): class CustomEmail(EmailMultiAlternatives):
def _create_mime_attachment(self, content, mimetype): def _create_mime_attachment(self, content, mimetype):
@@ -796,15 +816,22 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
body = str(template) body = str(template)
if context and placeholder_mode: if context and placeholder_mode:
body = format_map(body, context, mode=placeholder_mode) body = format_map(body, context, mode=placeholder_mode)
return body
else: else:
tpl = get_template(template) 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 k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
for k, v in context.items() for k, v in context.items()
} } | {"to_html": True}, autoescape=True)
body = FormattedString(tpl.render(context)) return PlainHtmlAlternativeString(
return body plain=tpl.template.render(plain_context),
html=tpl.template.render(html_context),
)
def replace_images_with_cid_paths(body_html): def replace_images_with_cid_paths(body_html):
+3 -3
View File
@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
with language(event.settings.locale): with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '', email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list]) voucher_list=[v.code for v in voucher_list])
mail( outgoing_mail = mail(
r['email'], r['email'],
subject, subject,
LazyI18nString(message), LazyI18nString(message),
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
data={ data={
'recipient': r['email'], 'recipient': r['email'],
'name': r.get('name'), 'name': r.get('name'),
'subject': subject, 'subject': outgoing_mail.subject,
'message': message, 'message': outgoing_mail.body_plain,
}, },
save=False save=False
)) ))
+1 -1
View File
@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
le.save(update_fields=['data', 'shredded']) le.save(update_fields=['data', 'shredded'])
else: else:
shred_log_fields(le, banlist=[ 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',
]) ])
@@ -24,7 +24,9 @@
{% if log.display %} {% if log.display %}
<br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }} <br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }}
{% endif %} {% 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 }} <br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
{% endif %} {% endif %}
</p> </p>
+4 -4
View File
@@ -2413,9 +2413,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
with language(order.locale, self.request.event.settings.region): with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order) email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message']) 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': 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 = { self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format( 'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(order.event, escape(email_subject), highlight=True) 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): 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_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message']) 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': 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 = { self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format( 'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(position.order.event, escape(email_subject), highlight=True)) subject=prefix_subject(position.order.event, escape(email_subject), highlight=True))
+15 -34
View File
@@ -38,13 +38,10 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context from pretix.base.email import get_email_context
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
CachedFile, Checkin, Event, InvoiceAddress, Order, User,
)
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.base.services.tasks import ProfiledEventTask from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers.format import format_map
def _chunks(lst, n): 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 user = User.objects.get(pk=user) if user else None
subject = LazyI18nString(subject) subject = LazyI18nString(subject)
message = LazyI18nString(message) 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): def _send_to_order(o):
send_to_order = recipients in ('both', 'orders') 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): with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p) email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
mail( outgoing_mail = mail(
p.attendee_email, p.attendee_email,
subject, subject,
message, message,
@@ -135,25 +131,17 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
attach_ical=attach_ical, attach_ical=attach_ical,
attach_cached_files=attachments attach_cached_files=attachments
) )
o.log_action( if outgoing_mail:
'pretix.plugins.sendmail.order.email.sent.attendee', o.log_action(
user=user, 'pretix.plugins.sendmail.order.email.sent.attendee',
data={ user=user,
'position': p.positionid, data=outgoing_mail.log_data(),
'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 send_to_order and o.email: if send_to_order and o.email:
with language(o.locale, event.settings.region): 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)
mail( outgoing_mail = mail(
o.email, o.email,
subject, subject,
message, message,
@@ -165,19 +153,12 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
attach_ical=attach_ical, attach_ical=attach_ical,
attach_cached_files=attachments, attach_cached_files=attachments,
) )
o.log_action( if outgoing_mail:
'pretix.plugins.sendmail.order.email.sent', o.log_action(
user=user, 'pretix.plugins.sendmail.order.email.sent',
data={ user=user,
'subject': format_map(subject.localize(o.locale), email_context), data=outgoing_mail.log_data(),
'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): for chunk in _chunks(objects, 1000):
orders = Order.objects.filter(pk__in=chunk, event=event) orders = Order.objects.filter(pk__in=chunk, event=event)
+12 -10
View File
@@ -42,7 +42,6 @@ import pytest
from django.conf import settings from django.conf import settings
from django.core import mail as djmail from django.core import mail as djmail
from django.test import override_settings from django.test import override_settings
from django.utils.html import escape
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled 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 len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email] 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 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
assert escape('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 '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body assert '**Meta**: *Beep*' in djmail.outbox[0].body
assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body assert '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 assert '<a ' not in djmail.outbox[0].body
# todo: assert '&amp;' 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 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body
assert 'EUR' not in djmail.outbox[0].body assert 'EUR' not in djmail.outbox[0].body
html = _extract_html(djmail.outbox[0]) 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 '<strong>event' not in html
assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}' 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>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 'Unevaluated placeholder: {currency}' in html
assert 'EUR' not 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( 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>', r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html html
) )
+17 -1
View File
@@ -1,13 +1,29 @@
{% load i18n %} {% load i18n %}
This is a test file for sending mails. This is a test file for sending mails.
Django variables will get evaluated:
Event name: {{ event }} Event name: {{ event }}
pretix variables will not in a template rendering:
Unevaluated placeholder: {currency} Unevaluated placeholder: {currency}
We can use advanced Django things:
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
The language code used for rendering this email is {{ 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 }} {{ payment_info }}
**Meta**: {{ meta_Test }} **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 %}