We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
sending recovery email fails -- `Object of type MIMEImage is not JSON serializable`
**Describe the bug**
Fails both from the admin UI using the `Send Recovery Link` button or via the `Forgot password` self-service recovery flow.

**To Reproduce**

1. Attempt to send a recovery email
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
From the admin UI:
![image](https://github.com/goauthentik/authentik/assets/200575/f1078e90-5704-4911-9b8b-363940dee057)
Attempting to do it using recovery flow:
![image](https://github.com/goauthentik/authentik/assets/200575/d93632b2-e005-4fe0-b35e-8b8a17fc76fc)

**Logs**

<details>
    <summary>Stacktrace from authentik</summary>

```
Traceback (most recent call last):
  File "/authentik/flows/views/executor.py", line 287, in get
    stage_response = self.current_stage_view.dispatch(request)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/views/generic/base.py", line 143, in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/authentik/stages/email/stage.py", line 142, in get
    self.send_email()
  File "/authentik/stages/email/stage.py", line 121, in send_email
    send_mails(current_stage, message)
  File "/authentik/stages/email/tasks.py", line 26, in send_mails
    promise = lazy_group()
              ^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py", line 1568, in __call__
    return self.apply_async(partial_args, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py", line 1598, in apply_async
    results = list(self._apply_tasks(tasks, producer, app, p,
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py", line 1775, in _apply_tasks
    sig.apply_async(producer=producer, add_to_parent=False,
  File "/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py", line 400, in apply_async
    return _apply(args, kwargs, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/celery/app/task.py", line 594, in apply_async
    return app.send_task(
           ^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/celery/app/base.py", line 798, in send_task
    amqp.send_task_message(P, name, message, **options)
  File "/ak-root/venv/lib/python3.11/site-packages/celery/app/amqp.py", line 517, in send_task_message
    ret = producer.publish(
          ^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/messaging.py", line 175, in publish
    body, content_type, content_encoding = self._prepare(
                                           ^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/messaging.py", line 268, in _prepare
    body) = dumps(body, serializer=serializer)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py", line 219, in dumps
    with _reraise_errors(EncodeError):
  File "/usr/local/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py", line 45, in _reraise_errors
    reraise(wrapper, wrapper(exc), sys.exc_info()[2])
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/exceptions.py", line 34, in reraise
    raise value.with_traceback(tb)
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py", line 41, in _reraise_errors
    yield
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py", line 220, in dumps
    payload = encoder(data)
              ^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/utils/json.py", line 57, in dumps
    return _dumps(s, cls=cls, **dict(default_kwargs, **kwargs))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
          ^^^^^^^^^^^
  File "/usr/local/lib/python3.11/json/encoder.py", line 200, in encode
    chunks = self.iterencode(o, _one_shot=True)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/json/encoder.py", line 258, in iterencode
    return _iterencode(o, 0)
           ^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/kombu/utils/json.py", line 45, in default
    return super().default(o)
           ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/json/encoder.py", line 180, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
kombu.exceptions.EncodeError: Object of type MIMEImage is not JSON serializable
```

**Version and Deployment (please complete the following information):**

-   authentik version: 2023.10.1
-   Deployment: docker-compose


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `authentik/stages/email/utils.py`
Content:
```
1 """email utils"""
2 from email.mime.image import MIMEImage
3 from functools import lru_cache
4 from pathlib import Path
5 
6 from django.core.mail import EmailMultiAlternatives
7 from django.template.loader import render_to_string
8 from django.utils import translation
9 
10 
11 @lru_cache()
12 def logo_data():
13     """Get logo as MIME Image for emails"""
14     path = Path("web/icons/icon_left_brand.png")
15     if not path.exists():
16         path = Path("web/dist/assets/icons/icon_left_brand.png")
17     with open(path, "rb") as _logo_file:
18         logo = MIMEImage(_logo_file.read())
19     logo.add_header("Content-ID", "logo.png")
20     return logo
21 
22 
23 class TemplateEmailMessage(EmailMultiAlternatives):
24     """Wrapper around EmailMultiAlternatives with integrated template rendering"""
25 
26     def __init__(self, template_name=None, template_context=None, language="", **kwargs):
27         with translation.override(language):
28             html_content = render_to_string(template_name, template_context)
29         super().__init__(**kwargs)
30         self.content_subtype = "html"
31         self.mixed_subtype = "related"
32         self.attach(logo_data())
33         self.attach_alternative(html_content, "text/html")
34
```

Path: `authentik/stages/email/tasks.py`
Content:
```
1 """email stage tasks"""
2 from email.utils import make_msgid
3 from smtplib import SMTPException
4 from typing import Any, Optional
5 
6 from celery import group
7 from django.core.mail import EmailMultiAlternatives
8 from django.core.mail.utils import DNS_NAME
9 from django.utils.text import slugify
10 from structlog.stdlib import get_logger
11 
12 from authentik.events.models import Event, EventAction
13 from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
14 from authentik.root.celery import CELERY_APP
15 from authentik.stages.email.models import EmailStage
16 
17 LOGGER = get_logger()
18 
19 
20 def send_mails(stage: EmailStage, *messages: list[EmailMultiAlternatives]):
21     """Wrapper to convert EmailMessage to dict and send it from worker"""
22     tasks = []
23     for message in messages:
24         tasks.append(send_mail.s(message.__dict__, stage.pk))
25     lazy_group = group(*tasks)
26     promise = lazy_group()
27     return promise
28 
29 
30 def get_email_body(email: EmailMultiAlternatives) -> str:
31     """Get the email's body. Will return HTML alt if set, otherwise plain text body"""
32     for alt_content, alt_type in email.alternatives:
33         if alt_type == "text/html":
34             return alt_content
35     return email.body
36 
37 
38 @CELERY_APP.task(
39     bind=True,
40     autoretry_for=(
41         SMTPException,
42         ConnectionError,
43         OSError,
44     ),
45     retry_backoff=True,
46     base=MonitoredTask,
47 )
48 def send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Optional[int] = None):
49     """Send Email for Email Stage. Retries are scheduled automatically."""
50     self.save_on_success = False
51     message_id = make_msgid(domain=DNS_NAME)
52     self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
53     try:
54         if not email_stage_pk:
55             stage: EmailStage = EmailStage(use_global_settings=True)
56         else:
57             stages = EmailStage.objects.filter(pk=email_stage_pk)
58             if not stages.exists():
59                 self.set_status(
60                     TaskResult(
61                         TaskResultStatus.WARNING,
62                         messages=["Email stage does not exist anymore. Discarding message."],
63                     )
64                 )
65                 return
66             stage: EmailStage = stages.first()
67         try:
68             backend = stage.backend
69         except ValueError as exc:
70             LOGGER.warning("failed to get email backend", exc=exc)
71             self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
72             return
73         backend.open()
74         # Since django's EmailMessage objects are not JSON serialisable,
75         # we need to rebuild them from a dict
76         message_object = EmailMultiAlternatives()
77         for key, value in message.items():
78             setattr(message_object, key, value)
79         if not stage.use_global_settings:
80             message_object.from_email = stage.from_address
81         # Because we use the Message-ID as UID for the task, manually assign it
82         message_object.extra_headers["Message-ID"] = message_id
83 
84         LOGGER.debug("Sending mail", to=message_object.to)
85         backend.send_messages([message_object])
86         Event.new(
87             EventAction.EMAIL_SENT,
88             message=f"Email to {', '.join(message_object.to)} sent",
89             subject=message_object.subject,
90             body=get_email_body(message_object),
91             from_email=message_object.from_email,
92             to_email=message_object.to,
93         ).save()
94         self.set_status(
95             TaskResult(
96                 TaskResultStatus.SUCCESSFUL,
97                 messages=["Successfully sent Mail."],
98             )
99         )
100     except (SMTPException, ConnectionError, OSError) as exc:
101         LOGGER.debug("Error sending email, retrying...", exc=exc)
102         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
103         raise exc
104
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/authentik/stages/email/tasks.py b/authentik/stages/email/tasks.py
--- a/authentik/stages/email/tasks.py
+++ b/authentik/stages/email/tasks.py
@@ -13,6 +13,7 @@
 from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 from authentik.root.celery import CELERY_APP
 from authentik.stages.email.models import EmailStage
+from authentik.stages.email.utils import logo_data
 
 LOGGER = get_logger()
 
@@ -81,6 +82,10 @@
         # Because we use the Message-ID as UID for the task, manually assign it
         message_object.extra_headers["Message-ID"] = message_id
 
+        # Add the logo (we can't add it in the previous message since MIMEImage
+        # can't be converted to json)
+        message_object.attach(logo_data())
+
         LOGGER.debug("Sending mail", to=message_object.to)
         backend.send_messages([message_object])
         Event.new(
diff --git a/authentik/stages/email/utils.py b/authentik/stages/email/utils.py
--- a/authentik/stages/email/utils.py
+++ b/authentik/stages/email/utils.py
@@ -9,7 +9,7 @@
 
 
 @lru_cache()
-def logo_data():
+def logo_data() -> MIMEImage:
     """Get logo as MIME Image for emails"""
     path = Path("web/icons/icon_left_brand.png")
     if not path.exists():
@@ -29,5 +29,4 @@
         super().__init__(**kwargs)
         self.content_subtype = "html"
         self.mixed_subtype = "related"
-        self.attach(logo_data())
         self.attach_alternative(html_content, "text/html")

{"golden_diff": "diff --git a/authentik/stages/email/tasks.py b/authentik/stages/email/tasks.py\n--- a/authentik/stages/email/tasks.py\n+++ b/authentik/stages/email/tasks.py\n@@ -13,6 +13,7 @@\n from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus\n from authentik.root.celery import CELERY_APP\n from authentik.stages.email.models import EmailStage\n+from authentik.stages.email.utils import logo_data\n \n LOGGER = get_logger()\n \n@@ -81,6 +82,10 @@\n         # Because we use the Message-ID as UID for the task, manually assign it\n         message_object.extra_headers[\"Message-ID\"] = message_id\n \n+        # Add the logo (we can't add it in the previous message since MIMEImage\n+        # can't be converted to json)\n+        message_object.attach(logo_data())\n+\n         LOGGER.debug(\"Sending mail\", to=message_object.to)\n         backend.send_messages([message_object])\n         Event.new(\ndiff --git a/authentik/stages/email/utils.py b/authentik/stages/email/utils.py\n--- a/authentik/stages/email/utils.py\n+++ b/authentik/stages/email/utils.py\n@@ -9,7 +9,7 @@\n \n \n @lru_cache()\n-def logo_data():\n+def logo_data() -> MIMEImage:\n     \"\"\"Get logo as MIME Image for emails\"\"\"\n     path = Path(\"web/icons/icon_left_brand.png\")\n     if not path.exists():\n@@ -29,5 +29,4 @@\n         super().__init__(**kwargs)\n         self.content_subtype = \"html\"\n         self.mixed_subtype = \"related\"\n-        self.attach(logo_data())\n         self.attach_alternative(html_content, \"text/html\")\n", "issue": "sending recovery email fails -- `Object of type MIMEImage is not JSON serializable`\n**Describe the bug**\r\nFails both from the admin UI using the `Send Recovery Link` button or via the `Forgot password` self-service recovery flow.\r\n\r\n**To Reproduce**\r\n\r\n1. Attempt to send a recovery email\r\n4. See error\r\n\r\n**Expected behavior**\r\nA clear and concise description of what you expected to happen.\r\n\r\n**Screenshots**\r\nFrom the admin UI:\r\n![image](https://github.com/goauthentik/authentik/assets/200575/f1078e90-5704-4911-9b8b-363940dee057)\r\nAttempting to do it using recovery flow:\r\n![image](https://github.com/goauthentik/authentik/assets/200575/d93632b2-e005-4fe0-b35e-8b8a17fc76fc)\r\n\r\n**Logs**\r\n\r\n<details>\r\n    <summary>Stacktrace from authentik</summary>\r\n\r\n```\r\nTraceback (most recent call last):\r\n  File \"/authentik/flows/views/executor.py\", line 287, in get\r\n    stage_response = self.current_stage_view.dispatch(request)\r\n                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/views/generic/base.py\", line 143, in dispatch\r\n    return handler(request, *args, **kwargs)\r\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/authentik/stages/email/stage.py\", line 142, in get\r\n    self.send_email()\r\n  File \"/authentik/stages/email/stage.py\", line 121, in send_email\r\n    send_mails(current_stage, message)\r\n  File \"/authentik/stages/email/tasks.py\", line 26, in send_mails\r\n    promise = lazy_group()\r\n              ^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py\", line 1568, in __call__\r\n    return self.apply_async(partial_args, **options)\r\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py\", line 1598, in apply_async\r\n    results = list(self._apply_tasks(tasks, producer, app, p,\r\n              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py\", line 1775, in _apply_tasks\r\n    sig.apply_async(producer=producer, add_to_parent=False,\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/celery/canvas.py\", line 400, in apply_async\r\n    return _apply(args, kwargs, **options)\r\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/celery/app/task.py\", line 594, in apply_async\r\n    return app.send_task(\r\n           ^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/celery/app/base.py\", line 798, in send_task\r\n    amqp.send_task_message(P, name, message, **options)\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/celery/app/amqp.py\", line 517, in send_task_message\r\n    ret = producer.publish(\r\n          ^^^^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/messaging.py\", line 175, in publish\r\n    body, content_type, content_encoding = self._prepare(\r\n                                           ^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/messaging.py\", line 268, in _prepare\r\n    body) = dumps(body, serializer=serializer)\r\n            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py\", line 219, in dumps\r\n    with _reraise_errors(EncodeError):\r\n  File \"/usr/local/lib/python3.11/contextlib.py\", line 155, in __exit__\r\n    self.gen.throw(typ, value, traceback)\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py\", line 45, in _reraise_errors\r\n    reraise(wrapper, wrapper(exc), sys.exc_info()[2])\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/exceptions.py\", line 34, in reraise\r\n    raise value.with_traceback(tb)\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py\", line 41, in _reraise_errors\r\n    yield\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/serialization.py\", line 220, in dumps\r\n    payload = encoder(data)\r\n              ^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/utils/json.py\", line 57, in dumps\r\n    return _dumps(s, cls=cls, **dict(default_kwargs, **kwargs))\r\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/usr/local/lib/python3.11/json/__init__.py\", line 238, in dumps\r\n    **kw).encode(obj)\r\n          ^^^^^^^^^^^\r\n  File \"/usr/local/lib/python3.11/json/encoder.py\", line 200, in encode\r\n    chunks = self.iterencode(o, _one_shot=True)\r\n             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/usr/local/lib/python3.11/json/encoder.py\", line 258, in iterencode\r\n    return _iterencode(o, 0)\r\n           ^^^^^^^^^^^^^^^^^\r\n  File \"/ak-root/venv/lib/python3.11/site-packages/kombu/utils/json.py\", line 45, in default\r\n    return super().default(o)\r\n           ^^^^^^^^^^^^^^^^^^\r\n  File \"/usr/local/lib/python3.11/json/encoder.py\", line 180, in default\r\n    raise TypeError(f'Object of type {o.__class__.__name__} '\r\nkombu.exceptions.EncodeError: Object of type MIMEImage is not JSON serializable\r\n```\r\n\r\n**Version and Deployment (please complete the following information):**\r\n\r\n-   authentik version: 2023.10.1\r\n-   Deployment: docker-compose\r\n\n", "before_files": [{"content": "\"\"\"email utils\"\"\"\nfrom email.mime.image import MIMEImage\nfrom functools import lru_cache\nfrom pathlib import Path\n\nfrom django.core.mail import EmailMultiAlternatives\nfrom django.template.loader import render_to_string\nfrom django.utils import translation\n\n\n@lru_cache()\ndef logo_data():\n    \"\"\"Get logo as MIME Image for emails\"\"\"\n    path = Path(\"web/icons/icon_left_brand.png\")\n    if not path.exists():\n        path = Path(\"web/dist/assets/icons/icon_left_brand.png\")\n    with open(path, \"rb\") as _logo_file:\n        logo = MIMEImage(_logo_file.read())\n    logo.add_header(\"Content-ID\", \"logo.png\")\n    return logo\n\n\nclass TemplateEmailMessage(EmailMultiAlternatives):\n    \"\"\"Wrapper around EmailMultiAlternatives with integrated template rendering\"\"\"\n\n    def __init__(self, template_name=None, template_context=None, language=\"\", **kwargs):\n        with translation.override(language):\n            html_content = render_to_string(template_name, template_context)\n        super().__init__(**kwargs)\n        self.content_subtype = \"html\"\n        self.mixed_subtype = \"related\"\n        self.attach(logo_data())\n        self.attach_alternative(html_content, \"text/html\")\n", "path": "authentik/stages/email/utils.py"}, {"content": "\"\"\"email stage tasks\"\"\"\nfrom email.utils import make_msgid\nfrom smtplib import SMTPException\nfrom typing import Any, Optional\n\nfrom celery import group\nfrom django.core.mail import EmailMultiAlternatives\nfrom django.core.mail.utils import DNS_NAME\nfrom django.utils.text import slugify\nfrom structlog.stdlib import get_logger\n\nfrom authentik.events.models import Event, EventAction\nfrom authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus\nfrom authentik.root.celery import CELERY_APP\nfrom authentik.stages.email.models import EmailStage\n\nLOGGER = get_logger()\n\n\ndef send_mails(stage: EmailStage, *messages: list[EmailMultiAlternatives]):\n    \"\"\"Wrapper to convert EmailMessage to dict and send it from worker\"\"\"\n    tasks = []\n    for message in messages:\n        tasks.append(send_mail.s(message.__dict__, stage.pk))\n    lazy_group = group(*tasks)\n    promise = lazy_group()\n    return promise\n\n\ndef get_email_body(email: EmailMultiAlternatives) -> str:\n    \"\"\"Get the email's body. Will return HTML alt if set, otherwise plain text body\"\"\"\n    for alt_content, alt_type in email.alternatives:\n        if alt_type == \"text/html\":\n            return alt_content\n    return email.body\n\n\n@CELERY_APP.task(\n    bind=True,\n    autoretry_for=(\n        SMTPException,\n        ConnectionError,\n        OSError,\n    ),\n    retry_backoff=True,\n    base=MonitoredTask,\n)\ndef send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Optional[int] = None):\n    \"\"\"Send Email for Email Stage. Retries are scheduled automatically.\"\"\"\n    self.save_on_success = False\n    message_id = make_msgid(domain=DNS_NAME)\n    self.set_uid(slugify(message_id.replace(\".\", \"_\").replace(\"@\", \"_\")))\n    try:\n        if not email_stage_pk:\n            stage: EmailStage = EmailStage(use_global_settings=True)\n        else:\n            stages = EmailStage.objects.filter(pk=email_stage_pk)\n            if not stages.exists():\n                self.set_status(\n                    TaskResult(\n                        TaskResultStatus.WARNING,\n                        messages=[\"Email stage does not exist anymore. Discarding message.\"],\n                    )\n                )\n                return\n            stage: EmailStage = stages.first()\n        try:\n            backend = stage.backend\n        except ValueError as exc:\n            LOGGER.warning(\"failed to get email backend\", exc=exc)\n            self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))\n            return\n        backend.open()\n        # Since django's EmailMessage objects are not JSON serialisable,\n        # we need to rebuild them from a dict\n        message_object = EmailMultiAlternatives()\n        for key, value in message.items():\n            setattr(message_object, key, value)\n        if not stage.use_global_settings:\n            message_object.from_email = stage.from_address\n        # Because we use the Message-ID as UID for the task, manually assign it\n        message_object.extra_headers[\"Message-ID\"] = message_id\n\n        LOGGER.debug(\"Sending mail\", to=message_object.to)\n        backend.send_messages([message_object])\n        Event.new(\n            EventAction.EMAIL_SENT,\n            message=f\"Email to {', '.join(message_object.to)} sent\",\n            subject=message_object.subject,\n            body=get_email_body(message_object),\n            from_email=message_object.from_email,\n            to_email=message_object.to,\n        ).save()\n        self.set_status(\n            TaskResult(\n                TaskResultStatus.SUCCESSFUL,\n                messages=[\"Successfully sent Mail.\"],\n            )\n        )\n    except (SMTPException, ConnectionError, OSError) as exc:\n        LOGGER.debug(\"Error sending email, retrying...\", exc=exc)\n        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))\n        raise exc\n", "path": "authentik/stages/email/tasks.py"}], "after_files": [{"content": "\"\"\"email utils\"\"\"\nfrom email.mime.image import MIMEImage\nfrom functools import lru_cache\nfrom pathlib import Path\n\nfrom django.core.mail import EmailMultiAlternatives\nfrom django.template.loader import render_to_string\nfrom django.utils import translation\n\n\n@lru_cache()\ndef logo_data() -> MIMEImage:\n    \"\"\"Get logo as MIME Image for emails\"\"\"\n    path = Path(\"web/icons/icon_left_brand.png\")\n    if not path.exists():\n        path = Path(\"web/dist/assets/icons/icon_left_brand.png\")\n    with open(path, \"rb\") as _logo_file:\n        logo = MIMEImage(_logo_file.read())\n    logo.add_header(\"Content-ID\", \"logo.png\")\n    return logo\n\n\nclass TemplateEmailMessage(EmailMultiAlternatives):\n    \"\"\"Wrapper around EmailMultiAlternatives with integrated template rendering\"\"\"\n\n    def __init__(self, template_name=None, template_context=None, language=\"\", **kwargs):\n        with translation.override(language):\n            html_content = render_to_string(template_name, template_context)\n        super().__init__(**kwargs)\n        self.content_subtype = \"html\"\n        self.mixed_subtype = \"related\"\n        self.attach_alternative(html_content, \"text/html\")\n", "path": "authentik/stages/email/utils.py"}, {"content": "\"\"\"email stage tasks\"\"\"\nfrom email.utils import make_msgid\nfrom smtplib import SMTPException\nfrom typing import Any, Optional\n\nfrom celery import group\nfrom django.core.mail import EmailMultiAlternatives\nfrom django.core.mail.utils import DNS_NAME\nfrom django.utils.text import slugify\nfrom structlog.stdlib import get_logger\n\nfrom authentik.events.models import Event, EventAction\nfrom authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus\nfrom authentik.root.celery import CELERY_APP\nfrom authentik.stages.email.models import EmailStage\nfrom authentik.stages.email.utils import logo_data\n\nLOGGER = get_logger()\n\n\ndef send_mails(stage: EmailStage, *messages: list[EmailMultiAlternatives]):\n    \"\"\"Wrapper to convert EmailMessage to dict and send it from worker\"\"\"\n    tasks = []\n    for message in messages:\n        tasks.append(send_mail.s(message.__dict__, stage.pk))\n    lazy_group = group(*tasks)\n    promise = lazy_group()\n    return promise\n\n\ndef get_email_body(email: EmailMultiAlternatives) -> str:\n    \"\"\"Get the email's body. Will return HTML alt if set, otherwise plain text body\"\"\"\n    for alt_content, alt_type in email.alternatives:\n        if alt_type == \"text/html\":\n            return alt_content\n    return email.body\n\n\n@CELERY_APP.task(\n    bind=True,\n    autoretry_for=(\n        SMTPException,\n        ConnectionError,\n        OSError,\n    ),\n    retry_backoff=True,\n    base=MonitoredTask,\n)\ndef send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Optional[int] = None):\n    \"\"\"Send Email for Email Stage. Retries are scheduled automatically.\"\"\"\n    self.save_on_success = False\n    message_id = make_msgid(domain=DNS_NAME)\n    self.set_uid(slugify(message_id.replace(\".\", \"_\").replace(\"@\", \"_\")))\n    try:\n        if not email_stage_pk:\n            stage: EmailStage = EmailStage(use_global_settings=True)\n        else:\n            stages = EmailStage.objects.filter(pk=email_stage_pk)\n            if not stages.exists():\n                self.set_status(\n                    TaskResult(\n                        TaskResultStatus.WARNING,\n                        messages=[\"Email stage does not exist anymore. Discarding message.\"],\n                    )\n                )\n                return\n            stage: EmailStage = stages.first()\n        try:\n            backend = stage.backend\n        except ValueError as exc:\n            LOGGER.warning(\"failed to get email backend\", exc=exc)\n            self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))\n            return\n        backend.open()\n        # Since django's EmailMessage objects are not JSON serialisable,\n        # we need to rebuild them from a dict\n        message_object = EmailMultiAlternatives()\n        for key, value in message.items():\n            setattr(message_object, key, value)\n        if not stage.use_global_settings:\n            message_object.from_email = stage.from_address\n        # Because we use the Message-ID as UID for the task, manually assign it\n        message_object.extra_headers[\"Message-ID\"] = message_id\n\n        # Add the logo (we can't add it in the previous message since MIMEImage\n        # can't be converted to json)\n        message_object.attach(logo_data())\n\n        LOGGER.debug(\"Sending mail\", to=message_object.to)\n        backend.send_messages([message_object])\n        Event.new(\n            EventAction.EMAIL_SENT,\n            message=f\"Email to {', '.join(message_object.to)} sent\",\n            subject=message_object.subject,\n            body=get_email_body(message_object),\n            from_email=message_object.from_email,\n            to_email=message_object.to,\n        ).save()\n        self.set_status(\n            TaskResult(\n                TaskResultStatus.SUCCESSFUL,\n                messages=[\"Successfully sent Mail.\"],\n            )\n        )\n    except (SMTPException, ConnectionError, OSError) as exc:\n        LOGGER.debug(\"Error sending email, retrying...\", exc=exc)\n        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))\n        raise exc\n", "path": "authentik/stages/email/tasks.py"}]}