SES templates with MJML

If you are a AWS SES user (AWS’ email system) you probably know that working with its JSON based templates is not a user-friendly task:

  • Text and HTML content are defined as properties of a JSON object
  • It’s a JSON file, meaning that you have to escape some characters, like " in the HTML
  • It’s quite hard to find the content to change in the HTML being stored in a single line

But still, is a quite convenient system, as hosting your own email server is quite an effort, and you want it to be reliable.

That’s why I decided to use it, despite the not so friendly interface. Together with a Lambda function triggered by SNS messages, I have managed to build a quite robust mail gateway solution for Papyro (let me know if you are interested in knowing how theses pieces fit together and I’ll write a future post about it).

But the pain of creating some nice looking emails using this system is way too much from the moment that you have to manage more than 3 or 4 email templates.

That’s when I started looking into MJML. MJML is a responsive email library that allows you to create rich emails using a syntax similar to HTML. It also offers a CLI to convert the MJML files into (minimized, and this is important) HTML.

So in order to use MJML for SES templates, two issues needed to be adressed:

  1. Generate plain text from MJML - not supported directly by MJML
  2. All generated output must be valid when expressed in a single line - because still, the output must be part of a JSON property in the SES template

For 1, and given an already minimized HTML, it’s just a matter of removing all the \n and making sure that the generated CSS does not contain any problematic comment (which by default, is the case, phew).

For 2, I decided to use html-to-text (https://www.npmjs.com/package/html-to-text) node package (specifically its CLI) and some bash magic to properly format the contents in the format that AWS expects (using \r\n for line breaks).

So this is what my solution looks like:

├── bin
│   └── upload-ses-template.sh
├── build
│   └── welcome.json
└── templates
    ├── welcome.json
    └── welcome.mjml

This bash script is driving all the process (upload-ses-template.sh):

#!/usr/bin/env bash

TEMPLATE=$1

RAW_HTML=$(mjml ./templates/$TEMPLATE.mjml --config.minify true)

HTML=$(echo $RAW_HTML | sed -e 's/"/\\\\"/g')

TEXT=$(echo $RAW_HTML | html-to-text --ignore-image --noLinkBrackets | sed -e 's/"/\\\\"/g')
TEXT=$(while IFS= read -r line; do echo "$line\\\\r\\\\n"; done <<< "$TEXT" | tr -d '\n')

mkdir -p ./build
cat ./templates/$TEMPLATE.json | sed -e "s~%html%~$HTML~" -e 's/%html%/\&/g' -e "s~%text%~$TEXT~" -e 's/%text%/\&/g' > ./build/$TEMPLATE.json

cat ./build/$TEMPLATE.json

set -x

aws ses delete-template --template-name papyro-$TEMPLATE
aws ses create-template --cli-input-json file://build/$TEMPLATE.jso

(BTW, if you know a better way of joining a multiline text using \r\n, please let me know, I did struggle quite a bit and the solution is not that nice as you may have noticed)

So given a wireframe (note the %text% and %html% placeholders) of SES template (templates/welcome.json):

{
  "Template": {
    "TemplateName": "papyro-welcome",
    "SubjectPart": "Welcome to Papyro!",
    "TextPart": "%text%",
    "HtmlPart": "%html%"
  }
}

and its MJML template (templates/welcome.mjml):

<mjml>
  <mj-head>
    <mj-title>Papyro</mj-title>
  </mj-head>

  <mj-body>
    <mj-section>
      <mj-column>
        <mj-image width="100px" src="https://app.papyro.dplabs.tech/assets/app-logo/logo.png"></mj-image>
        <mj-divider border-color="#fcba03"></mj-divider>

        <mj-text font-size="20px" color="#fcba03" font-family="helvetica">Hello {{name}}!</mj-text>

        <mj-text font-size="16px" font-family="helvetica">Welcome to Papyro!</mj-text>

        <mj-text font-size="16px" font-family="helvetica">Before starting, we need to validate your email. Click in this link to proceed:</mj-text>

        <mj-button font-size="20px" font-family="helvetica" background-color="#fcba03" href="{{link}}"> Validate email </mj-button>

        <mj-text font-size="20px" color="#fcba03" font-family="helvetica">Papyro Team.</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

the script generates a valid AWS SES template file (build/welcome.json) and uploads to SES it using aws cli:

$ ./bin/upload-ses-template.sh welcome
{
  "Template": {
    "TemplateName": "papyro-welcome",
    "SubjectPart": "Welcome to Papyro!",
    "TextPart": "Hello {{name}}!\r\nWelcome to Papyro!\r\nBefore starting, we need to validate your email. Click in this link to proceed:\r\n\r\nValidate email {{link}}\r\n\r\nPapyro Team.\r\n",
    "HtmlPart": "<!-- FILE: ./templates/welcome.mjml --> <!doctype html><html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><title>Papyro</title><!--[if !mso]><!--><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><!--<![endif]--><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style type=\"text/css\">#outlook a { padding: 0; } body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } table, td { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } p { display: block; margin: 13px 0; }</style><!--[if mso]> <xml> <o:OfficeDocumentSettings> <o:AllowPNG/> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> </xml> <![endif]--><!--[if lte mso 11]> <style type=\"text/css\"> .mj-outlook-group-fix { width:100% !important; } </style> <![endif]--><style type=\"text/css\">@media only screen and (min-width:480px) { .mj-column-per-100 { width: 100% !important; max-width: 100%; } }</style><style type=\"text/css\">@media only screen and (max-width:481px) { table.mj-full-width-mobile { width: 100% !important; } td.mj-full-width-mobile { width: auto !important; } }</style></head><body style=\"word-spacing:normal;\"><div><!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" style=\"width:600px;\" width=\"600\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]--><div style=\"margin:0px auto;max-width:600px;\"><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\"><tbody><tr><td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\"><!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]--><div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\"><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\"><tbody><tr><td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\"><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\"><tbody><tr><td style=\"width:100px;\"><img height=\"auto\" src=\"https://app.papyro.dplabs.tech/assets/app-logo/logo.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;\" width=\"100\"></td></tr></tbody></table></td></tr><tr><td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\"><p style=\"border-top:solid 4px #fcba03;font-size:1px;margin:0px auto;width:100%;\"></p><!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-top:solid 4px #fcba03;font-size:1px;margin:0px auto;width:550px;\" role=\"presentation\" width=\"550px\" ><tr><td style=\"height:0;line-height:0;\"> &nbsp; </td></tr></table><![endif]--></td></tr><tr><td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\"><div style=\"font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#fcba03;\">Hello {{name}}!</div></td></tr><tr><td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\"><div style=\"font-family:helvetica;font-size:16px;line-height:1;text-align:left;color:#000000;\">Welcome to Papyro!</div></td></tr><tr><td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\"><div style=\"font-family:helvetica;font-size:16px;line-height:1;text-align:left;color:#000000;\">Before starting, we need to validate your email. Click in this link to proceed:</div></td></tr><tr><td align=\"center\" vertical-align=\"middle\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\"><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\"><tr><td align=\"center\" bgcolor=\"#fcba03\" role=\"presentation\" style=\"border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#fcba03;\" valign=\"middle\"><a href=\"{{link}}\" style=\"display:inline-block;background:#fcba03;color:#ffffff;font-family:helvetica;font-size:20px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;\" target=\"_blank\">Validate email</a></td></tr></table></td></tr><tr><td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\"><div style=\"font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#fcba03;\">Papyro Team.</div></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>"
  }
}
+ aws ses delete-template --template-name papyro-welcome
+ aws ses create-template --cli-input-json file://build/welcome.json

resulting in an email looking like this:

Hope this could be useful to someone else. I used to hate having to create emails, but now I can easily do it and I get really nice results with little effort.