# Markdown-powered emails in Django

Programmatically sending "nice-looking" HTML emails with minimal effort is hard. This is why projects like [MJML](https://mjml.io/) exist. MJML is cool, but I think it comes with some bit of overhead, as you have to learn (and write) the markup and design the layouts (you can make use of available [templates](https://mjml.io/templates) if they fit your use-case). This is fine if you have the time, and email visual quality is a core feature of your project. However, if you don't have time, and you just want to send decent looking emails with minimal effort, there is another way.

You probably already use [Markdown](https://daringfireball.net/projects/markdown/) to write your docs, and even if you don't often write docs (which you should be doing!), your project's README is most likely written in Markdown. I have become so used to writing in Markdown that I even use the [markdown-here](https://github.com/adam-p/markdown-here) browser extension so I can write emails in Markdown right within the Gmail interface! As [John Gruber](https://en.wikipedia.org/wiki/John_Gruber) said in his [introduction to Markdown](https://daringfireball.net/projects/markdown/):

> Markdown allows you to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid XHTML (or HTML).

This is why I find it appealing — you just write, with less worries about layouts and formatting. Wouldn't it be cool if we could write our Django email templates in Markdown, and render nice looking emails without breaking a sweat? Well, we can! So let's dive in and see how we can get this done.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">TLDR; Here's a <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/engineervix/blog-post--django-markdown-powered-emails" style="pointer-events: none">GitHub repo</a> where you can see the code in action, and try it out for yourself.</div>
</div>

Suppose, in our Django project, we want to send notification emails whenever somebody registers for an event. We have a plain text email template as follows:

```plaintext
Howdy {{ user }},

This is to confirm that you have successfully registered for “{{ event.title }}”. Here are the details, for your reference:

-----------------------------
Event: {{ event.title }}
Description: {{ event.description }}
Date and Time: {{ event.date_and_time }}
Location: {{ event.location }}
-----------------------------

We look forward to seeing you there!

Cheers,

— The Project Team
```

This is a [Django template](https://docs.djangoproject.com/en/5.0/ref/templates/language/), with [context variables](https://docs.djangoproject.com/en/5.0/ref/templates/language/#variables) `user` and `event`. This template is rendered using the [`render_to_string`](https://docs.djangoproject.com/en/5.0/topics/templates/#django.template.loader.render_to_string) function in the `django.template.loader` module. Here's a custom `send_mail` function, where this is done:

```python
from typing import List, Optional

from django.conf import settings
from django.core.mail import EmailMessage
from django.template.loader import render_to_string


def send_email(
    subject: str,
    to_email_list: List[str],
    template: str,
    context: Optional[dict] = None,
) -> None:
    """
    Sends an email with the specified subject,
    to the specified list of email recipients,
    using the supplied text template with optional context.

    Args:
        subject: The subject line of the email.
        to_email_list: A list of email addresses to send the email to.
        template: The path to the text template to use.
        context: A dictionary containing values to pass to the template (optional).

    Returns:
        None
    """

    # If context is None, set it to an empty dictionary
    if context is None:
        context = {}

    # Render the text template using the provided context
    text_content = render_to_string(template, context)

    # Create the email message object
    email = EmailMessage(
        subject=subject,
        body=text_content,
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=to_email_list,
    )

    # Send the email
    email.send()
```

So, in order to send an email, we'd call our custom `send_mail` function like this:

```python
send_email(
    subject=f"Event Registration for {event.title}",
    to_email_list=[user.email],
    template="events/event_registration_notification_email.txt",
    context={"event": event, "user": user},
)
```

And the user will get a plain text email like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711794406664/8acc2e83-15ce-420a-9810-1ac8242457fa.png align="center")

Now, in order to bring Markdown into the mix, we need to make a couple of changes:

1. Write our template in Markdown
    
2. Install a Markdown parsing and rendering library
    
3. Modify our `send_mail` function to use the library above
    

So, let's get to it. Here's a slightly modified version of the template:

```plaintext
Howdy {{ user }},

This is to confirm that you have successfully registered for **{{ event.title }}**. Here are the details, for your reference:

---

- _Event_: {{ event.title }}
- _Description_: {{ event.description }}
- _Date and Time_: **{{ event.date_and_time }}**
- _Location_: {{ event.location }}

---

We look forward to seeing you there!

Cheers,

— The Project Team
```

Regarding a Markdown parsing and rendering library, the choice is entirely up to you! There are several options available, including [Python-Markdown](https://github.com/Python-Markdown/markdown) and [Python-Markdown2](https://github.com/trentm/python-markdown2), which are quite popular. These are Python implementations of John Gruber's original Perl-implemented Markdown. In my case, I wanted something that was based on [GitHub's fork of `cmark`](https://github.com/github/cmark-gfm), so I could use [GitHub Flavoured Markdown (GFM)](https://github.github.com/gfm/) out-of-the-box (without installing / configuring additional extensions), if I needed to. This led me to [`cmarkgfm`](https://github.com/theacodes/cmarkgfm) and [`pycmarkgfm`](https://github.com/zopieux/pycmarkgfm). I went with `pycmarkgfm`, which is similar to `cmarkgfm`, but with support for additional features such as [task lists](https://github.github.com/gfm/#task-list-items-extension-).

Install the library:

```bash
pip install pycmarkgfm
```

Now let's modify the `send_mail` function so we can make use of `pycmarkgfm`:

```python
from typing import List, Optional

import pycmarkgfm
from django.conf import settings
from django.core.mail import EmailMessage
from django.template.loader import render_to_string


def send_email(
    subject: str,
    to_email_list: List[str],
    template: str,
    context: Optional[dict] = None,
    md_to_html: Optional[bool] = False,
) -> None:
    """
    Sends an email with the specified subject,
    to the specified list of email recipients,
    using the supplied text template with optional context.

    Args:
        subject: The subject line of the email.
        to_email_list: A list of email addresses to send the email to.
        template: The path to the text template to use.
        context: A dictionary containing values to pass to the template (optional).
        md_to_html: Whether the template content is written in Markdown format and
        should be rendered as HTML.

    Returns:
        None
    """

    # If context is None, set it to an empty dictionary
    if context is None:
        context = {}

    # Render the text template using the provided context
    text_content = render_to_string(template, context)

    if md_to_html:
        text_content = pycmarkgfm.gfm_to_html(text_content)

    # Create the email message object
    email = EmailMessage(
        subject=subject,
        body=text_content,
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=to_email_list,
    )

    # If markdown to html is enabled, set the content type to HTML
    if md_to_html:
        email.content_subtype = "html"

    # Send the email
    email.send()
```

We have introduced a new Boolean `md_to_html` keyword argument, so that, when it's `True`:

* we apply `pycmarkgfm`'s `gfm_to_html` function to the text rendered by `django.template.loader.render_to_string`, and
    
* change the email `content_subtype` to `"html"`.
    

We could use the `EmailMultiAlternatives` class here, in order to send both text and HTML versions of our email. However, we'll keep things simple and just send the HTML version. You can [check the Django docs](https://docs.djangoproject.com/en/5.0/topics/email/#sending-alternative-content-types) for details of how to use the `EmailMultiAlternatives` class.

Here's what the email looks like

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711800689070/83f6331d-13d6-429a-a288-7c12af14ae29.png align="center")

Pretty cool, right? We could end here and call it a day! However, we can make it even better, by adding a custom stylesheet. We can replicate the GitHub Markdown style by using [Sindre Sorhus](https://sindresorhus.com/)' [github-markdown-css](https://github.com/sindresorhus/github-markdown-css).

For a web page, we can easily use an external stylesheet via

```xml
<link href="example.css" rel="stylesheet" />
```

Or we could even embed the stylesheet via the `<style>` tag.

However, with emails, things are a little bit more complicated, mainly because

* most email clients do not support linking to external stylesheets due to security and privacy concerns
    
* email client CSS support for embedded stylesheets is limited (at the time of writing this post)
    

For more information about CSS in emails, please see the following resources, which I found quite helpful:

* [https://myemma.com/blog/css-in-html-emails-what-you-need-to-know-to-get-started/](https://myemma.com/blog/css-in-html-emails-what-you-need-to-know-to-get-started/)
    
* [https://mailtrap.io/blog/email-css/](https://mailtrap.io/blog/email-css/)
    
* [https://customer.io/blog/how-to-make-css-play-nice-in-html-emails-without-breaking-everything/](https://customer.io/blog/how-to-make-css-play-nice-in-html-emails-without-breaking-everything/)
    

To ensure your emails render properly across different email clients, it is recommended to use [inline CSS](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style). That sounds like too much work! Fortunately, someone has written an excellent tool to do all this work for us. Enter [premailer](https://premailer.io/), by [Peter Bengtsson](https://www.peterbe.com/about)!

From the [project's README](https://github.com/peterbe/premailer):

> \[Premailer\] parses an HTML page, looks up `style` blocks and parses the CSS. It then uses the `lxml.html` parser to modify the DOM tree of the page accordingly.

Exactly what we need! Let's start by installing the package

```bash
pip install premailer
```

Next up, we'll need to download the [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) stylesheet and update our `send_mail` function to use `premailer`:

```python
import os
from typing import List, Optional

import pycmarkgfm
from django.conf import settings
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from premailer import transform


def send_email(
    subject: str,
    to_email_list: List[str],
    template: str,
    context: Optional[dict] = None,
    md_to_html: Optional[bool] = False,
) -> None:
    """
    Sends an email with the specified subject,
    to the specified list of email recipients,
    using the supplied text template with optional context.

    Args:
        subject: The subject line of the email.
        to_email_list: A list of email addresses to send the email to.
        template: The path to the text template to use.
        context: A dictionary containing values to pass to the template (optional).
        md_to_html: Whether the template content is written in Markdown format and
        should be rendered as HTML.

    Returns:
        None
    """

    # If context is None, set it to an empty dictionary
    if context is None:
        context = {}

    # Render the text template using the provided context
    text_content = render_to_string(template, context)

    if md_to_html:
        html = pycmarkgfm.gfm_to_html(text_content)
        with open(
            os.path.join(settings.PROJECT_DIR, "assets/css/github-markdown.min.css"),
            "r",
            encoding="utf-8",
        ) as f:
            github_markdown_css = f.read()
        # see https://github.com/sindresorhus/github-markdown-css?tab=readme-ov-file#usage
        formatted_content = f"""
        <!DOCTYPE html>
        <html>
            <head>
                <meta name="viewport" content="width=device-width, initial-scale=1">
                <style>{github_markdown_css}</style>
                <style>
                    .markdown-body {{
                        box-sizing: border-box;
                        min-width: 200px;
                        max-width: 980px;
                        margin: 0 auto;
                        padding: 45px;
                    }}
                    @media (max-width: 767px) {{
                        .markdown-body {{
                            padding: 15px;
                        }}
                    }}
                </style>
            </head>
            <body class="markdown-body">{html}</body>
        </html>
        """
        text_content = transform(formatted_content)

    # Create the email message object
    email = EmailMessage(
        subject=subject,
        body=text_content,
        from_email=settings.DEFAULT_FROM_EMAIL,
        to=to_email_list,
    )

    # If markdown to html is enabled, set the content type to HTML
    if md_to_html:
        email.content_subtype = "html"

    # Send the email
    email.send()
```

Notes:

1. **Asset Loading**: We load the [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) stylesheet from a file (`github-markdown.min.css`), and embed the stylesheet via the `<style>` tag. The file can be located anywhere (it doesn't need to be within the [`STATICFILES_DIRS`](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs) because it's not used on the frontend — we are only using it when generating emails). I minified the file using [`sass`](https://sass-lang.com/documentation/cli/) in order to reduce the file size.
    
2. **HTML Content Generation**: We create a formatted HTML string based on the example in the [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) README.
    
3. **Inlining CSS Styles**: The `premailer.transform` function is applied to the formatted HTML content to inline CSS styles.
    

This is the result:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1711997990978/689a3a96-21a5-486d-8299-bd3350a0ce29.png align="center")

Pretty sweet, right? Well, there you have it — decent-looking emails with minimal effort. After all, [simple is better than complex](https://github.com/python/peps/blob/3cdfdd90c512978ec631d8ad7b9455bcdd74f32d/peps/pep-0020.rst?plain=1#L26)!
