Skip to content

Send request notification emails with webhooks

When you need to stay informed of the status of various deployment or pipeline requests, several options are available. One of these options is to make use of the UbiOps webhooks functionality. Webhooks allow you to send an HTTP request to a specific URL when a certain request event occurs. However, sometimes you might prefer to receive an email notification instead of just a webhook request. This how-to will guide you through the process of setting up a webhook and deployment that will send an email to a configurable list of e-mail adresses.

Deployment

A deployment will send the email notifications. The deployment will make use of an SMTP Email server to send the emails to the the configured recipients. You can therefore use a small CPU deployment for this purpose, as the deployment will not require any heavy computation. The deployment will be triggered by a webhook request, which will contain the necessary information to send the email. The deployment configuration can be observed in the Configuration section, with the corresponding code and explanation in the dropdown below.
An image containing the dataflow can be seen in the dropdown below.

Click here to see the dataflow!

Dataflow Image

Configuration

Input/Output Type Name Data Type
Input Plain - -
Output Structured status String
Deployment Code

The UbiOps deployment code provided below can be used to send an email notification to a list of recipients. The deployment is triggered by a webhook request. This webhook request is parsed inside the deployment code to extract the necessary information to send the email. The parsed webhook is then formatted into an email template and sent by making use of an SMTP server. In this example, mailgun is used as the SMTP server, but you can use any other SMTP server as well. Just make sure to update the SMPT_SERVER and SMPT_PORT environment variables accordingly, with the correct login credentials.
Furthermore, 2 distinct methods have been provided to format the email body. The first method format_mail_body_simple parses a simple HTML string, encoded inside the deployment code. The second method format_mail_body_advanced uses a Jinja2 template to render the email body. The Jinja2 template is stored in a separate file in the email-templates folder. Do note that Jinja2 needs to be present in the deployment environment to correctly work, so make sure to include it in the deployment requirements file. This folder should be in the same directory as the deployment code. A sample email template is provided in the HTML Email Templates section.
Furthermore, the deployment code makes use of the following environment variables:
- EMAIL_ADDRESS_SENDER: The email address of the sender.
- EMAIL_ADDRESS_RECIPIENT: The email address of the recipient(s). Multiple recipients can be specified by separating them with a comma.
- EMAIL_PASSWORD: The password of the email address of the sender.
- EMAIL_SUBJECT: The subject of the email.
- SMTP_SERVER: The SMTP server to use.
- SMTP_PORT: The port of the SMTP server.

The deployment file structure should look like this after following the instructions in this how-to:

.
├── deployment.py
└── email-templates/ (optional)
    ├── base.html
    └── request-notifications.html

Below are two images of succesfully sent emails. The first image is the result of using the format_mail_body_simple method, while the second image is the result of using the format_mail_body_advanced method.

Raw Email Image

image

HTML Email Image

image

Code

import json
import os
import smtplib
from email.mime.text import MIMEText
from jinja2 import Environment, FileSystemLoader


class Deployment:

    def __init__(self, base_directory, context):
        '''
        Initialization method for the deployment.
        '''
        self.email_address_sender = os.getenv('EMAIL_ADDRESS_SENDER', '[email protected]')
        # Convert (comma-separated) recipient emails to list
        self.email_address_recipient_str = os.getenv('EMAIL_ADDRESS_RECIPIENT', '[email protected]')
        self.email_address_recipient_lst = self.email_address_recipient_str.split(',')
        self.email_password = os.environ['EMAIL_PASSWORD']
        self.subject = os.getenv("EMAIL_SUBJECT", "UbiOps Notification")

        self.smpt_server = os.getenv('SMTP_SERVER', 'smtp.eu.mailgun.org')
        self.smpt_port = os.getenv('SMTP_PORT', 465)

        self.ssl_enabled = True  # You can set this based on your needs
        self.context = context

        self.server = None

    def request(self, data):
        '''        
        Method for deployment requests, called separately for each individual request.
        '''

        # Load webhook data
        try:
            webhook_body = json.loads(data)
            message_data = {"project": self.context['project'],
                            **webhook_body}
            message_body = self.format_mail_body_simple(message_data)
        except KeyError as e:
            return {"status": f"Failed to send email: Missing key {str(e)}"}

        self.authenticate_to_server()
        self.send_email(message_body)

        return {"status": "Email sent successfully"}

    def authenticate_to_server(self):
        """
        Method to authenticate to the SMTP server.
        """
        if self.ssl_enabled:
            self.server = smtplib.SMTP_SSL(self.smpt_server, self.smpt_port)
            self.server.ehlo()  # Tell the server that we are running an SMTP client
            self.server.login(self.email_address_sender, self.email_password)  # Authenticate to the SMTP server
        else:
            raise NotImplementedError("Non-SSL SMTP is not supported")

    def send_email(self, message_body):
        """
        Method to send an email using SMTP.
        """
        msg = MIMEText(message_body, 'html')
        msg['Subject'] = self.subject
        msg['From'] = self.email_address_sender
        msg['To'] = self.email_address_recipient_str
        self.server.sendmail(msg['From'], self.email_address_recipient_lst, msg.as_string())  # Send the email

        self.server.close()  # Clean up the connection
        self.server = None

    def format_mail_body_simple(self, webhook_body):
        """
        Formats the email content with HTML, filling in the template structure.
        """
        ubiops_support_link = "https://support.ubiops.com"

        return f"""
        <html>
            <body>
                <h1 style="color: #333333; font-size: 22px; font-weight: bold;">Hi,</h1>
                <p>Your request with ID <strong>{webhook_body['request_id']}</strong> to version 
                <strong>{webhook_body['deployment_version_name']}</strong> of deployment <strong>{webhook_body['deployment_name']}</strong> 
                in project <strong>{self.context['project']}</strong> has finished processing with status: 
                <strong>{webhook_body['status']}</strong>.</p>

                <hr>
                <p><strong>Event:</strong> {webhook_body['event']}</p>
                <p><strong>Deployment ID:</strong> {webhook_body['deployment_id']}</p>
                <p><strong>Deployment Name:</strong> {webhook_body['deployment_name']}</p>
                <p><strong>Deployment Version ID:</strong> {webhook_body['deployment_version_id']}</p>
                <p><strong>Deployment Version Name:</strong> {webhook_body['deployment_version_name']}</p>
                <p><strong>Request ID:</strong> {webhook_body['request_id']}</p>
                <p><strong>Status:</strong> {webhook_body['status']}</p>
                <p><strong>Result:</strong> {webhook_body.get('result', {})}</p>

                <hr>
                <p>If you have any questions, feel free to contact <a href="{ubiops_support_link}" style="color: #3869D4;">our support team</a>!</p>
                <p>The UbiOps Team</p>
            </body>
        </html>
        """

    def format_mail_body_advanced(self, webhook_body):
        env = Environment(
            loader=FileSystemLoader('email-templates'))  # 'templates' is the directory containing your template

        # Load the template file
        template = env.get_template('request-notifications.html')
        rendered_email = template.render(webhook_body)

        return rendered_email

Webhook Trigger

In order to trigger this deployment to send a notification email, we use the UbiOps webhooks feature. We will specify the endpoint URL (appended with /requests) of the email deployment as the callback URL of the webhook. The webhook itself will in turn be triggered by another deployment that we want to receive notifications about. When the webhook sends a post request to the request endpoint of the deployment, this will trigger a request to the email deployment. You can find the endpoint URL of your email deployment (version) in the overview page of that deployment (version).

Click here to see how to find the Endpoint URL of your deployment

Do note once again that the endpoint URL should be appended with /requests to trigger the deployment.

image

Webhook Configuration

A webhook can be configured to send an HTTP request for different request events of a deployment or pipeline. The webhook will directly be triggered when a pipeline/deployment request reaches this specific event. A list of all possible events can be found in the UbiOps documentation. The webhook can be configured with the UI or by making use of the UbiOps Python client library. Pictures of the configuration in the UI can be found in the Webhook Configuration UI section, while the corresponding Python code can be found in the Webhook Configuration Client Library section.

Webhook Configuration UI

Click here to see the webhook configuration UI images!

One of the ways to configure a webhook is in the UI. This Webhooks section can be found under the Monitoring tab. When creating a new webhook in this section, you will be greeted with a form where you can specify the details of the webhook. The following fields need to be filled in (as showcased in the images below):
- Name: The name of the webhook.
- Callback URL: The endpoint URL of the email deployment (appended with /requests).
- Event Trigger: The event that triggers the webhook. (Request finished in this case)
- Object Selection: The object that triggers the webhook. (v1 of deployment test-deployment in this case)
- Headers: The headers that need to be sent with the request.
- Authorization: The token that is used to authenticate the request to the email deployment.
- Content-Type: The content type of the request (text/plain in this case).
- Payload: Specify whether the result of the request should be included in the webhook request (True in this case).
- Timeout & Retry: The timeout and retry settings for the webhook (any value in this case).

Webhook UI Image 1 Webhook UI Image 2

Webhook Configuration Client Library

To correctly configure a webhook to send a request to the email deployment on the completion of a request (which can be succesful or failed), you can use the following code snippet:

import ubiops

webhook_data = ubiops.WebhookCreate(
    name='send-email-notification',
    object_type='deployment',
    object_name='<NAME OF THE DEPLOYMENT THAT YOU WANT TO BE INFORMED ON>',
    version='version_name',
    url='<EMAIL DEPLOYMENT ENDPOINT URL>/requests',
    headers=[ubiops.WebhookHeader(
        key='Authorization',
        value='<UBIOPS AUTHORIZATION TOKEN>',
        protected=True),
        ubiops.WebhookHeader(
            key='Content-Type',
            value='text/plain',  # Because input type of the deployment is unstructured.
            protected=False)
    ],
    timeout=30,
    include_result=True,
    event='deployment_request_finished',
)

You can test your workflow by sending a request to the deployment that you want to monitor. This will trigger the corresponding webhook, which sends a HTTP POST request to your new email deployment, which will send an e-mail notification with details on the event to the specified email adresses. If all went successful, you will now see an e-mail in your mailbox with the request details!

Conclusion

With the previous code snippets, you can now send email notifications from a deployment by making use of webhooks!

Appendix

HTML Email Templates

If you want to send more complex emails where the contents are not static and not viable to be hardcoded in the deployment code, you can make use of (HTML) templates. These templates can then be rendered by making use of a templating engine like Jinja2. We will provide a single template in this how-to, with the corresponding code to render this template in the deployment code with the necessary data. We provide 2 files, which result in a single email template. Put these files in a folder called email-templates in the same directory as your deployment code.

HTML Template

request-notifications.html

{%  extends "base.html" %}

{% block preheader %}Request completion notification for {{ object_type }} {{ object_name }}.{% endblock %}

{% block email_masthead %} - Request for {{ object_type }} {{ object_name }} is completed{% endblock %}

{% block email_content %}
<h1 style="margin-top: 0; color: #333333; font-size: 22px; font-weight: bold; text-align: left;" align="left">Hi,</h1>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">Your request with ID <strong>{{
    request_id }}</strong> to version {{ deployment_version_name }} of deployment {{ deployment_name }} in project {{
    project }} has finished processing and is now completed with status: <strong>{{ status }}</strong>.</p>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;"> Do you happen to have any
    further questions? Feel free to contact <a href="{{ support_link }}" style="color: #3869D4;">our support team</a>!
</p>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">The UbiOps Team</p>
{% endblock %}

{% block body_sub_content %}
<tr>
    <td style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
        <p class="f-fallback sub" style="font-size: 13px; line-height: 1.625; color: #6B6E76; margin: .4em 0 1.1875em;">
            If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
        <p class="f-fallback sub" style="font-size: 13px; line-height: 1.625; color: #6B6E76; margin: .4em 0 1.1875em;">
            {{ interface_host }}/{{ request_link }}</p>
    </td>
</tr>
{% endblock %}

base.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta name="x-apple-disable-message-reformatting"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title></title>
    <style type="text/css" rel="stylesheet" media="all">
        /* Base ------------------------------ */

        @import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap");

        body {
            width: 100% !important;
            height: 100%;
            margin: 0;
            -webkit-text-size-adjust: none;
        }

        a {
            color: #3869D4;
        }

        td {
            word-break: break-word;
        }

        .preheader {
            display: none !important;
            visibility: hidden;
            mso-hide: all;
            font-size: 1px;
            line-height: 1px;
            max-height: 0;
            max-width: 0;
            opacity: 0;
            overflow: hidden;
        }

        /* Type ------------------------------ */

        body,
        td,
        th {
            font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
        }

        h1 {
            margin-top: 0;
            color: #333333;
            font-size: 22px;
            font-weight: bold;
            text-align: left;
        }

        td,
        th {
            font-size: 16px;
        }

        p,
        ul,
        ol,
        blockquote {
            margin: .4em 0 1.1875em;
            font-size: 16px;
            line-height: 1.625;
        }

        p.sub {
            font-size: 13px;
        }

        .align-center {
            text-align: center;
        }


        body {
            background-color: #F4F4F7;
            color: #51545E;
        }

        p {
            color: #51545E;
        }

        p.sub {
            color: #6B6E76;
        }

        .email-wrapper {
            width: 100%;
            margin: 0;
            padding: 0;
            -premailer-width: 100%;
            -premailer-cellpadding: 0;
            -premailer-cellspacing: 0;
            background-color: #F4F4F7;
        }

        .email-content {
            width: 100%;
            margin: 0;
            padding: 0;
            -premailer-width: 100%;
            -premailer-cellpadding: 0;
            -premailer-cellspacing: 0;
        }

        /* Masthead ----------------------- */

        .email-masthead {
            padding: 25px 0;
            text-align: center;
            background-color: #1B2430;
        }

        .email-masthead_name {
            font-size: 16px;
            font-weight: bold;
            color: #FFFFFF;
            text-decoration: none;
            text-shadow: 0 1px 0 white;
        }

        .email-body {
            width: 100%;
            margin: 0;
            padding: 0;
            -premailer-width: 100%;
            -premailer-cellpadding: 0;
            -premailer-cellspacing: 0;
            background-color: #F4F4F7;
        }

        .email-body_inner {
            width: 570px;
            margin: 0 auto;
            padding: 0;
            -premailer-width: 570px;
            -premailer-cellpadding: 0;
            -premailer-cellspacing: 0;
            background-color: #FFFFFF;
        }

        .email-footer {
            width: 570px;
            margin: 0 auto;
            padding: 0;
            -premailer-width: 570px;
            -premailer-cellpadding: 0;
            -premailer-cellspacing: 0;
            text-align: center;
        }

        .email-footer p {
            color: #6B6E76;
        }

        .body-sub {
            margin-top: 25px;
            padding-top: 25px;
            border-top: 1px solid #EAEAEC;
        }

        .content-cell {
            padding: 35px;
        }

        p,
        ul,
        ol,
        blockquote,
        h1,
        h2,
        h3 {
            color: #FFF !important;
        }

        .email-masthead_name {
            text-shadow: none !important;
        }

        }
    </style>
    <!--[if mso]>
    <style type="text/css">
        .f-fallback {
            font-family: Arial, sans-serif;
        }
    </style>
    <![endif]-->
    <style type="text/css" rel="stylesheet" media="all">
        body {
            width: 100% !important;
            height: 100%;
            margin: 0;
            -webkit-text-size-adjust: none;
        }

        body {
            font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
        }

        body {
            background-color: #F4F4F7;
            color: #51545E;
        }
    </style>
</head>
<body style="width: 100% !important; height: 100%; -webkit-text-size-adjust: none; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; background-color: #F4F4F7; color: #51545E; margin: 0;"
      bgcolor="#F4F4F7">
{% if has_preheader %}
<span class="preheader"
      style="display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;">{% block preheader %} {% endblock %}</span>
{% endif %}
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation"
       style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #F4F4F7; margin: 0; padding: 0;"
       bgcolor="#F4F4F7">
    <tr>
        <td align="center"
            style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
            <table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation"
                   style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
                <tr>
                    <td class="email-masthead"
                        style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; text-align: center; padding: 15px 0; background-color: #1B2430"
                        align="center">
                        <a href="https://ubiops.com" class="f-fallback email-masthead_name"
                           style="color: #FFF; font-size: 16px; font-weight: bold; text-decoration: none; text-shadow: 0 1px 0 white;">
                            <span class='bigger'>UbiOps</span> {% block email_masthead %} {% endblock %}
                        </a>
                    </td>
                </tr>
                <!-- Email Body -->
                <tr>
                    <td class="email-body" width="100%" cellpadding="0" cellspacing="0"
                        style="word-break: break-word; margin: 0; padding: 0; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #F4F4F7;"
                        bgcolor="#FFFFFF">
                        <table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0"
                               role="presentation"
                               style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #FFFFFF; margin: 0 auto; padding: 0;"
                               bgcolor="#FFFFFF">
                            <!-- Body content -->
                            <tr>
                                <td class="content-cell"
                                    style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; padding: 24px;">
                                    <div class="f-fallback">

                                        {% block email_content %}
                                        {% endblock %}

                                        {% if has_body_sub %}
                                        <!-- Sub copy -->
                                        <table class="body-sub" role="presentation"
                                               style="margin-top: 25px; padding-top: 25px; border-top-width: 1px; border-top-color: #EAEAEC; border-top-style: solid;">
                                            {% block body_sub_content %}
                                            {% endblock %}
                                        </table>
                                        {% endif %}
                                    </div>
                                </td>
                            </tr>
                        </table>
                    </td>
                </tr>
                <tr>
                    <td style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
                        <table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0"
                               role="presentation"
                               style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 0 auto; padding: 0;">
                            <tr>
                                <td class="content-cell" align="center"
                                    style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; padding: 10px;">
                                    <p class="f-fallback sub align-center"
                                       style="font-size: 13px; line-height: 1.625; text-align: center; color: #6B6E76; margin: .4em 0 1.1875em;"
                                       align="center">© {{ year }} UbiOps. All rights reserved.</p>
                                    <p class="f-fallback sub align-center"
                                       style="font-size: 13px; line-height: 1.625; text-align: center; color: #6B6E76; margin: .4em 0 1.1875em;"
                                       align="center">
                                        UbiOps
                                        <br/>[email protected]
                                        <br/>Tel: +31 (0)70-7920091
                                        <br/>Wilhelmina van Pruisenweg 35
                                        <br/>2595 AN The Hague,
                                        <br/>The Netherlands
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </td>
                </tr>
            </table>
        </td>
    </tr>
</table>
</body>
</html>