mirror of
https://github.com/pretix/pretix.git
synced 2026-08-04 09:47:50 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e445c4d11e | ||
|
|
162205ffaf |
+31
-18
@@ -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,
|
||||||
|
|||||||
@@ -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': [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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 '<' not in djmail.outbox[0].body
|
assert '<a ' not in djmail.outbox[0].body
|
||||||
# todo: assert '&' not in djmail.outbox[0].body
|
assert '<' not in djmail.outbox[0].body
|
||||||
|
assert '&' 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: <strong>event & co. kg</strong> {currency}' in html
|
assert 'Event name: <strong>event & co. kg</strong> {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: [<strong>event & co. kg</strong> {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'<strong>event & co. kg</strong> {currency}</a>',
|
r'<strong>event & co. kg</strong> {currency}</a>',
|
||||||
html
|
html
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 %}
|
||||||
Reference in New Issue
Block a user