authentik.stages.email.tasks

email stage tasks

  1"""email stage tasks"""
  2
  3from email.utils import make_msgid
  4from typing import Any
  5
  6from django.core.mail import EmailMultiAlternatives
  7from django.core.mail.utils import DNS_NAME
  8from django.utils.text import slugify
  9from django.utils.translation import gettext_lazy as _
 10from dramatiq.actor import actor
 11from dramatiq.composition import group
 12from structlog.stdlib import get_logger
 13
 14from authentik.events.models import Event, EventAction
 15from authentik.lib.utils.reflection import class_to_path, path_to_class
 16from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
 17from authentik.stages.email.models import EmailStage
 18from authentik.stages.email.utils import logo_data
 19from authentik.tasks.middleware import CurrentTask
 20
 21LOGGER = get_logger()
 22
 23
 24def send_mails(
 25    stage: EmailStage | AuthenticatorEmailStage | None, *messages: EmailMultiAlternatives
 26):
 27    """Wrapper to convert EmailMessage to dict and send it from worker
 28
 29    Args:
 30        stage: Either an EmailStage or AuthenticatorEmailStage instance,
 31            or nothing to use global settings
 32        messages: List of email messages to send
 33    Returns:
 34        Dramatiq group promise for the email sending tasks
 35    """
 36    tasks = []
 37    # Use the class path instead of the class itself for serialization
 38    stage_class_path, stage_pk = None, None
 39    if stage:
 40        stage_class_path = class_to_path(stage.__class__)
 41        stage_pk = str(stage.pk)
 42    for message in messages:
 43        tasks.append(send_mail.message(message.__dict__, stage_class_path, stage_pk))
 44    return group(tasks).run()
 45
 46
 47def get_email_body(email: EmailMultiAlternatives) -> str:
 48    """Get the email's body. Will return HTML alt if set, otherwise plain text body"""
 49    for alt_content, alt_type in email.alternatives:
 50        if alt_type == "text/html":
 51            return alt_content
 52    return email.body
 53
 54
 55@actor(description=_("Send email."))
 56def send_mail(
 57    message: dict[Any, Any],
 58    stage_class_path: str | None = None,
 59    email_stage_pk: str | None = None,
 60):
 61    """Send Email for Email Stage. Retries are scheduled automatically."""
 62    self = CurrentTask.get_task()
 63    message_id = make_msgid(domain=DNS_NAME)
 64    self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
 65    if not stage_class_path or not email_stage_pk:
 66        stage = EmailStage(use_global_settings=True)
 67    else:
 68        stage_class = path_to_class(stage_class_path)
 69        stages = stage_class.objects.filter(pk=email_stage_pk)
 70        if not stages.exists():
 71            self.warning("Email stage does not exist anymore. Discarding message.")
 72            return
 73        stage: EmailStage | AuthenticatorEmailStage = stages.first()
 74    try:
 75        backend = stage.backend
 76    except ValueError as exc:
 77        LOGGER.warning("failed to get email backend", exc=exc)
 78        self.error(exc)
 79        return
 80    backend.open()
 81    # Since django's EmailMessage objects are not JSON serialisable,
 82    # we need to rebuild them from a dict
 83    message_object = EmailMultiAlternatives()
 84    for key, value in message.items():
 85        setattr(message_object, key, value)
 86    if not stage.use_global_settings:
 87        message_object.from_email = stage.from_address
 88    # Because we use the Message-ID as UID for the task, manually assign it
 89    message_object.extra_headers["Message-ID"] = message_id
 90
 91    # Add the logo if it is used in the email body (we can't add it in the
 92    # previous message since MIMEImage can't be converted to json)
 93    body = get_email_body(message_object)
 94    if "cid:logo" in body:
 95        message_object.attach(logo_data())
 96
 97    if (
 98        message_object.to
 99        and isinstance(message_object.to[0], str)
100        and "=?utf-8?" in message_object.to[0]
101    ):
102        message_object.to = [message_object.to[0].split("<")[-1].replace(">", "")]
103
104    LOGGER.debug("Sending mail", to=message_object.to)
105    backend.send_messages([message_object])
106    Event.new(
107        EventAction.EMAIL_SENT,
108        message=f"Email to {', '.join(message_object.to)} sent",
109        subject=message_object.subject,
110        body=get_email_body(message_object),
111        from_email=message_object.from_email,
112        to_email=message_object.to,
113    ).save()
114    self.info("Successfully sent mail.")
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
def send_mails( stage: authentik.stages.email.models.EmailStage | authentik.stages.authenticator_email.models.AuthenticatorEmailStage | None, *messages: django.core.mail.message.EmailMultiAlternatives):
25def send_mails(
26    stage: EmailStage | AuthenticatorEmailStage | None, *messages: EmailMultiAlternatives
27):
28    """Wrapper to convert EmailMessage to dict and send it from worker
29
30    Args:
31        stage: Either an EmailStage or AuthenticatorEmailStage instance,
32            or nothing to use global settings
33        messages: List of email messages to send
34    Returns:
35        Dramatiq group promise for the email sending tasks
36    """
37    tasks = []
38    # Use the class path instead of the class itself for serialization
39    stage_class_path, stage_pk = None, None
40    if stage:
41        stage_class_path = class_to_path(stage.__class__)
42        stage_pk = str(stage.pk)
43    for message in messages:
44        tasks.append(send_mail.message(message.__dict__, stage_class_path, stage_pk))
45    return group(tasks).run()

Wrapper to convert EmailMessage to dict and send it from worker

Args: stage: Either an EmailStage or AuthenticatorEmailStage instance, or nothing to use global settings messages: List of email messages to send Returns: Dramatiq group promise for the email sending tasks

def get_email_body(email: django.core.mail.message.EmailMultiAlternatives) -> str:
48def get_email_body(email: EmailMultiAlternatives) -> str:
49    """Get the email's body. Will return HTML alt if set, otherwise plain text body"""
50    for alt_content, alt_type in email.alternatives:
51        if alt_type == "text/html":
52            return alt_content
53    return email.body

Get the email's body. Will return HTML alt if set, otherwise plain text body

send_mail = Actor(<function send_mail>, queue_name='default', actor_name='send_mail')

Send Email for Email Stage. Retries are scheduled automatically.