Add new configuration options for task emails
This change extends the functionality for sending emails in task stages. Add the following configuration options for configuring what email address to send task emails to: * `to` (`str`) - Send the email to the given arbitrary email address * `email_current_user` (`bool`) - Email the user who initiated the task New variables have also been added to the task email template context, to allow referencing other candidate email addresses in the template (e.g. "A confirmation email has been sent to `<new email address>` to confirm your email change"): * `from_address` (`str`) - The email address the email is being sent from internally. Not always the same as the `From` header, which is set to `reply_address`. * `reply_address` (`str`) - The reply-to email address added to the email header. * `email_address` (`str`) - The target email address for this email template. * `email_current_user_address` (`str | None`) - The email address of the user that created the task, if available. * `emails_action_addresses` (`dict[str, str]`) - The recipient email addresses configured on the task actions, if a recipient email address is set. The key is the action name, the value is the target email address. Multiple emails can now be sent per stage in a task by defining the `emails` (`list[dict[str, Any]]`) option, and setting configuration overrides for each individual email. Change-Id: Ifc83c42bdefcc24e343b8489a917080e6c9785dc
This commit is contained in:
parent
c2cb1dafda
commit
c99499d9df
@ -14,6 +14,9 @@
|
|||||||
|
|
||||||
from confspirator import groups
|
from confspirator import groups
|
||||||
from confspirator import fields
|
from confspirator import fields
|
||||||
|
from confspirator import types
|
||||||
|
|
||||||
|
from adjutant.common import constants
|
||||||
|
|
||||||
|
|
||||||
config_group = groups.ConfigGroup("workflow")
|
config_group = groups.ConfigGroup("workflow")
|
||||||
@ -39,23 +42,42 @@ config_group.register_child_config(
|
|||||||
|
|
||||||
def _build_default_email_group(
|
def _build_default_email_group(
|
||||||
group_name,
|
group_name,
|
||||||
email_subject,
|
subject,
|
||||||
email_from,
|
email_from,
|
||||||
|
email_to,
|
||||||
email_reply,
|
email_reply,
|
||||||
email_template,
|
template,
|
||||||
email_html_template,
|
html_template,
|
||||||
|
email_current_user,
|
||||||
|
emails,
|
||||||
):
|
):
|
||||||
email_group = groups.ConfigGroup(group_name)
|
email_group = groups.ConfigGroup(group_name)
|
||||||
email_group.register_child_config(
|
email_group.register_child_config(
|
||||||
fields.StrConfig(
|
fields.StrConfig(
|
||||||
"subject",
|
"subject",
|
||||||
help_text="Default email subject for this stage",
|
help_text="Default email subject for this stage",
|
||||||
default=email_subject,
|
default=subject,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
email_group.register_child_config(
|
email_group.register_child_config(
|
||||||
fields.StrConfig(
|
fields.StrConfig(
|
||||||
"from", help_text="Default from email for this stage", default=email_from
|
"from",
|
||||||
|
help_text="Default from email for this stage",
|
||||||
|
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
|
||||||
|
default=email_from,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
email_group.register_child_config(
|
||||||
|
fields.StrConfig(
|
||||||
|
"to",
|
||||||
|
help_text=(
|
||||||
|
"Send the email to the given email address. "
|
||||||
|
"If not set, the email will be sent to the "
|
||||||
|
"recipient email address determined by the action "
|
||||||
|
"being run."
|
||||||
|
),
|
||||||
|
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
|
||||||
|
default=email_to,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
email_group.register_child_config(
|
email_group.register_child_config(
|
||||||
@ -69,14 +91,32 @@ def _build_default_email_group(
|
|||||||
fields.StrConfig(
|
fields.StrConfig(
|
||||||
"template",
|
"template",
|
||||||
help_text="Default email template for this stage",
|
help_text="Default email template for this stage",
|
||||||
default=email_template,
|
default=template,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
email_group.register_child_config(
|
email_group.register_child_config(
|
||||||
fields.StrConfig(
|
fields.StrConfig(
|
||||||
"html_template",
|
"html_template",
|
||||||
help_text="Default email html template for this stage",
|
help_text="Default email html template for this stage",
|
||||||
default=email_html_template,
|
default=html_template,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
email_group.register_child_config(
|
||||||
|
fields.BoolConfig(
|
||||||
|
"email_current_user",
|
||||||
|
help_text="Email the user who initiated the task",
|
||||||
|
default=email_current_user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
email_group.register_child_config(
|
||||||
|
fields.ListConfig(
|
||||||
|
"emails",
|
||||||
|
item_type=types.List(item_type=types.Dict()),
|
||||||
|
help_text=(
|
||||||
|
"Send more than one email, setting parameter overrides "
|
||||||
|
"for each specific email as required"
|
||||||
|
),
|
||||||
|
default=emails,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return email_group
|
return email_group
|
||||||
@ -90,31 +130,40 @@ _task_defaults_group.register_child_config(_email_defaults_group)
|
|||||||
_email_defaults_group.register_child_config(
|
_email_defaults_group.register_child_config(
|
||||||
_build_default_email_group(
|
_build_default_email_group(
|
||||||
group_name="initial",
|
group_name="initial",
|
||||||
email_subject="Task Confirmation",
|
subject="Task Confirmation",
|
||||||
email_reply="no-reply@example.com",
|
|
||||||
email_from="bounce+%(task_uuid)s@example.com",
|
email_from="bounce+%(task_uuid)s@example.com",
|
||||||
email_template="initial.txt",
|
email_to=None,
|
||||||
email_html_template=None,
|
email_reply="no-reply@example.com",
|
||||||
|
template="initial.txt",
|
||||||
|
html_template=None,
|
||||||
|
email_current_user=False,
|
||||||
|
emails=[],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_email_defaults_group.register_child_config(
|
_email_defaults_group.register_child_config(
|
||||||
_build_default_email_group(
|
_build_default_email_group(
|
||||||
group_name="token",
|
group_name="token",
|
||||||
email_subject="Task Token",
|
subject="Task Token",
|
||||||
email_reply="no-reply@example.com",
|
|
||||||
email_from="bounce+%(task_uuid)s@example.com",
|
email_from="bounce+%(task_uuid)s@example.com",
|
||||||
email_template="token.txt",
|
email_to=None,
|
||||||
email_html_template=None,
|
email_reply="no-reply@example.com",
|
||||||
|
template="token.txt",
|
||||||
|
html_template=None,
|
||||||
|
email_current_user=False,
|
||||||
|
emails=[],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_email_defaults_group.register_child_config(
|
_email_defaults_group.register_child_config(
|
||||||
_build_default_email_group(
|
_build_default_email_group(
|
||||||
group_name="completed",
|
group_name="completed",
|
||||||
email_subject="Task Completed",
|
subject="Task Completed",
|
||||||
email_reply="no-reply@example.com",
|
|
||||||
email_from="bounce+%(task_uuid)s@example.com",
|
email_from="bounce+%(task_uuid)s@example.com",
|
||||||
email_template="completed.txt",
|
email_to=None,
|
||||||
email_html_template=None,
|
email_reply="no-reply@example.com",
|
||||||
|
template="completed.txt",
|
||||||
|
html_template=None,
|
||||||
|
email_current_user=False,
|
||||||
|
emails=[],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ from django.template import loader
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from adjutant.api.models import Token
|
from adjutant.api.models import Token
|
||||||
|
from adjutant.common import user_store
|
||||||
from adjutant.notifications.utils import create_notification
|
from adjutant.notifications.utils import create_notification
|
||||||
from adjutant.config import CONF
|
from adjutant.config import CONF
|
||||||
from adjutant import exceptions
|
from adjutant import exceptions
|
||||||
@ -58,27 +59,109 @@ def create_token(task, expiry_time=None):
|
|||||||
|
|
||||||
|
|
||||||
def send_stage_email(task, email_conf, token=None):
|
def send_stage_email(task, email_conf, token=None):
|
||||||
|
"""Send one or more stage emails for a task using the given configuration.
|
||||||
|
|
||||||
|
This also accepts ``None`` for ``email_conf``, in which case
|
||||||
|
no emails are sent.
|
||||||
|
|
||||||
|
:param task: Task to send the stage email for
|
||||||
|
:type task: Task
|
||||||
|
:param email_conf: Stage email configuration (if configured)
|
||||||
|
:type email_conf: confspirator.groups.GroupNamespace | None
|
||||||
|
:param token: Token to add to the email template, defaults to None
|
||||||
|
:type token: str | None, optional
|
||||||
|
"""
|
||||||
|
|
||||||
if not email_conf:
|
if not email_conf:
|
||||||
return
|
return
|
||||||
|
|
||||||
text_template = loader.get_template(
|
# Send one or more emails according to per-email configurations
|
||||||
email_conf["template"], using="include_etc_templates"
|
# if provided. If not, send a single email using the stage-global
|
||||||
)
|
# email configuration values.
|
||||||
html_template = email_conf["html_template"]
|
emails = email_conf["emails"] or [{}]
|
||||||
|
|
||||||
|
# For each per-email configuration, send a stage email using
|
||||||
|
# that configuration.
|
||||||
|
# We want to use the per-email configuration values if provided,
|
||||||
|
# but fall back to the stage-global email configuration value
|
||||||
|
# for any that are not.
|
||||||
|
for conf in emails:
|
||||||
|
_send_stage_email(
|
||||||
|
task=task,
|
||||||
|
token=token,
|
||||||
|
subject=conf.get("subject", email_conf["subject"]),
|
||||||
|
template=conf.get("template", email_conf["template"]),
|
||||||
|
html_template=conf.get(
|
||||||
|
"html_template",
|
||||||
|
email_conf["html_template"],
|
||||||
|
),
|
||||||
|
email_from=conf.get("from", email_conf["from"]),
|
||||||
|
email_to=conf.get("to", email_conf["to"]),
|
||||||
|
email_reply=conf.get("reply", email_conf["reply"]),
|
||||||
|
email_current_user=conf.get(
|
||||||
|
"email_current_user",
|
||||||
|
email_conf["email_current_user"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_stage_email(
|
||||||
|
task,
|
||||||
|
token,
|
||||||
|
subject,
|
||||||
|
template,
|
||||||
|
html_template,
|
||||||
|
email_from,
|
||||||
|
email_to,
|
||||||
|
email_reply,
|
||||||
|
email_current_user,
|
||||||
|
):
|
||||||
|
text_template = loader.get_template(template, using="include_etc_templates")
|
||||||
if html_template:
|
if html_template:
|
||||||
html_template = loader.get_template(
|
html_template = loader.get_template(
|
||||||
html_template, using="include_etc_templates"
|
html_template, using="include_etc_templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# find our set of emails and actions that require email
|
||||||
emails = set()
|
emails = set()
|
||||||
actions = {}
|
actions = {}
|
||||||
# find our set of emails and actions that require email
|
|
||||||
|
# Fetch all possible email addresses that can be configured.
|
||||||
|
# Even if these are not actually used as the target email,
|
||||||
|
# they are made available in the email templates to be referenced.
|
||||||
|
if CONF.identity.username_is_email and "username" in task.keystone_user:
|
||||||
|
email_current_user_address = task.keystone_user["username"]
|
||||||
|
elif "user_id" in task.keystone_user:
|
||||||
|
id_manager = user_store.IdentityManager()
|
||||||
|
user = id_manager.get_user(task.keystone_user["user_id"])
|
||||||
|
email_current_user_address = user.email if user else None
|
||||||
|
else:
|
||||||
|
email_current_user_address = None
|
||||||
|
email_action_addresses = {}
|
||||||
for action in task.actions:
|
for action in task.actions:
|
||||||
act = action.get_action()
|
act = action.get_action()
|
||||||
email = act.get_email()
|
email = act.get_email()
|
||||||
if email:
|
if email:
|
||||||
emails.add(email)
|
action_name = str(act)
|
||||||
actions[str(act)] = act
|
email_action_addresses[action_name] = email
|
||||||
|
actions[action_name] = act
|
||||||
|
|
||||||
|
if email_to:
|
||||||
|
emails.add(email_to)
|
||||||
|
elif email_current_user:
|
||||||
|
if not email_current_user_address:
|
||||||
|
notes = {
|
||||||
|
"errors": (
|
||||||
|
"Error: Unable to send update, "
|
||||||
|
"task email is configured to send to current user "
|
||||||
|
f"but no username or user ID found in task: {task.uuid}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
create_notification(task, notes, error=True)
|
||||||
|
return
|
||||||
|
emails.add(email_current_user_address)
|
||||||
|
else:
|
||||||
|
emails |= set(email_action_addresses.values())
|
||||||
|
|
||||||
if not emails:
|
if not emails:
|
||||||
return
|
return
|
||||||
@ -93,7 +176,20 @@ def send_stage_email(task, email_conf, token=None):
|
|||||||
create_notification(task, notes, error=True)
|
create_notification(task, notes, error=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
context = {"task": task, "actions": actions}
|
# from_email is the return-path and is distinct from the
|
||||||
|
# message headers
|
||||||
|
from_email = email_from % {"task_uuid": task.uuid} if email_from else email_reply
|
||||||
|
email_address = emails.pop()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"task": task,
|
||||||
|
"actions": actions,
|
||||||
|
"from_address": from_email,
|
||||||
|
"reply_address": email_reply,
|
||||||
|
"email_address": email_address,
|
||||||
|
"email_current_user_address": email_current_user_address,
|
||||||
|
"email_action_addresses": email_action_addresses,
|
||||||
|
}
|
||||||
if token:
|
if token:
|
||||||
tokenurl = CONF.workflow.horizon_url
|
tokenurl = CONF.workflow.horizon_url
|
||||||
if not tokenurl.endswith("/"):
|
if not tokenurl.endswith("/"):
|
||||||
@ -104,28 +200,20 @@ def send_stage_email(task, email_conf, token=None):
|
|||||||
try:
|
try:
|
||||||
message = text_template.render(context)
|
message = text_template.render(context)
|
||||||
|
|
||||||
# from_email is the return-path and is distinct from the
|
|
||||||
# message headers
|
|
||||||
from_email = email_conf["from"]
|
|
||||||
if not from_email:
|
|
||||||
from_email = email_conf["reply"]
|
|
||||||
elif "%(task_uuid)s" in from_email:
|
|
||||||
from_email = from_email % {"task_uuid": task.uuid}
|
|
||||||
|
|
||||||
# these are the message headers which will be visible to
|
# these are the message headers which will be visible to
|
||||||
# the email client.
|
# the email client.
|
||||||
headers = {
|
headers = {
|
||||||
"X-Adjutant-Task-UUID": task.uuid,
|
"X-Adjutant-Task-UUID": task.uuid,
|
||||||
# From needs to be set to be disctinct from return-path
|
# From needs to be set to be disctinct from return-path
|
||||||
"From": email_conf["reply"],
|
"From": email_reply,
|
||||||
"Reply-To": email_conf["reply"],
|
"Reply-To": email_reply,
|
||||||
}
|
}
|
||||||
|
|
||||||
email = EmailMultiAlternatives(
|
email = EmailMultiAlternatives(
|
||||||
email_conf["subject"],
|
subject,
|
||||||
message,
|
message,
|
||||||
from_email,
|
from_email,
|
||||||
[emails.pop()],
|
[email_address],
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added the ``to`` field to task stage email configurations, for setting
|
||||||
|
an arbitrary address to send task stage emails to.
|
||||||
|
- |
|
||||||
|
Added the ``email_current_user`` field to task stage email configurations,
|
||||||
|
for sending task stage emails to the user who initiated the task.
|
||||||
|
Set ``email_current_user`` to ``true`` to enable this behaviour.
|
||||||
|
- |
|
||||||
|
Added the ``from_address`` variable to task stage email template
|
||||||
|
contexts, allowing the address the email is being sent from internally
|
||||||
|
to be templated in task stage email bodies.
|
||||||
|
Note that this is not necessarily the same address that is set in the
|
||||||
|
``From`` header of the email. For that address, use
|
||||||
|
``reply_address`` instead.
|
||||||
|
- |
|
||||||
|
Added the ``reply_address`` variable to task stage email template
|
||||||
|
contexts, allowing the reply-to address sent to the recipient to be
|
||||||
|
templated in task stage email bodies.
|
||||||
|
- |
|
||||||
|
Added the ``email_address`` variable to task stage email template contexts,
|
||||||
|
allowing the recipient email address to be templated in task stage email
|
||||||
|
bodies.
|
||||||
|
- |
|
||||||
|
Added the ``email_current_user_address`` variable to task stage email
|
||||||
|
template contexts, which exposes the email address of the user that
|
||||||
|
initiated the task for use in task stage email templates.
|
||||||
|
Note that depending on the task being run this value may not be
|
||||||
|
available for use, in which case it will be set to ``None``.
|
||||||
|
- |
|
||||||
|
Added the ``email_action_addresses`` variable to task stage email
|
||||||
|
template contexts, which exposes a dictionary mapping task actions
|
||||||
|
to their recipient email addresses for use in task stage email templates.
|
||||||
|
Note that depending on the task being run there may not be an email
|
||||||
|
address available for certain actions, in which case the dictionary will
|
||||||
|
not store a value for those tasks. If no tasks have any recipient email
|
||||||
|
addresses, the dictionary will be empty.
|
||||||
|
- |
|
||||||
|
Multiple emails can now be sent per task stage using the new ``emails``
|
||||||
|
configuration field. To send multiple emails per task stage, define a list
|
||||||
|
of emails to be sent as ``emails``, with per-email configuration set in
|
||||||
|
the list elements. If a value is not set per-email, the value set in the
|
||||||
|
stage configuration will be used, and if that is unset, the default value
|
||||||
|
will be used.
|
Loading…
Reference in New Issue
Block a user