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.