mirror of
https://github.com/pretix/pretix.git
synced 2026-02-19 08:52:26 +00:00
Compare commits
1 Commits
mail-clean
...
mail-html-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e445c4d11e |
@@ -22,7 +22,7 @@
|
||||
import logging
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
from typing import TypeVar
|
||||
from typing import TypeVar, Union
|
||||
|
||||
import bleach
|
||||
import css_inline
|
||||
@@ -31,6 +31,7 @@ from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event
|
||||
@@ -39,7 +40,9 @@ from pretix.base.templatetags.rich_text import (
|
||||
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
|
||||
markdown_compile_email, truelink_callback,
|
||||
)
|
||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||
from pretix.helpers.format import (
|
||||
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -83,8 +86,8 @@ class BaseHTMLMailRenderer:
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||
position=None, context=None) -> str:
|
||||
def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
|
||||
subject: str, order=None, position=None, context=None) -> str:
|
||||
"""
|
||||
This method should generate the HTML part of the email.
|
||||
|
||||
@@ -140,27 +143,37 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
def compile_markdown(self, plaintext, context=None):
|
||||
return markdown_compile_email(plaintext, context=context)
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||
apply_format_map = not isinstance(plain_body, FormattedString)
|
||||
body_md = self.compile_markdown(plain_body, context)
|
||||
if context:
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
if apply_format_map:
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
|
||||
subject: str, order=None, position=None, context=None) -> str:
|
||||
if isinstance(content, FormattedString):
|
||||
# Raw string that is already formatted but not markdown-rendered
|
||||
body_content_html = self.compile_markdown(content, context)
|
||||
|
||||
elif isinstance(content, PlainHtmlAlternativeString):
|
||||
# HTML already rendered by Django templates
|
||||
body_content_html = content.html
|
||||
|
||||
else:
|
||||
# Raw string that is not yet formatted or markdown-rendered
|
||||
body_content_html = self.compile_markdown(content, context)
|
||||
if context:
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
body_content_html = format_map(
|
||||
body_content_html,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
'body': body_md,
|
||||
'body': mark_safe(body_content_html),
|
||||
'subject': str(subject),
|
||||
'color': settings.PRETIX_PRIMARY_COLOR,
|
||||
'rtl': get_language() in settings.LANGUAGES_RTL or get_language().split('-')[0] in settings.LANGUAGES_RTL,
|
||||
|
||||
@@ -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
|
||||
@@ -261,17 +262,22 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
|
||||
_autoextend_context(context, order)
|
||||
|
||||
# Build raw content
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
content = render_mail(template, context, placeholder_mode=None)
|
||||
if settings_holder:
|
||||
signature = str(settings_holder.settings.get('mail_text_signature'))
|
||||
else:
|
||||
signature = ""
|
||||
|
||||
# Build full plain-text body
|
||||
if not isinstance(content_plain, FormattedString):
|
||||
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
if isinstance(content, FormattedString):
|
||||
# Already formatted by render_mail() from format_values
|
||||
body_plain = content
|
||||
elif isinstance(content, PlainHtmlAlternativeString):
|
||||
# Already formatted by render_mail() form a django template
|
||||
body_plain = content.plain
|
||||
else:
|
||||
body_plain = content_plain
|
||||
# Not yet formatted
|
||||
body_plain = format_map(content, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
|
||||
|
||||
# Build subject
|
||||
@@ -298,19 +304,19 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
|
||||
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
body_html = renderer.render(content, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
body_html = renderer.render(content, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
body_html = renderer.render(content, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
@@ -810,15 +816,22 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
|
||||
body = str(template)
|
||||
if context and placeholder_mode:
|
||||
body = format_map(body, context, mode=placeholder_mode)
|
||||
return body
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
context = {
|
||||
# Known bug, should behave differently for plain and HTML but we'll fix after security release
|
||||
|
||||
plain_context = Context({
|
||||
k: v.plain if isinstance(v, PlainHtmlAlternativeString) else v
|
||||
for k, v in context.items()
|
||||
} | {"to_html": False}, autoescape=False)
|
||||
html_context = Context({
|
||||
k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
|
||||
for k, v in context.items()
|
||||
}
|
||||
body = FormattedString(tpl.render(context))
|
||||
return body
|
||||
} | {"to_html": True}, autoescape=True)
|
||||
return PlainHtmlAlternativeString(
|
||||
plain=tpl.template.render(plain_context),
|
||||
html=tpl.template.render(html_context),
|
||||
)
|
||||
|
||||
|
||||
def replace_images_with_cid_paths(body_html):
|
||||
|
||||
@@ -42,7 +42,6 @@ import pytest
|
||||
from django.conf import settings
|
||||
from django.core import mail as djmail
|
||||
from django.test import override_settings
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
@@ -332,13 +331,14 @@ def test_placeholder_html_rendering_from_template(env):
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
# Known bug for now: These should not have HTML for the plain body, but we'll fix this safter the security release
|
||||
assert escape('Event name: <strong>event & co. kg</strong> {currency}') in djmail.outbox[0].body
|
||||
assert '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body
|
||||
assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body
|
||||
# todo: assert '<' not in djmail.outbox[0].body
|
||||
# todo: assert '&' not in djmail.outbox[0].body
|
||||
assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
|
||||
assert 'Event: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
|
||||
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: *Beep*' in djmail.outbox[0].body
|
||||
assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body
|
||||
assert '<a ' not in djmail.outbox[0].body
|
||||
assert '<' not in djmail.outbox[0].body
|
||||
assert '&' not in djmail.outbox[0].body
|
||||
assert 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body
|
||||
assert 'EUR' not in djmail.outbox[0].body
|
||||
html = _extract_html(djmail.outbox[0])
|
||||
@@ -346,11 +346,13 @@ def test_placeholder_html_rendering_from_template(env):
|
||||
assert '<strong>event' not in html
|
||||
assert 'Event name: <strong>event & co. kg</strong> {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: [<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(
|
||||
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>',
|
||||
html
|
||||
)
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
{% load i18n %}
|
||||
This is a test file for sending mails.
|
||||
|
||||
Django variables will get evaluated:
|
||||
|
||||
Event name: {{ event }}
|
||||
|
||||
pretix variables will not in a template rendering:
|
||||
|
||||
Unevaluated placeholder: {currency}
|
||||
|
||||
We can use advanced Django things:
|
||||
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
The language code used for rendering this email is {{ LANGUAGE_CODE }}.
|
||||
|
||||
Custom content is rendered safely without HTML/Markdown/parameter injection
|
||||
unless the parameter is marked as "HTML-safe":
|
||||
|
||||
Payment info:
|
||||
{{ payment_info }}
|
||||
|
||||
**Meta**: {{ meta_Test }}
|
||||
|
||||
Event website: [{{event}}](https://example.org/{{event_slug}})
|
||||
Markdown will not be evaluated when coming from a template file!
|
||||
|
||||
Event website: [{{event}}](https://example.org/{{event_slug}})
|
||||
|
||||
Event: {% if to_html %}<a href="https://example.com/{{event_slug}}">{% endif %}{{ event }}{% if to_html %}</a>{% endif %}
|
||||
Reference in New Issue
Block a user