diff --git a/authentik/admin/templates/administration/task/list.html b/authentik/admin/templates/administration/task/list.html
index 7f0ef9ea5..373bff1fc 100644
--- a/authentik/admin/templates/administration/task/list.html
+++ b/authentik/admin/templates/administration/task/list.html
@@ -38,7 +38,7 @@
{% for task in object_list %}
- {{ task.task_name }}
+ {{ task.html_name|join:"_" }}
|
diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml
index 4f9e1bd56..72f19b633 100644
--- a/authentik/lib/default.yml
+++ b/authentik/lib/default.yml
@@ -21,6 +21,17 @@ error_reporting:
environment: customer
send_pii: false
+# Global email settings
+email:
+ host: localhost
+ port: 25
+ username: ""
+ password: ""
+ use_tls: false
+ use_ssl: false
+ timeout: 10
+ from: authentik@localhost
+
outposts:
docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
diff --git a/authentik/lib/tasks.py b/authentik/lib/tasks.py
index eb9aff13c..132d03a5e 100644
--- a/authentik/lib/tasks.py
+++ b/authentik/lib/tasks.py
@@ -52,6 +52,11 @@ class TaskInfo:
task_description: Optional[str] = field(default=None)
+ @property
+ def html_name(self) -> list[str]:
+ """Get task_name, but split on underscores, so we can join in the html template."""
+ return self.task_name.split("_")
+
@staticmethod
def all() -> Dict[str, "TaskInfo"]:
"""Get all TaskInfo objects"""
diff --git a/authentik/root/settings.py b/authentik/root/settings.py
index d68852758..4cc359628 100644
--- a/authentik/root/settings.py
+++ b/authentik/root/settings.py
@@ -196,7 +196,7 @@ ROOT_URLCONF = "authentik.root.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
- "DIRS": [],
+ "DIRS": ["/templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
@@ -238,6 +238,18 @@ DATABASES = {
}
}
+# Email
+EMAIL_HOST = CONFIG.y("email.host")
+EMAIL_PORT = int(CONFIG.y("email.port"))
+EMAIL_HOST_USER = CONFIG.y("email.username")
+EMAIL_HOST_PASSWORD = CONFIG.y("email.password")
+EMAIL_USE_TLS = CONFIG.y("email.use_tls")
+EMAIL_USE_SSL = CONFIG.y("email.use_ssl")
+EMAIL_TIMEOUT = int(CONFIG.y("email.timeout"))
+DEFAULT_FROM_EMAIL = CONFIG.y("email.from")
+SERVER_EMAIL = DEFAULT_FROM_EMAIL
+EMAIL_SUBJECT_PREFIX = "[authentik] "
+
# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
diff --git a/authentik/stages/email/api.py b/authentik/stages/email/api.py
index b3a4860f0..01cdf3d2b 100644
--- a/authentik/stages/email/api.py
+++ b/authentik/stages/email/api.py
@@ -2,18 +2,23 @@
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
-from authentik.stages.email.models import EmailStage
+from authentik.stages.email.models import EmailStage, get_template_choices
class EmailStageSerializer(ModelSerializer):
"""EmailStage Serializer"""
+ def __init__(self, *args, **kwrags):
+ super().__init__(*args, **kwrags)
+ self.fields["template"].choices = get_template_choices()
+
class Meta:
model = EmailStage
fields = [
"pk",
"name",
+ "use_global_settings",
"host",
"port",
"username",
diff --git a/authentik/stages/email/apps.py b/authentik/stages/email/apps.py
index e7fe92115..5bd33d6f7 100644
--- a/authentik/stages/email/apps.py
+++ b/authentik/stages/email/apps.py
@@ -2,6 +2,12 @@
from importlib import import_module
from django.apps import AppConfig
+from django.db import ProgrammingError
+from django.template.exceptions import TemplateDoesNotExist
+from django.template.loader import get_template
+from structlog.stdlib import get_logger
+
+LOGGER = get_logger()
class AuthentikStageEmailConfig(AppConfig):
@@ -13,3 +19,30 @@ class AuthentikStageEmailConfig(AppConfig):
def ready(self):
import_module("authentik.stages.email.tasks")
+ try:
+ self.validate_stage_templates()
+ except ProgrammingError:
+ pass
+
+ def validate_stage_templates(self):
+ """Ensure all stage's templates actually exist"""
+ from authentik.stages.email.models import EmailStage, EmailTemplates
+ from authentik.events.models import Event, EventAction
+
+ for stage in EmailStage.objects.all():
+ try:
+ get_template(stage.template)
+ except TemplateDoesNotExist:
+ LOGGER.warning(
+ "Stage template does not exist, resetting", path=stage.template
+ )
+ Event.new(
+ EventAction.CONFIGURATION_ERROR,
+ stage=stage,
+ message=(
+ f"Template {stage.template} does not exist, resetting to default."
+ f" (Stage {stage.name})"
+ ),
+ ).save()
+ stage.template = EmailTemplates.ACCOUNT_CONFIRM
+ stage.save()
diff --git a/authentik/stages/email/forms.py b/authentik/stages/email/forms.py
index 6c3ea986f..303d7834b 100644
--- a/authentik/stages/email/forms.py
+++ b/authentik/stages/email/forms.py
@@ -2,7 +2,7 @@
from django import forms
from django.utils.translation import gettext_lazy as _
-from authentik.stages.email.models import EmailStage
+from authentik.stages.email.models import EmailStage, get_template_choices
class EmailStageSendForm(forms.Form):
@@ -14,11 +14,17 @@ class EmailStageSendForm(forms.Form):
class EmailStageForm(forms.ModelForm):
"""Form to create/edit Email Stage"""
+ template = forms.ChoiceField(choices=get_template_choices)
+
class Meta:
model = EmailStage
fields = [
"name",
+ "use_global_settings",
+ "token_expiry",
+ "subject",
+ "template",
"host",
"port",
"username",
@@ -27,9 +33,6 @@ class EmailStageForm(forms.ModelForm):
"use_ssl",
"timeout",
"from_address",
- "token_expiry",
- "subject",
- "template",
]
widgets = {
"name": forms.TextInput(),
diff --git a/authentik/stages/email/management/__init__.py b/authentik/stages/email/management/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/stages/email/management/commands/__init__.py b/authentik/stages/email/management/commands/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/stages/email/management/commands/test_email.py b/authentik/stages/email/management/commands/test_email.py
new file mode 100644
index 000000000..02a190c0a
--- /dev/null
+++ b/authentik/stages/email/management/commands/test_email.py
@@ -0,0 +1,42 @@
+"""Send a test-email with global settings"""
+from uuid import uuid4
+
+from django.core.management.base import BaseCommand, no_translations
+
+from authentik.stages.email.models import EmailStage
+from authentik.stages.email.tasks import send_mail
+from authentik.stages.email.utils import TemplateEmailMessage
+
+
+class Command(BaseCommand): # pragma: no cover
+ """Send a test-email with global settings"""
+
+ @no_translations
+ def handle(self, *args, **options):
+ """Send a test-email with global settings"""
+ delete_stage = False
+ if options["stage"]:
+ stage = EmailStage.objects.get(name=options["stage"])
+ else:
+ stage = EmailStage.objects.create(
+ name=f"temp-global-stage-{uuid4()}", use_global_settings=True
+ )
+ delete_stage = True
+ message = TemplateEmailMessage(
+ subject="authentik Test-Email",
+ template_name="email/setup.html",
+ to=[options["to"]],
+ template_context={},
+ )
+ try:
+ # pyright: reportGeneralTypeIssues=false
+ send_mail( # pylint: disable=no-value-for-parameter
+ stage.pk, message.__dict__
+ )
+ finally:
+ if delete_stage:
+ stage.delete()
+
+ def add_arguments(self, parser):
+ parser.add_argument("to", type=str)
+ parser.add_argument("-s", "--stage", type=str)
diff --git a/authentik/stages/email/migrations/0001_initial.py b/authentik/stages/email/migrations/0001_initial.py
index b3412e76d..8db5fb842 100644
--- a/authentik/stages/email/migrations/0001_initial.py
+++ b/authentik/stages/email/migrations/0001_initial.py
@@ -50,15 +50,15 @@ class Migration(migrations.Migration):
models.TextField(
choices=[
(
- "stages/email/for_email/password_reset.html",
+ "email/password_reset.html",
"Password Reset",
),
(
- "stages/email/for_email/account_confirmation.html",
+ "email/account_confirmation.html",
"Account Confirmation",
),
],
- default="stages/email/for_email/password_reset.html",
+ default="email/password_reset.html",
),
),
],
diff --git a/authentik/stages/email/migrations/0002_emailstage_use_global_settings.py b/authentik/stages/email/migrations/0002_emailstage_use_global_settings.py
new file mode 100644
index 000000000..a13acf2de
--- /dev/null
+++ b/authentik/stages/email/migrations/0002_emailstage_use_global_settings.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.1.4 on 2021-01-04 13:15
+
+from django.apps.registry import Apps
+from django.db import migrations, models
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+
+def update_template_path(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
+ EmailStage = apps.get_model("authentik_stages_email", "EmailStage")
+ db_alias = schema_editor.connection.alias
+
+ for stage in EmailStage.objects.using(db_alias).all():
+ stage.template = stage.template.replace("stages/email/for_email/", "email/")
+ stage.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_email", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="emailstage",
+ name="use_global_settings",
+ field=models.BooleanField(
+ default=False,
+ help_text="When enabled, global Email connection settings will be used and connection settings below will be ignored.",
+ ),
+ ),
+ migrations.RunPython(update_template_path),
+ ]
diff --git a/authentik/stages/email/models.py b/authentik/stages/email/models.py
index 448e0f97b..936952b15 100644
--- a/authentik/stages/email/models.py
+++ b/authentik/stages/email/models.py
@@ -1,6 +1,9 @@
"""email stage models"""
+from os import R_OK, access
+from pathlib import Path
from typing import Type
+from django.conf import settings
from django.core.mail import get_connection
from django.core.mail.backends.base import BaseEmailBackend
from django.db import models
@@ -8,26 +11,60 @@ from django.forms import ModelForm
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.serializers import BaseSerializer
+from structlog.stdlib import get_logger
from authentik.flows.models import Stage
+LOGGER = get_logger()
+
class EmailTemplates(models.TextChoices):
"""Templates used for rendering the Email"""
PASSWORD_RESET = (
- "stages/email/for_email/password_reset.html",
+ "email/password_reset.html",
_("Password Reset"),
) # nosec
ACCOUNT_CONFIRM = (
- "stages/email/for_email/account_confirmation.html",
+ "email/account_confirmation.html",
_("Account Confirmation"),
)
+def get_template_choices():
+ """Get all available Email templates, including dynamically mounted ones.
+ Directories are taken from TEMPLATES.DIR setting"""
+ static_choices = EmailTemplates.choices
+
+ dirs = [Path(x) for x in settings.TEMPLATES[0]["DIRS"]]
+ for template_dir in dirs:
+ if not template_dir.exists():
+ continue
+ for template in template_dir.glob("**/*.html"):
+ path = str(template)
+ if not access(path, R_OK):
+ LOGGER.warning(
+ "Custom template file is not readable, check permissions", path=path
+ )
+ continue
+ rel_path = template.relative_to(template_dir)
+ static_choices.append((str(rel_path), f"Custom Template: {rel_path}"))
+ return static_choices
+
+
class EmailStage(Stage):
"""Sends an Email to the user with a token to confirm their Email address."""
+ use_global_settings = models.BooleanField(
+ default=False,
+ help_text=_(
+ (
+ "When enabled, global Email connection settings will be used and "
+ "connection settings below will be ignored."
+ )
+ ),
+ )
+
host = models.TextField(default="localhost")
port = models.IntegerField(default=25)
username = models.TextField(default="", blank=True)
@@ -42,7 +79,7 @@ class EmailStage(Stage):
)
subject = models.TextField(default="authentik")
template = models.TextField(
- choices=EmailTemplates.choices, default=EmailTemplates.PASSWORD_RESET
+ choices=get_template_choices(), default=EmailTemplates.PASSWORD_RESET
)
@property
@@ -65,7 +102,9 @@ class EmailStage(Stage):
@property
def backend(self) -> BaseEmailBackend:
- """Get fully configured EMail Backend instance"""
+ """Get fully configured Email Backend instance"""
+ if self.use_global_settings:
+ return get_connection()
return get_connection(
host=self.host,
port=self.port,
diff --git a/authentik/stages/email/static/stages/email/css/base.css b/authentik/stages/email/static/stages/email/css/base.css
index 6f624e138..b8b6ab985 100644
--- a/authentik/stages/email/static/stages/email/css/base.css
+++ b/authentik/stages/email/static/stages/email/css/base.css
@@ -1,325 +1,285 @@
/* -------------------------------------
-GLOBAL RESETS
+ GLOBAL
+ A very basic CSS reset
------------------------------------- */
-
-/*All the styling goes here*/
+* {
+ margin: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ box-sizing: border-box;
+ font-size: 14px;
+}
img {
- border: none;
- -ms-interpolation-mode: bicubic;
- max-width: 100%;
+ max-width: 100%;
}
body {
- background-color: #fafafa;
- font-family: sans-serif;
- -webkit-font-smoothing: antialiased;
- font-size: 14px;
- line-height: 1.4;
- margin: 0;
- padding: 0;
- -ms-text-size-adjust: 100%;
- -webkit-text-size-adjust: 100%;
+ -webkit-font-smoothing: antialiased;
+ -webkit-text-size-adjust: none;
+ width: 100% !important;
+ height: 100%;
+ line-height: 1.6em;
+ /* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
+ /*line-height: 22px;*/
}
-table {
- border-collapse: separate;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- width: 100%; }
- table td {
- font-family: sans-serif;
- font-size: 14px;
- vertical-align: top;
- }
+/* Let's make sure all tables have defaults */
+table td {
+ vertical-align: top;
+}
- /* -------------------------------------
+/* -------------------------------------
BODY & CONTAINER
- ------------------------------------- */
+------------------------------------- */
+body {
+ background-color: #f6f6f6;
+}
- .body {
- background-color: #fafafa;
- width: 100%;
- }
+.body-wrap {
+ background-color: #f6f6f6;
+ width: 100%;
+}
- /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
- .container {
- display: block;
- margin: 0 auto !important;
- /* makes it centered */
- max-width: 580px;
- padding: 10px;
- width: 580px;
- }
+.container {
+ display: block !important;
+ max-width: 600px !important;
+ margin: 0 auto !important;
+ /* makes it centered */
+ clear: both !important;
+}
- /* This should also be a block element, so that it will fill 100% of the .container */
- .content {
- box-sizing: border-box;
- display: block;
- margin: 0 auto;
- max-width: 580px;
- padding: 10px;
- }
+.content {
+ max-width: 600px;
+ margin: 0 auto;
+ display: block;
+ padding: 20px;
+}
- /* -------------------------------------
+/* -------------------------------------
HEADER, FOOTER, MAIN
- ------------------------------------- */
- .main {
- background: #ffffff;
- border-radius: 3px;
- width: 100%;
- }
+------------------------------------- */
+.main {
+ background-color: #fff;
+ border: 1px solid #e9e9e9;
+ border-radius: 3px;
+}
- .wrapper {
- box-sizing: border-box;
- padding: 20px;
- }
+.content-wrap {
+ padding: 20px;
+}
- .content-block {
- padding-bottom: 10px;
- padding-top: 10px;
- }
+.content-block {
+ padding: 0 0 20px;
+}
- .footer {
- clear: both;
- margin-top: 10px;
- text-align: center;
- width: 100%;
- }
- .footer td,
- .footer p,
- .footer span,
- .footer a {
- color: #999999;
- font-size: 12px;
- text-align: center;
- }
+.header {
+ width: 100%;
+ margin-bottom: 20px;
+}
- /* -------------------------------------
+.footer {
+ width: 100%;
+ clear: both;
+ color: #999;
+ padding: 20px;
+}
+.footer p, .footer a, .footer td {
+ color: #999;
+ font-size: 12px;
+}
+
+/* -------------------------------------
TYPOGRAPHY
- ------------------------------------- */
- h1,
- h2,
- h3,
- h4 {
- color: #000000;
- font-family: sans-serif;
- font-weight: 400;
- line-height: 1.4;
- margin: 0;
- margin-bottom: 30px;
- }
+------------------------------------- */
+h1, h2, h3 {
+ font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+ color: #000;
+ margin: 40px 0 0;
+ line-height: 1.2em;
+ font-weight: 400;
+}
- h1 {
- font-size: 35px;
- font-weight: 300;
- text-align: center;
- text-transform: capitalize;
- }
+h1 {
+ font-size: 32px;
+ font-weight: 500;
+ /* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
+ /*line-height: 38px;*/
+}
- p,
- ul,
- ol {
- font-family: sans-serif;
- font-size: 14px;
- font-weight: normal;
- margin: 0;
- margin-bottom: 15px;
- }
- p li,
- ul li,
- ol li {
- list-style-position: inside;
- margin-left: 5px;
- }
+h2 {
+ font-size: 24px;
+ /* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
+ /*line-height: 29px;*/
+}
- a {
- color: #06c;
- border-radius: 3px;
- text-decoration: underline;
- }
+h3 {
+ font-size: 18px;
+ /* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
+ /*line-height: 22px;*/
+}
- /* -------------------------------------
- BUTTONS
- ------------------------------------- */
- .btn {
- box-sizing: border-box;
- width: 100%; }
- .btn > tbody > tr > td {
- padding-bottom: 15px; }
- .btn table {
- width: auto;
- }
- .btn table td {
- background-color: #ffffff;
- border-radius: 5px;
- text-align: center;
- }
- .btn a {
- background-color: #ffffff;
- border: solid 1px #06c;
- border-radius: 5px;
- box-sizing: border-box;
- color: #06c;
- cursor: pointer;
- display: inline-block;
- font-size: 14px;
- font-weight: bold;
- margin: 0;
- padding: 12px 25px;
- text-decoration: none;
- text-transform: capitalize;
- }
+h4 {
+ font-size: 14px;
+ font-weight: 600;
+}
- .btn-primary table td {
- background-color: #06c;
- }
+p, ul, ol {
+ margin-bottom: 10px;
+ font-weight: normal;
+}
+p li, ul li, ol li {
+ margin-left: 5px;
+ list-style-position: inside;
+}
- .btn-primary a {
- background-color: #06c;
- border-color: #06c;
- color: #ffffff;
- }
+/* -------------------------------------
+ LINKS & BUTTONS
+------------------------------------- */
+a {
+ color: #348eda;
+ text-decoration: underline;
+}
- /* -------------------------------------
- OTHER STYLES THAT MIGHT BE USEFUL
- ------------------------------------- */
- .last {
- margin-bottom: 0;
- }
+.btn-primary {
+ text-decoration: none;
+ color: #FFF;
+ background-color: #348eda;
+ border: solid #348eda;
+ border-width: 10px 20px;
+ line-height: 2em;
+ /* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
+ /*line-height: 28px;*/
+ font-weight: bold;
+ text-align: center;
+ cursor: pointer;
+ display: inline-block;
+ border-radius: 5px;
+ text-transform: capitalize;
+}
- .first {
- margin-top: 0;
- }
+/* -------------------------------------
+ OTHER STYLES THAT MIGHT BE USEFUL
+------------------------------------- */
+.last {
+ margin-bottom: 0;
+}
- .align-center {
- text-align: center;
- }
+.first {
+ margin-top: 0;
+}
- .align-right {
- text-align: right;
- }
+.aligncenter {
+ text-align: center;
+}
- .align-left {
- text-align: left;
- }
+.alignright {
+ text-align: right;
+}
- .clear {
- clear: both;
- }
+.alignleft {
+ text-align: left;
+}
- .mt0 {
- margin-top: 0;
- }
+.clear {
+ clear: both;
+}
- .mb0 {
- margin-bottom: 0;
- }
+/* -------------------------------------
+ ALERTS
+ Change the class depending on warning email, good email or bad email
+------------------------------------- */
+.alert {
+ font-size: 16px;
+ color: #fff;
+ font-weight: 500;
+ padding: 20px;
+ text-align: center;
+ border-radius: 3px 3px 0 0;
+}
+.alert a {
+ color: #fff;
+ text-decoration: none;
+ font-weight: 500;
+ font-size: 16px;
+}
+.alert-brand {
+ background-color: #fd4b2d;
+}
+.alert-warning {
+ background-color: #F0AB00;
+}
+.alert-danger {
+ background-color: #C9190B;
+}
+.alert-success {
+ background-color: #3E8635;
+}
- .preheader {
- color: transparent;
- display: none;
- height: 0;
- max-height: 0;
- max-width: 0;
- opacity: 0;
- overflow: hidden;
- mso-hide: all;
- visibility: hidden;
- width: 0;
- }
+/* -------------------------------------
+ INVOICE
+ Styles for the billing table
+------------------------------------- */
+.invoice {
+ margin: 40px auto;
+ text-align: left;
+ width: 80%;
+}
+.invoice td {
+ padding: 5px 0;
+}
+.invoice .invoice-items {
+ width: 100%;
+}
+.invoice .invoice-items td {
+ border-top: #eee 1px solid;
+}
+.invoice .invoice-items .total td {
+ border-top: 2px solid #333;
+ border-bottom: 2px solid #333;
+ font-weight: 700;
+}
- .powered-by a {
- text-decoration: none;
- }
+/* -------------------------------------
+ RESPONSIVE AND MOBILE FRIENDLY STYLES
+------------------------------------- */
+@media only screen and (max-width: 640px) {
+ body {
+ padding: 0 !important;
+ }
- hr {
- border: 0;
- border-bottom: 1px solid #fafafa;
- margin: 20px 0;
- }
+ h1, h2, h3, h4 {
+ font-weight: 800 !important;
+ margin: 20px 0 5px !important;
+ }
- /* -------------------------------------
- RESPONSIVE AND MOBILE FRIENDLY STYLES
- ------------------------------------- */
- @media only screen and (max-width: 620px) {
- table[class=body] h1 {
- font-size: 28px !important;
- margin-bottom: 10px !important;
- }
- table[class=body] p,
- table[class=body] ul,
- table[class=body] ol,
- table[class=body] td,
- table[class=body] span,
- table[class=body] a {
- font-size: 16px !important;
- }
- table[class=body] .wrapper,
- table[class=body] .article {
- padding: 10px !important;
- }
- table[class=body] .content {
- padding: 0 !important;
- }
- table[class=body] .container {
- padding: 0 !important;
- width: 100% !important;
- }
- table[class=body] .main {
- border-left-width: 0 !important;
- border-radius: 0 !important;
- border-right-width: 0 !important;
- }
- table[class=body] .btn table {
- width: 100% !important;
- }
- table[class=body] .btn a {
- width: 100% !important;
- }
- table[class=body] .img-responsive {
- height: auto !important;
- max-width: 100% !important;
- width: auto !important;
- }
- }
+ h1 {
+ font-size: 22px !important;
+ }
- /* -------------------------------------
- PRESERVE THESE STYLES IN THE HEAD
- ------------------------------------- */
- @media all {
- .ExternalClass {
- width: 100%;
- }
- .ExternalClass,
- .ExternalClass p,
- .ExternalClass span,
- .ExternalClass font,
- .ExternalClass td,
- .ExternalClass div {
- line-height: 100%;
- }
- .apple-link a {
- color: inherit !important;
- font-family: inherit !important;
- font-size: inherit !important;
- font-weight: inherit !important;
- line-height: inherit !important;
- text-decoration: none !important;
- }
- #MessageViewBody a {
- color: inherit;
- text-decoration: none;
- font-size: inherit;
- font-family: inherit;
- font-weight: inherit;
- line-height: inherit;
- }
- .btn-primary table td:hover {
- background-color: #34495e !important;
- }
- .btn-primary a:hover {
- background-color: #34495e !important;
- border-color: #34495e !important;
- }
- }
+ h2 {
+ font-size: 18px !important;
+ }
+
+ h3 {
+ font-size: 16px !important;
+ }
+
+ .container {
+ padding: 0 !important;
+ width: 100% !important;
+ }
+
+ .content {
+ padding: 0 !important;
+ }
+
+ .content-wrap {
+ padding: 10px !important;
+ }
+
+ .invoice {
+ width: 100% !important;
+ }
+}
+
+/*# sourceMappingURL=styles.css.map */
diff --git a/authentik/stages/email/tasks.py b/authentik/stages/email/tasks.py
index bf1e02634..2903bf002 100644
--- a/authentik/stages/email/tasks.py
+++ b/authentik/stages/email/tasks.py
@@ -6,6 +6,7 @@ from typing import Any, Dict, List
from celery import group
from django.core.mail import EmailMultiAlternatives
from django.core.mail.utils import DNS_NAME
+from django.utils.text import slugify
from structlog.stdlib import get_logger
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
@@ -38,7 +39,7 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
"""Send Email for Email Stage. Retries are scheduled automatically."""
self.save_on_success = False
message_id = make_msgid(domain=DNS_NAME)
- self.set_uid(message_id)
+ self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
try:
stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
backend = stage.backend
@@ -48,7 +49,8 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
message_object = EmailMultiAlternatives()
for key, value in message.items():
setattr(message_object, key, value)
- message_object.from_email = stage.from_address
+ if not stage.use_global_settings:
+ message_object.from_email = stage.from_address
# Because we use the Message-ID as UID for the task, manually assign it
message_object.extra_headers["Message-ID"] = message_id
@@ -61,5 +63,6 @@ def send_mail(self: MonitoredTask, email_stage_pk: int, message: Dict[Any, Any])
)
)
except (SMTPException, ConnectionError) as exc:
+ LOGGER.debug("Error sending email, retrying...", exc=exc)
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
raise exc
diff --git a/authentik/stages/email/templates/stages/email/for_email/account_confirmation.html b/authentik/stages/email/templates/email/account_confirmation.html
similarity index 95%
rename from authentik/stages/email/templates/stages/email/for_email/account_confirmation.html
rename to authentik/stages/email/templates/email/account_confirmation.html
index 8c2b353e4..b6e1f7f62 100644
--- a/authentik/stages/email/templates/stages/email/for_email/account_confirmation.html
+++ b/authentik/stages/email/templates/email/account_confirmation.html
@@ -1,4 +1,4 @@
-{% extends 'stages/email/for_email/base.html' %}
+{% extends 'email/base.html' %}
{% load authentik_stages_email %}
{% load i18n %}
diff --git a/authentik/stages/email/templates/email/base.html b/authentik/stages/email/templates/email/base.html
new file mode 100644
index 000000000..3797461a1
--- /dev/null
+++ b/authentik/stages/email/templates/email/base.html
@@ -0,0 +1,34 @@
+{% load authentik_stages_email %}
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ {% block content %}
+ {% endblock %}
+
+
+
+ |
+ |
+
+
+
+
diff --git a/authentik/stages/email/templates/email/generic.html b/authentik/stages/email/templates/email/generic.html
new file mode 100644
index 000000000..01a242111
--- /dev/null
+++ b/authentik/stages/email/templates/email/generic.html
@@ -0,0 +1,20 @@
+{% extends "email/base.html" %}
+
+{% block content %}
+
+
+ {{ title }}
+ |
+
+
+
+
+ |
+
+{% endblock %}
diff --git a/authentik/stages/email/templates/stages/email/for_email/password_reset.html b/authentik/stages/email/templates/email/password_reset.html
similarity index 95%
rename from authentik/stages/email/templates/stages/email/for_email/password_reset.html
rename to authentik/stages/email/templates/email/password_reset.html
index d6818daf2..3d6659ad4 100644
--- a/authentik/stages/email/templates/stages/email/for_email/password_reset.html
+++ b/authentik/stages/email/templates/email/password_reset.html
@@ -1,4 +1,4 @@
-{% extends "stages/email/for_email/base.html" %}
+{% extends "email/base.html" %}
{% load authentik_utils %}
{% load i18n %}
diff --git a/authentik/stages/email/templates/email/setup.html b/authentik/stages/email/templates/email/setup.html
new file mode 100644
index 000000000..1efc08004
--- /dev/null
+++ b/authentik/stages/email/templates/email/setup.html
@@ -0,0 +1,25 @@
+{% extends "email/base.html" %}
+
+{% load authentik_stages_email %}
+{% load i18n %}
+
+{% block content %}
+
+
+ {% trans 'authentik Test-Email' %}
+ |
+
+
+
+
+
+
+ {% blocktrans %}
+ This is a test email to inform you, that you've successfully configured authentik emails.
+ {% endblocktrans %}
+ |
+
+
+ |
+
+{% endblock %}
diff --git a/authentik/stages/email/templates/stages/email/for_email/base.html b/authentik/stages/email/templates/stages/email/for_email/base.html
deleted file mode 100644
index 1261007a9..000000000
--- a/authentik/stages/email/templates/stages/email/for_email/base.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% load authentik_stages_email %}
-{% load authentik_utils %}
-{% load static %}
-{% load i18n %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
- {% block content %}
- {% endblock %}
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
- |
- |
-
-
-
-
-
diff --git a/authentik/stages/email/templates/stages/email/for_email/generic_email.html b/authentik/stages/email/templates/stages/email/for_email/generic_email.html
deleted file mode 100644
index 8246f93b6..000000000
--- a/authentik/stages/email/templates/stages/email/for_email/generic_email.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{% extends "stages/email/for_email/base.html" %}
-
-{% block content %}
-
-
-
- |
-
-
-
-
- |
-
-{% endblock %}
diff --git a/authentik/stages/email/tests/__init__.py b/authentik/stages/email/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/stages/email/tests/test_sending.py b/authentik/stages/email/tests/test_sending.py
new file mode 100644
index 000000000..b97a8ce7c
--- /dev/null
+++ b/authentik/stages/email/tests/test_sending.py
@@ -0,0 +1,83 @@
+"""email tests"""
+from smtplib import SMTPException
+from unittest.mock import MagicMock, patch
+
+from django.core import mail
+from django.core.mail.backends.locmem import EmailBackend
+from django.shortcuts import reverse
+from django.test import Client, TestCase
+
+from authentik.core.models import User
+from authentik.flows.markers import StageMarker
+from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
+from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
+from authentik.flows.views import SESSION_KEY_PLAN
+from authentik.stages.email.models import EmailStage
+
+
+class TestEmailStageSending(TestCase):
+ """Email tests"""
+
+ def setUp(self):
+ super().setUp()
+ self.user = User.objects.create_user(
+ username="unittest", email="test@beryju.org"
+ )
+ self.client = Client()
+
+ self.flow = Flow.objects.create(
+ name="test-email",
+ slug="test-email",
+ designation=FlowDesignation.AUTHENTICATION,
+ )
+ self.stage = EmailStage.objects.create(
+ name="email",
+ )
+ FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
+
+ def test_pending_user(self):
+ """Test with pending user"""
+ plan = FlowPlan(
+ flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
+ )
+ plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
+ session = self.client.session
+ session[SESSION_KEY_PLAN] = plan
+ session.save()
+
+ url = reverse(
+ "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
+ )
+ with self.settings(
+ EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
+ ):
+ response = self.client.post(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, "authentik")
+
+ def test_send_error(self):
+ """Test error during sending (sending will be retried)"""
+ plan = FlowPlan(
+ flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
+ )
+ plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
+ session = self.client.session
+ session[SESSION_KEY_PLAN] = plan
+ session.save()
+
+ url = reverse(
+ "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
+ )
+ with self.settings(
+ EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
+ ):
+ with patch(
+ "django.core.mail.backends.locmem.EmailBackend.send_messages",
+ MagicMock(side_effect=[SMTPException, EmailBackend.send_messages]),
+ ):
+ response = self.client.post(url)
+ response = self.client.post(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertTrue(len(mail.outbox) >= 1)
+ self.assertEqual(mail.outbox[0].subject, "authentik")
diff --git a/authentik/stages/email/tests.py b/authentik/stages/email/tests/test_stage.py
similarity index 93%
rename from authentik/stages/email/tests.py
rename to authentik/stages/email/tests/test_stage.py
index c71a0b2d5..0a84ecf7b 100644
--- a/authentik/stages/email/tests.py
+++ b/authentik/stages/email/tests/test_stage.py
@@ -87,6 +87,14 @@ class TestEmailStage(TestCase):
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
+ def test_use_global_settings(self):
+ """Test use_global_settings"""
+ host = "some-unique-string"
+ with self.settings(
+ EMAIL_HOST=host, EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
+ ):
+ self.assertEqual(EmailStage(use_global_settings=True).backend.host, host)
+
def test_token(self):
"""Test with token"""
# Make sure token exists
diff --git a/authentik/stages/email/tests/test_templates.py b/authentik/stages/email/tests/test_templates.py
new file mode 100644
index 000000000..3c2c06c93
--- /dev/null
+++ b/authentik/stages/email/tests/test_templates.py
@@ -0,0 +1,28 @@
+"""email tests"""
+from os import unlink
+from pathlib import Path
+from tempfile import gettempdir, mkstemp
+from typing import Any
+
+from django.conf import settings
+from django.test import TestCase
+
+from authentik.stages.email.models import get_template_choices
+
+
+def get_templates_setting(temp_dir: str) -> dict[str, Any]:
+ """Patch settings TEMPLATE's dir property"""
+ templates_setting = settings.TEMPLATES
+ templates_setting[0]["DIRS"] = [temp_dir]
+ return templates_setting
+
+
+class TestEmailStageTemplates(TestCase):
+ """Email tests"""
+
+ def test_custom_template(self):
+ """Test with custom template"""
+ with self.settings(TEMPLATES=get_templates_setting(gettempdir())):
+ _, file = mkstemp(suffix=".html")
+ self.assertEqual(get_template_choices()[-1][0], Path(file).name)
+ unlink(file)
diff --git a/docker-compose.yml b/docker-compose.yml
index b6c0b4659..a3653d32a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -29,6 +29,7 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- ./media:/media
+ - ./custom-templates:/templates
ports:
- 8000
networks:
@@ -57,6 +58,7 @@ services:
volumes:
- ./backups:/backups
- /var/run/docker.sock:/var/run/docker.sock
+ - ./custom-templates:/templates
env_file:
- .env
static:
diff --git a/helm/README.md b/helm/README.md
index 5780b8026..a2df9d6cc 100644
--- a/helm/README.md
+++ b/helm/README.md
@@ -2,9 +2,9 @@
| Name | Default | Description |
|-----------------------------------|-------------------------|-------------|
-| image.name | beryju/authentik | Image used to run the authentik server and worker |
-| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
-| image.tag | 0.14.2-stable | Image tag |
+| image.name | beryju/authentik | Image used to run the authentik server and worker |
+| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
+| image.tag | 0.14.2-stable | Image tag |
| image.pullPolicy | IfNotPresent | Image Pull Policy used for all deployments |
| serverReplicas | 1 | Replicas for the Server deployment |
| workerReplicas | 1 | Replicas for the Worker deployment |
@@ -14,13 +14,21 @@
| config.errorReporting.environment | customer | Environment sent with the error reporting |
| config.errorReporting.sendPii | false | Whether to send Personally-identifiable data with the error reporting |
| config.logLevel | warning | Log level of authentik |
+| config.email.host | localhost | SMTP Host Emails are sent to |
+| config.email.port | 25 | SMTP Port Emails are sent to |
+| config.email.username | | SMTP Username |
+| config.email.password | | SMTP Password |
+| config.email.use_tls | false | Enable StartTLS |
+| config.email.use_ssl | false | Enable SSL |
+| config.email.timeout | 10 | SMTP Timeout |
+| config.email.from | authentik@localhost | Email address authentik will send from, should have a correct @domain |
| backup.accessKey | | Optionally enable S3 Backup, Access Key |
| backup.secretKey | | Optionally enable S3 Backup, Secret Key |
| backup.bucket | | Optionally enable S3 Backup, Bucket |
| backup.region | | Optionally enable S3 Backup, Region |
| backup.host | | Optionally enable S3 Backup, to custom Endpoint like minio |
| ingress.annotations | {} | Annotations for the ingress object |
-| ingress.hosts | [authentik.k8s.local] | Hosts which the ingress will match |
+| ingress.hosts | [authentik.k8s.local] | Hosts which the ingress will match |
| ingress.tls | [] | TLS Configuration, same as Ingress objects |
| install.postgresql | true | Enables/disables the packaged PostgreSQL Chart
| install.redis | true | Enables/disables the packaged Redis Chart
diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml
index 61af2bf65..6a3a3c796 100644
--- a/helm/templates/configmap.yaml
+++ b/helm/templates/configmap.yaml
@@ -8,7 +8,6 @@ data:
POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}"
{{- if .Values.backup }}
POSTGRESQL__S3_BACKUP__ACCESS_KEY: "{{ .Values.backup.accessKey }}"
- POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}"
POSTGRESQL__S3_BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
POSTGRESQL__S3_BACKUP__REGION: "{{ .Values.backup.region }}"
POSTGRESQL__S3_BACKUP__HOST: "{{ .Values.backup.host }}"
@@ -19,3 +18,10 @@ data:
ERROR_REPORTING__SEND_PII: "{{ .Values.config.errorReporting.sendPii }}"
LOG_LEVEL: "{{ .Values.config.logLevel }}"
OUTPOSTS__DOCKER_IMAGE_BASE: "{{ .Values.image.name_outposts }}"
+ EMAIL__HOST: "{{ .Values.config.email.host }}"
+ EMAIL__PORT: "{{ .Values.config.email.port }}"
+ EMAIL__USERNAM: "{{ .Values.config.email.username }}"
+ EMAIL__USE_TLS: "{{ .Values.config.email.use_tls }}"
+ EMAIL__USE_SSL: "{{ .Values.config.email.use_ssl }}"
+ EMAIL__TIMEOUT: "{{ .Values.config.email.timeout }}"
+ EMAIL__FROM: "{{ .Values.config.email.from }}"
diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml
index bbe9ff8d5..90a5d1043 100644
--- a/helm/templates/secret.yaml
+++ b/helm/templates/secret.yaml
@@ -6,7 +6,11 @@ metadata:
data:
monitoring_username: bW9uaXRvcg== # monitor in base64
{{- if .Values.config.secretKey }}
- secret_key: {{ .Values.config.secretKey | b64enc | quote }}
+ SECRET_KEY: {{ .Values.config.secretKey | b64enc | quote }}
{{- else }}
- secret_key: {{ randAlphaNum 50 | b64enc | quote}}
+ SECRET_KEY: {{ randAlphaNum 50 | b64enc | quote}}
{{- end }}
+ {{- if .Values.backup }}
+ POSTGRESQL__S3_BACKUP__SECRET_KEY: "{{ .Values.backup.secretKey }}"
+ {{- end}}
+ EMAIL__PASSWOR: "{{ .Values.config.email.password }}"
diff --git a/helm/templates/web-deployment.yaml b/helm/templates/web-deployment.yaml
index 179be6251..5a2fb3268 100644
--- a/helm/templates/web-deployment.yaml
+++ b/helm/templates/web-deployment.yaml
@@ -51,12 +51,10 @@ spec:
- configMapRef:
name: {{ include "authentik.fullname" . }}-config
prefix: AUTHENTIK_
+ - secretRef:
+ name: {{ include "authentik.fullname" . }}-secret-key
+ prefix: AUTHENTIK_
env:
- - name: AUTHENTIK_SECRET_KEY
- valueFrom:
- secretKeyRef:
- name: {{ include "authentik.fullname" . }}-secret-key
- key: secret_key
- name: AUTHENTIK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
diff --git a/helm/templates/worker-deployment.yaml b/helm/templates/worker-deployment.yaml
index af084295b..60fb58f1f 100644
--- a/helm/templates/worker-deployment.yaml
+++ b/helm/templates/worker-deployment.yaml
@@ -54,12 +54,10 @@ spec:
- configMapRef:
name: "{{ include "authentik.fullname" . }}-config"
prefix: "AUTHENTIK_"
+ - secretRef:
+ name: {{ include "authentik.fullname" . }}-secret-key
+ prefix: AUTHENTIK_
env:
- - name: AUTHENTIK_SECRET_KEY
- valueFrom:
- secretKeyRef:
- name: "{{ include "authentik.fullname" . }}-secret-key"
- key: secret_key
- name: AUTHENTIK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
diff --git a/helm/values.yaml b/helm/values.yaml
index be9aab158..d66d2ef38 100644
--- a/helm/values.yaml
+++ b/helm/values.yaml
@@ -25,6 +25,21 @@ config:
# Log level used by web and worker
# Can be either debug, info, warning, error
logLevel: warning
+ # Global Email settings
+ email:
+ # SMTP Host Emails are sent to
+ host: localhost
+ port: 25
+ # Optionally authenticate
+ username: ""
+ password: ""
+ # Use StartTLS
+ useTls: false
+ # Use SSL
+ useSsl: false
+ timeout: 10
+ # Email address authentik will send from, should have a correct @domain
+ from: authentik@localhost
# Enable Database Backups to S3
# backup:
diff --git a/swagger.yaml b/swagger.yaml
index adbc2e5a2..e0305698f 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -8579,6 +8579,11 @@ definitions:
title: Name
type: string
minLength: 1
+ use_global_settings:
+ title: Use global settings
+ description: When enabled, global Email connection settings will be used and
+ connection settings below will be ignored.
+ type: boolean
host:
title: Host
type: string
@@ -8625,8 +8630,8 @@ definitions:
title: Template
type: string
enum:
- - stages/email/for_email/password_reset.html
- - stages/email/for_email/account_confirmation.html
+ - email/password_reset.html
+ - email/account_confirmation.html
IdentificationStage:
description: IdentificationStage Serializer
required:
diff --git a/website/docs/flow/stages/email/custom-template.png b/website/docs/flow/stages/email/custom-template.png
new file mode 100644
index 000000000..072bae310
Binary files /dev/null and b/website/docs/flow/stages/email/custom-template.png differ
diff --git a/website/docs/flow/stages/email/index.md b/website/docs/flow/stages/email/index.md
index 3eec6474d..8837570af 100644
--- a/website/docs/flow/stages/email/index.md
+++ b/website/docs/flow/stages/email/index.md
@@ -5,3 +5,19 @@ title: Email stage
This stage can be used for email verification. authentik's background worker will send an email using the specified connection details. When an email can't be delivered, delivery is automatically retried periodically.
![](email-recovery.png)
+
+## Custom Templates
+
+You can also use custom email templates, to use your own design or layout.
+
+Place any custom templates in the `custom-templates` Folder, which is in the same folder as your docker-compose file. Afterwards, you'll be able to select the template when creating/editing an Email stage.
+
+:::info
+This is currently only supported for docker-compose installs, and supported starting version 0.15.
+:::
+
+:::info
+If you've add the line and created a file, and can't see if, check the logs using `docker-compose logs -f worker`.
+:::
+
+![](custom-template.png)
diff --git a/website/docs/index.md b/website/docs/index.md
index 1ea964dea..0b2d63055 100755
--- a/website/docs/index.md
+++ b/website/docs/index.md
@@ -9,7 +9,7 @@ authentik is an open-source Identity Provider focused on flexibility and versati
## Installation
-See [Docker-compose](installation/docker-compose.md) or [Kubernetes](installation/kubernetes.md)
+See [Docker-compose](installation/docker-compose) or [Kubernetes](installation/kubernetes)
## Screenshots
diff --git a/website/docs/installation/docker-compose.md b/website/docs/installation/docker-compose.md
index c33f32036..87005f5f2 100644
--- a/website/docs/installation/docker-compose.md
+++ b/website/docs/installation/docker-compose.md
@@ -9,7 +9,7 @@ This installation method is for test-setups and small-scale productive setups.
- docker
- docker-compose
-## Install
+## Preparation
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/authentik/master/docker-compose.yml). Place it in a directory of your choice.
@@ -25,6 +25,30 @@ echo "PG_PASS=$(pwgen 40 1)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(pwgen 50 1)" >> .env
```
+## Email configuration (optional, but recommended)
+
+It is also recommended to configure global email credentials. These are used by authentik to notify you about alerts, configuration issues. They can also be used by [Email stages](flow/stages/email/index.md) to send verification/recovery emails.
+
+Append this block to your `.env` file
+
+```
+# SMTP Host Emails are sent to
+AUTHENTIK_EMAIL__HOST=localhost
+AUTHENTIK_EMAIL__PORT=25
+# Optionally authenticate
+AUTHENTIK_EMAIL__USERNAME=""
+AUTHENTIK_EMAIL__PASSWORD=""
+# Use StartTLS
+AUTHENTIK_EMAIL__USE_TLS=false
+# Use SSL
+AUTHENTIK_EMAIL__USE_SSL=false
+AUTHENTIK_EMAIL__TIMEOUT=10
+# Email address authentik will send from, should have a correct @domain
+AUTHENTIK_EMAIL__FROM=authentik@localhost
+```
+
+## Startup
+
Afterwards, run these commands to finish
```
@@ -39,8 +63,6 @@ If you plan to use this setup for production, it is also advised to change the P
Now you can pull the Docker images needed by running `docker-compose pull`. After this has finished, run `docker-compose up -d` to start authentik.
-authentik will then be reachable via HTTP on port 80, and HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
-
-If you plan to access authentik via a reverse proxy which does SSL Termination, make sure you use the HTTPS port, so authentik is aware of the SSL connection.
+authentik will then be reachable HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
The initial setup process also creates a default admin user, the username and password for which is `akadmin`. It is highly recommended to change this password as soon as you log in.
diff --git a/website/docs/installation/kubernetes.md b/website/docs/installation/kubernetes.md
index 192e59ec8..79722fc04 100644
--- a/website/docs/installation/kubernetes.md
+++ b/website/docs/installation/kubernetes.md
@@ -14,6 +14,8 @@ helm install authentik/authentik --devel -f values.yaml
This installation automatically applies database migrations on startup. After the installation is done, you can use `akadmin` as username and password.
+It is also recommended to configure global email credentials. These are used by authentik to notify you about alerts, configuration issues. They can also be used by [Email stages](flow/stages/email/index.md) to send verification/recovery emails.
+
```yaml
###################################
# Values directly affecting authentik
@@ -41,6 +43,21 @@ config:
# Log level used by web and worker
# Can be either debug, info, warning, error
logLevel: warning
+ # Global Email settings
+ email:
+ # SMTP Host Emails are sent to
+ host: localhost
+ port: 25
+ # Optionally authenticate
+ username: ""
+ password: ""
+ # Use StartTLS
+ useTls: false
+ # Use SSL
+ useSsl: false
+ timeout: 10
+ # Email address authentik will send from, should have a correct @domain
+ from: authentik@localhost
# Enable Database Backups to S3
# backup:
@@ -80,6 +97,4 @@ redis:
master:
persistence:
enabled: false
- # https://stackoverflow.com/a/59189742
- disableCommands: []
```
diff --git a/website/docs/troubleshooting/emails.md b/website/docs/troubleshooting/emails.md
new file mode 100644
index 000000000..13323624e
--- /dev/null
+++ b/website/docs/troubleshooting/emails.md
@@ -0,0 +1,23 @@
+---
+title: Troubleshooting Email sending
+---
+
+To test if an email stage, or the global email settings are configured correctly, you can run the following command:
+
+````
+./manage.py test_email [-s ]
+```
+
+If you omit the `-s` parameter, the email will be sent using the global settings. Otherwise, the settings of the specified stage will be used.
+
+To run this command with docker-compose, use
+
+```
+docker-compose exec -it worker ./manage.py test_email [...]
+```
+
+To run this command with Kubernetes, use
+
+```
+kubectl exec -it authentik-worker-xxxxx -- ./manage.py test_email [...]
+```
diff --git a/website/sidebars.js b/website/sidebars.js
index 1760bf768..62a3d6f44 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -135,7 +135,10 @@ module.exports = {
{
type: "category",
label: "Troubleshooting",
- items: ["troubleshooting/access"],
+ items: [
+ "troubleshooting/access",
+ "troubleshooting/emails",
+ ],
},
{
type: "category",
diff --git a/website/static/flows/enrollment-email-verification.pbflow b/website/static/flows/enrollment-email-verification.pbflow
index 5a07481a3..b002933a1 100644
--- a/website/static/flows/enrollment-email-verification.pbflow
+++ b/website/static/flows/enrollment-email-verification.pbflow
@@ -99,7 +99,7 @@
"from_address": "system@authentik.local",
"token_expiry": 30,
"subject": "authentik",
- "template": "stages/email/for_email/account_confirmation.html"
+ "template": "email/account_confirmation.html"
}
},
{
diff --git a/website/static/flows/recovery-email-verification.pbflow b/website/static/flows/recovery-email-verification.pbflow
index 271b5591c..d6b9f65e0 100644
--- a/website/static/flows/recovery-email-verification.pbflow
+++ b/website/static/flows/recovery-email-verification.pbflow
@@ -92,7 +92,7 @@
"from_address": "system@authentik.local",
"token_expiry": 30,
"subject": "authentik",
- "template": "stages/email/for_email/password_reset.html"
+ "template": "email/password_reset.html"
}
},
{
|