stages/email: prevent authentik emails from being marked as spam (also add text template support) (#7949)
* use <> style email address with name Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add support for text templates Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix icon display in event log Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add text email templates Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs, update email screenshot Signed-off-by: Jens Langhammer <jens@goauthentik.io> * prevent prettier from breaking example template Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Optimised images with calibre/image-actions * Apply suggestions from code review Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Signed-off-by: Jens L. <jens@beryju.org> * reword docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com> Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
parent
218d61648b
commit
ec8f2d4bf9
|
@ -461,7 +461,7 @@ class NotificationTransport(SerializerModel):
|
||||||
}
|
}
|
||||||
mail = TemplateEmailMessage(
|
mail = TemplateEmailMessage(
|
||||||
subject=subject_prefix + context["title"],
|
subject=subject_prefix + context["title"],
|
||||||
to=[notification.user.email],
|
to=[f"{notification.user.name} <{notification.user.email}>"],
|
||||||
language=notification.user.locale(),
|
language=notification.user.locale(),
|
||||||
template_name="email/event_notification.html",
|
template_name="email/event_notification.html",
|
||||||
template_context=context,
|
template_context=context,
|
||||||
|
|
|
@ -110,7 +110,7 @@ class EmailStageView(ChallengeStageView):
|
||||||
try:
|
try:
|
||||||
message = TemplateEmailMessage(
|
message = TemplateEmailMessage(
|
||||||
subject=_(current_stage.subject),
|
subject=_(current_stage.subject),
|
||||||
to=[email],
|
to=[f"{pending_user.name} <{email}>"],
|
||||||
language=pending_user.locale(self.request),
|
language=pending_user.locale(self.request),
|
||||||
template_name=current_stage.template,
|
template_name=current_stage.template,
|
||||||
template_context={
|
template_context={
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -29,3 +29,9 @@ def inline_static_binary(path: str) -> str:
|
||||||
b64content = b64encode(_file.read().encode())
|
b64content = b64encode(_file.read().encode())
|
||||||
return f"data:image/{result.suffix};base64,{b64content.decode('utf-8')}"
|
return f"data:image/{result.suffix};base64,{b64content.decode('utf-8')}"
|
||||||
return path
|
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)
|
||||||
|
|
|
@ -58,9 +58,11 @@ class TestEmailStageSending(FlowTestCase):
|
||||||
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
|
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
|
||||||
self.assertEqual(len(events), 1)
|
self.assertEqual(len(events), 1)
|
||||||
event = events.first()
|
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["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")
|
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
||||||
|
|
||||||
def test_pending_fake_user(self):
|
def test_pending_fake_user(self):
|
||||||
|
|
|
@ -94,7 +94,7 @@ class TestEmailStage(FlowTestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
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(
|
@patch(
|
||||||
"authentik.stages.email.models.EmailStage.backend_class",
|
"authentik.stages.email.models.EmailStage.backend_class",
|
||||||
|
@ -114,7 +114,7 @@ class TestEmailStage(FlowTestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
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} <foo@bar.baz>"])
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
"authentik.stages.email.models.EmailStage.backend_class",
|
"authentik.stages.email.models.EmailStage.backend_class",
|
||||||
|
|
|
@ -4,6 +4,7 @@ from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.template.exceptions import TemplateDoesNotExist
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
|
||||||
|
@ -24,9 +25,15 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
||||||
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
|
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
|
||||||
|
|
||||||
def __init__(self, template_name=None, template_context=None, language="", **kwargs):
|
def __init__(self, template_name=None, template_context=None, language="", **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
with translation.override(language):
|
with translation.override(language):
|
||||||
html_content = render_to_string(template_name, template_context)
|
html_content = render_to_string(template_name, template_context)
|
||||||
super().__init__(**kwargs)
|
try:
|
||||||
self.content_subtype = "html"
|
text_content = render_to_string(
|
||||||
|
template_name.replace("html", "txt"), template_context
|
||||||
|
)
|
||||||
|
self.body = text_content
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
pass
|
||||||
self.mixed_subtype = "related"
|
self.mixed_subtype = "related"
|
||||||
self.attach_alternative(html_content, "text/html")
|
self.attach_alternative(html_content, "text/html")
|
||||||
|
|
|
@ -285,10 +285,12 @@ export class EventInfo extends AKElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmailSent() {
|
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`<div class="pf-c-card__title">${msg("Email info:")}</div>
|
return html`<div class="pf-c-card__title">${msg("Email info:")}</div>
|
||||||
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
|
<div class="pf-c-card__body">${this.getEmailInfo(this.event.context)}</div>
|
||||||
<ak-expand>
|
<ak-expand>
|
||||||
<iframe srcdoc=${this.event.context.body}></iframe>
|
<iframe srcdoc=${body}></iframe>
|
||||||
</ak-expand>`;
|
</ak-expand>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
|
@ -25,6 +25,10 @@ return True
|
||||||
|
|
||||||
You can also use custom email templates, to use your own design or layout.
|
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 Tabs from "@theme/Tabs";
|
||||||
import TabItem from "@theme/TabItem";
|
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.
|
- `user`: The pending user object.
|
||||||
- `expires`: The timestamp when the token expires.
|
- `expires`: The timestamp when the token expires.
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
```html
|
```html
|
||||||
{# This is how you can write comments which aren't rendered. #} {# Extend this
|
{# This is how you can write comments which aren't rendered. #}
|
||||||
template from the base email template, which includes base layout and CSS. #} {%
|
{# Extend this template from the base email template, which includes base layout and CSS. #}
|
||||||
extends "email/base.html" %} {# Load the internationalization module to
|
{% extends "email/base.html" %}
|
||||||
translate strings, and humanize to show date-time #} {% load i18n %} {% load
|
{# Load the internationalization module to translate strings, and humanize to show date-time #}
|
||||||
humanize %} {# The email/base.html template uses a single "content" block #} {%
|
{% load i18n %}
|
||||||
block content %}
|
{% load humanize %}
|
||||||
|
{# The email/base.html template uses a single "content" block #}
|
||||||
|
{% block content %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="alert alert-success">
|
<td class="alert alert-success">
|
||||||
{% blocktrans with username=user.username %} Hi {{ username }}, {%
|
{% blocktrans with username=user.username %} Hi {{ username }}, {%
|
||||||
|
@ -99,9 +107,9 @@ block content %}
|
||||||
<table width="100%" cellpadding="0" cellspacing="0">
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="content-block">
|
<td class="content-block">
|
||||||
{% blocktrans %} You recently requested to change your
|
{% blocktrans %}
|
||||||
password for you authentik account. Use the button below to
|
You recently requested to change your password for you authentik account. Use the button below to set a new password.
|
||||||
set a new password. {% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -130,8 +138,7 @@ block content %}
|
||||||
href="{{ url }}"
|
href="{{ url }}"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>{% trans 'Reset
|
>{% trans 'Reset Password' %}</a
|
||||||
Password' %}</a
|
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -145,9 +152,9 @@ block content %}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="content-block">
|
<td class="content-block">
|
||||||
{% blocktrans with expires=expires|naturaltime %} If you did
|
{% blocktrans with expires=expires|naturaltime %}
|
||||||
not request a password change, please ignore this Email. The
|
If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}.
|
||||||
link above is valid for {{ expires }}. {% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -155,3 +162,5 @@ block content %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
Reference in New Issue