diff --git a/authentik/events/models.py b/authentik/events/models.py index 965e38cc2..240ba11de 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -461,7 +461,7 @@ class NotificationTransport(SerializerModel): } mail = TemplateEmailMessage( subject=subject_prefix + context["title"], - to=[notification.user.email], + to=[f"{notification.user.name} <{notification.user.email}>"], language=notification.user.locale(), template_name="email/event_notification.html", template_context=context, diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index 0fa36bfbe..160a68e92 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -110,7 +110,7 @@ class EmailStageView(ChallengeStageView): try: message = TemplateEmailMessage( subject=_(current_stage.subject), - to=[email], + to=[f"{pending_user.name} <{email}>"], language=pending_user.locale(self.request), template_name=current_stage.template, template_context={ diff --git a/authentik/stages/email/templates/email/account_confirmation.txt b/authentik/stages/email/templates/email/account_confirmation.txt new file mode 100644 index 000000000..0a1fc70d1 --- /dev/null +++ b/authentik/stages/email/templates/email/account_confirmation.txt @@ -0,0 +1,8 @@ +{% load i18n %}{% translate "Welcome!" %} + +{% translate "We're excited to have you get started. First, you need to confirm your account. Just open the link below." %} + +{{ url }} + +-- +Powered by goauthentik.io. diff --git a/authentik/stages/email/templates/email/event_notification.html b/authentik/stages/email/templates/email/event_notification.html index e34563404..7ca78fc64 100644 --- a/authentik/stages/email/templates/email/event_notification.html +++ b/authentik/stages/email/templates/email/event_notification.html @@ -44,7 +44,7 @@ {% blocktranslate with name=source.from %} - This email was sent from the notification transport {{name}}. + This email was sent from the notification transport {{ name }}. {% endblocktranslate %} diff --git a/authentik/stages/email/templates/email/event_notification.txt b/authentik/stages/email/templates/email/event_notification.txt new file mode 100644 index 000000000..bd7d92896 --- /dev/null +++ b/authentik/stages/email/templates/email/event_notification.txt @@ -0,0 +1,18 @@ +{% load authentik_stages_email %}{% load i18n %}{% translate "Dear authentik user," %} + +{% translate "The following notification was created:" %} + + {{ body|indent }} + +{% if key_value %} +{% translate "Additional attributes:" %} +{% for key, value in key_value.items %} + {{ key }}: {{ value|indent }}{% endfor %} +{% endif %} + +{% if source %}{% blocktranslate with name=source.from %} +This email was sent from the notification transport {{ name }}. +{% endblocktranslate %}{% endif %} + +-- +Powered by goauthentik.io. diff --git a/authentik/stages/email/templates/email/password_reset.txt b/authentik/stages/email/templates/email/password_reset.txt new file mode 100644 index 000000000..0c13ad2f8 --- /dev/null +++ b/authentik/stages/email/templates/email/password_reset.txt @@ -0,0 +1,12 @@ +{% load i18n %}{% load humanize %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %} + +{% blocktrans %} +You recently requested to change your password for your authentik account. Use the link below to set a new password. +{% endblocktrans %} +{{ url }} +{% blocktrans with expires=expires|naturaltime %} +If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}. +{% endblocktrans %} + +-- +Powered by goauthentik.io. diff --git a/authentik/stages/email/templates/email/setup.txt b/authentik/stages/email/templates/email/setup.txt new file mode 100644 index 000000000..6d0eb0ce0 --- /dev/null +++ b/authentik/stages/email/templates/email/setup.txt @@ -0,0 +1,7 @@ +{% load i18n %}authentik Test-Email +{% blocktrans %} +This is a test email to inform you, that you've successfully configured authentik emails. +{% endblocktrans %} + +-- +Powered by goauthentik.io. diff --git a/authentik/stages/email/templatetags/authentik_stages_email.py b/authentik/stages/email/templatetags/authentik_stages_email.py index 7623c6c71..9b3dfb194 100644 --- a/authentik/stages/email/templatetags/authentik_stages_email.py +++ b/authentik/stages/email/templatetags/authentik_stages_email.py @@ -29,3 +29,9 @@ def inline_static_binary(path: str) -> str: b64content = b64encode(_file.read().encode()) return f"data:image/{result.suffix};base64,{b64content.decode('utf-8')}" return path + + +@register.filter(name="indent") +def indent_string(val, num_spaces=4): + """Intent text by a given amount of spaces""" + return val.replace("\n", "\n" + " " * num_spaces) diff --git a/authentik/stages/email/tests/test_sending.py b/authentik/stages/email/tests/test_sending.py index 424d474ce..5c67c8842 100644 --- a/authentik/stages/email/tests/test_sending.py +++ b/authentik/stages/email/tests/test_sending.py @@ -58,9 +58,11 @@ class TestEmailStageSending(FlowTestCase): events = Event.objects.filter(action=EventAction.EMAIL_SENT) self.assertEqual(len(events), 1) event = events.first() - self.assertEqual(event.context["message"], f"Email to {self.user.email} sent") + self.assertEqual( + event.context["message"], f"Email to {self.user.name} <{self.user.email}> sent" + ) self.assertEqual(event.context["subject"], "authentik") - self.assertEqual(event.context["to_email"], [self.user.email]) + self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"]) self.assertEqual(event.context["from_email"], "system@authentik.local") def test_pending_fake_user(self): diff --git a/authentik/stages/email/tests/test_stage.py b/authentik/stages/email/tests/test_stage.py index 853bc2a77..277549937 100644 --- a/authentik/stages/email/tests/test_stage.py +++ b/authentik/stages/email/tests/test_stage.py @@ -94,7 +94,7 @@ class TestEmailStage(FlowTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "authentik") - self.assertEqual(mail.outbox[0].to, [self.user.email]) + self.assertEqual(mail.outbox[0].to, [f"{self.user.name} <{self.user.email}>"]) @patch( "authentik.stages.email.models.EmailStage.backend_class", @@ -114,7 +114,7 @@ class TestEmailStage(FlowTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "authentik") - self.assertEqual(mail.outbox[0].to, ["foo@bar.baz"]) + self.assertEqual(mail.outbox[0].to, [f"{self.user.name} "]) @patch( "authentik.stages.email.models.EmailStage.backend_class", diff --git a/authentik/stages/email/utils.py b/authentik/stages/email/utils.py index a6edd4609..8f7f702ce 100644 --- a/authentik/stages/email/utils.py +++ b/authentik/stages/email/utils.py @@ -4,6 +4,7 @@ from functools import lru_cache from pathlib import Path from django.core.mail import EmailMultiAlternatives +from django.template.exceptions import TemplateDoesNotExist from django.template.loader import render_to_string from django.utils import translation @@ -24,9 +25,15 @@ class TemplateEmailMessage(EmailMultiAlternatives): """Wrapper around EmailMultiAlternatives with integrated template rendering""" def __init__(self, template_name=None, template_context=None, language="", **kwargs): + super().__init__(**kwargs) with translation.override(language): html_content = render_to_string(template_name, template_context) - super().__init__(**kwargs) - self.content_subtype = "html" + try: + text_content = render_to_string( + template_name.replace("html", "txt"), template_context + ) + self.body = text_content + except TemplateDoesNotExist: + pass self.mixed_subtype = "related" self.attach_alternative(html_content, "text/html") diff --git a/web/src/components/ak-event-info.ts b/web/src/components/ak-event-info.ts index 6901f31a8..e728958c2 100644 --- a/web/src/components/ak-event-info.ts +++ b/web/src/components/ak-event-info.ts @@ -285,10 +285,12 @@ export class EventInfo extends AKElement { } renderEmailSent() { + let body = this.event.context.body as string; + body = body.replace("cid:logo.png", "/static/dist/assets/icons/icon_left_brand.png"); return html`
${msg("Email info:")}
${this.getEmailInfo(this.event.context)}
- + `; } diff --git a/website/docs/flow/stages/email/email_recovery.png b/website/docs/flow/stages/email/email_recovery.png index 1dc5dbbc4..0bc14bafd 100644 Binary files a/website/docs/flow/stages/email/email_recovery.png and b/website/docs/flow/stages/email/email_recovery.png differ diff --git a/website/docs/flow/stages/email/index.mdx b/website/docs/flow/stages/email/index.mdx index 1064d8f13..a7751e295 100644 --- a/website/docs/flow/stages/email/index.mdx +++ b/website/docs/flow/stages/email/index.mdx @@ -25,6 +25,10 @@ return True You can also use custom email templates, to use your own design or layout. +:::info +Starting with authentik 2024.1, it is possible to create `.txt` files with the same name as the `.html` template. If a matching `.txt` file exists, the email sent will be a multipart email with both the text and HTML template. +::: + import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; @@ -81,13 +85,17 @@ Templates are rendered using Django's templating engine. The following variables - `user`: The pending user object. - `expires`: The timestamp when the token expires. + + ```html -{# This is how you can write comments which aren't rendered. #} {# Extend this -template from the base email template, which includes base layout and CSS. #} {% -extends "email/base.html" %} {# Load the internationalization module to -translate strings, and humanize to show date-time #} {% load i18n %} {% load -humanize %} {# The email/base.html template uses a single "content" block #} {% -block content %} +{# This is how you can write comments which aren't rendered. #} +{# Extend this template from the base email template, which includes base layout and CSS. #} +{% extends "email/base.html" %} +{# Load the internationalization module to translate strings, and humanize to show date-time #} +{% load i18n %} +{% load humanize %} +{# The email/base.html template uses a single "content" block #} +{% block content %} {% blocktrans with username=user.username %} Hi {{ username }}, {% @@ -99,9 +107,9 @@ block content %} @@ -130,8 +138,7 @@ block content %} href="{{ url }}" rel="noopener noreferrer" target="_blank" - >{% trans 'Reset - Password' %}{% trans 'Reset Password' %} @@ -145,9 +152,9 @@ block content %}
- {% blocktrans %} You recently requested to change your - password for you authentik account. Use the button below to - set a new password. {% endblocktrans %} + {% blocktrans %} + You recently requested to change your password for you authentik account. Use the button below to set a new password. + {% endblocktrans %}
- {% blocktrans with expires=expires|naturaltime %} If you did - not request a password change, please ignore this Email. The - link above is valid for {{ expires }}. {% endblocktrans %} + {% blocktrans with expires=expires|naturaltime %} + If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}. + {% endblocktrans %}
@@ -155,3 +162,5 @@ block content %} {% endblock %} ``` + +