Generate Html Email In Python Using Flask And Jinja2

January 31, 2018

When trying to generate html email, the first thing we thought using Python string formating. Things could get messy pretty quickly when we have multiple type of emails or more complex logic like if and loop is involved.

messages = ["Reminder 1", "Reminder 2"]

html = u"""
<p style="margin-bottom: 1em;">Hi {name},</p>

<p style="margin: 0.5em 0;">You have {reminder_number} reminder(s).</p>

<p style="margin: 0.5em 0; color: #848484;">---</p>

{content}

<p style="margin: 0.5em 0; color: #848484;">---</p>

<p style="margin: 1em 0;">
    Prepared by, <br />
    {author_name}, {author_title}
    <a href="{app_url}">{app_name}</a><br />
</p>

<p style="margin-top: 1em;">
<small style="color:#8899a6;text-decoration:none;">Stop Reminder for {{ unsubscribe }}</small>
</p>
""".format(name="Desmond",
           reminder_number=len(messages),
           content=u'<p style="margin: 0.5em 0; color: #848484;">---</p>'.join(messages),
           author_name="Test Machine", author_title="Tester",
           app_name="WhatIDoNow", app_url="https://www.whatidonow.com/")

Since I am using Flask to generate HTML page, why not use Jinja2 template to generate HTML email?

Like my HTML page, I create a base HTML email Jinja2 layout at templates/layouts/email.html. This layout shall served as the base layout of all my html emails.

The layout require the parameters of TITLE, APP_NAME and APP_URL with 2 blocks which can be overwritten, which is content and footer.

NOTE: the following html email template is based on leemunroe/responsive-html-email-template

<!doctype html>
<html>
  <head>
    <meta name="viewport" content="width=device-width">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>{{ TITLE }}</title>
    <style>
    /* -------------------------------------
        INLINED WITH htmlemail.io/inline
    ------------------------------------- */
    /* -------------------------------------
        RESPONSIVE AND MOBILE FRIENDLY STYLES
    ------------------------------------- */
    @media only screen and (max-width: 620px) {
      table[class=body] h1 {
        font-size: 28px !important;
        margin-bottom: 10px !important;
      }
      table[class=body] p,
            table[class=body] ul,
            table[class=body] ol,
            table[class=body] td,
            table[class=body] span,
            table[class=body] a {
        font-size: 16px !important;
      }
      table[class=body] .wrapper,
            table[class=body] .article {
        padding: 10px !important;
      }
      table[class=body] .content {
        padding: 0 !important;
      }
      table[class=body] .container {
        padding: 0 !important;
        width: 100% !important;
      }
      table[class=body] .main {
        border-left-width: 0 !important;
        border-radius: 0 !important;
        border-right-width: 0 !important;
      }
      table[class=body] .btn table {
        width: 100% !important;
      }
      table[class=body] .btn a {
        width: 100% !important;
      }
      table[class=body] .img-responsive {
        height: auto !important;
        max-width: 100% !important;
        width: auto !important;
      }
    }
    /* -------------------------------------
        PRESERVE THESE STYLES IN THE HEAD
    ------------------------------------- */
    @media all {
      .btn-primary table td:hover {
        background-color: #34495e !important;
      }
      .btn-primary a:hover {
        background-color: #34495e !important;
        border-color: #34495e !important;
      }
    }
    </style>
  </head>
  <body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
    <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
      <tr>
        <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
        <td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
          <div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;">

            <!-- START CENTERED WHITE CONTAINER -->
            <span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
            <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">

              <!-- START MAIN CONTENT AREA -->
              <tr>
                <td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
                  <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
                    <tr>
                      <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
                        {% block content %}<!-- main content area -->{% endblock %}
                      </td>
                    </tr>
                  </table>
                </td>
              </tr>

            <!-- END MAIN CONTENT AREA -->
            </table>

            <!-- START FOOTER -->
            <div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;">
              <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
                <tr>
                  <td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
                    <span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;"><a href="{{ APP_URL }}" style="text-decoration: none; color: #999999;">{{ APP_NAME }}</a></span>
                  </td>
                </tr>
                <tr>
                  <td style="font-family: sans-serif; vertical-align: top; font-size: 12px; color: #999999; text-align: center;">
                  {% block footer %}<!-- footer content area -->{% endblock %}
                  </td>
                </tr>
              </table>
            </div>
            <!-- END FOOTER -->

          <!-- END CENTERED WHITE CONTAINER -->
          </div>
        </td>
        <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
      </tr>
    </table>
  </body>
</html>

I create a reminder email template at templates/email/reminder.html which overwrite the content and footer block of the base layout template.

{% extends "layouts/email.html" %}

{% block content %}

{% set p_style = "font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;" %}

<p style="{{ p_style }}">Hi {{ name }},</p>

<p style="{{ p_style }}">You have {{ reminder_number }} reminder(s).</p>

<p style="{{ p_style }} color: #848484;">---</p>

{% for _item in items %}
{% if _item.type == 'now' %}
<p style="{{ p_style }}">
  <span style="color: #848484; font-size: 11px;">[{{ loop.index }}]</span> Update expected on {{ _item.date }} <span style="color: #848484; font-size: 11px;">{{ _item.ago }}</span> <br />
  <span style="color: #2E2E2E; font-size: 13px;">Project: <a href="{{ _item.url }}">{{ _item.name }}</a></span><br />
  <span style="color: #2E2E2E; font-size: 13px;">Update frequency: {{ _item.frequency }} day(s)</span>
</p>
{% elif _item.type == 'challenge' %}
<p style="{{ p_style }}">
  <span style="color: #848484; font-size: 11px;">[{{ loop.index }}]</span> Update expected on {{ _item.date }} <span style="color: #848484; font-size: 11px;">{{ _item.ago }}</span><br />
  <span style="color: #2E2E2E; font-size: 13px;">Challenge: <a href="{{ _item.url }}">{{ _item.name }}</a></span>
</p>
{% else %}
[NA]
{% endif %}
{% endfor %}

<p style="{{ p_style }} color: #848484;">---</p>

<p style="{{ p_style }} font-style: italic; color: #585858;">{{ quote }}</p>

<p style="{{ p_style }}; margin-top: 30px; color: #1C1C1C;">
  Prepared with ❤ by,<br />
  {{ author_name }}<br />
  {{ author_title }}
</p>

<p style="margin-top: 1em;">
    <small style="color:#8899a6;text-decoration:none;"></small>
</p>

{% endblock %}

{% block footer %}

<a href="{{ unsubscribe_url }}" style="text-decoration: none; color: #999999;">Unsubscribe reminder.</a>

{% endfor %}

{% endblock %}

Generate the html email using the Jinja2 reminder template.

from flask import render_template

# list of objects
messages = ...

html = render_template('email/reminder.html',
                       name="Desmond",
                       reminder_number=len(messages),
                       items=messages,
                       author_name="Test Machine", author_title="Tester",
                       APP_NAME="WhatIDoNow", APP_URL="https://www.whatidonow.com/",
                       unsubscribe_url="https://www.whatidonow.com/unsubsribe/xxx",
                       TITLE="Reminder Email")
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.