Post

4 Things I Learned About Website Email with EJS

I have made loads of progress on emails in the past weeks, and I have so much more to learn. Putting a pause here, though, I want to share what I have found because it took me a long time to get here.

1. Three emails per email

When you create an email that will be sent from your site, you need to produce three versions: plain text, watch text, and html.

email on phone

Plain Text

Plain text is simple, it is content with no html or styling. The most exciting thing in it is the \n new line. :rofl:

Watch Text

Watch text is a little more complicated. You can include some styling, but it is very basic. Some paragraph tags and lists are about what you can manage. This is explicitly for the Apple Watch.

HTML

The pretty emails that are eye catching and styled to engage your audience. Not everyone can or chooses to recieve html email, so make sure that your message is not hidden in the images or styling of the html email.

2. Special email styling

An HTML email may sound like a regular html page, it is not. That is because there are about a bajillion different email clients and all of them treat the carefully crafted HTML that you made a little different. From what I have learned, there are three basic rules (I wrote a little about this in my post on newsletters:

  • Everything is a table
  • All styles must be inlined
  • A big ol’ 600 px table wrapper protects your content

With regard to the 600px wrapper, there is so much discussion about this, and I’m honestly not sure which email clients we still need to keep this for. You may be able to test the limits. For example, in this litmus community discussion, designers are suggesting that they are getting some emails > 900 px. However, email on acid shows more limits with an example of a three column layout common in Outlook. I’m no expert, so I’m sticking with the 600px recommendation for now.

protip: think ahead about forwarding. Forwarded HTML email messages get real ugly, real fast. If you think your users will forward your emails often, use very limited styling or add a “forward to a friend” button or link that will send a fresh new email or possibly archive a version on the web, so that they can click on “view this in your browser”.

3. Just like building a web page

While the styling is different, the considerations are the same as building a web page. At first, I thought that I needed to use a different engine because nearly all of the examples were for handlebars. I was losing my mind a little because it seemed silly for me to use yet another templating engine when I already built my site with .ejs. And it was silly, and unnecessary. I finally realized that I could just do it with .ejs and javascript.

snippets in api

// in api/routes/admin
router.get(':uid/sendupdate', adminController.send_user_update);

// in api/controllers/admin
const mongoose = require('mongoose');
const async = require('async');
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');

const keys = require('../../config/keys');
const User = require('../models/user');
const nodemailer = require('nodemailer');

// ... probably a lot of other controller things

exports.send_user_custom_news = (req, res, next) => {
    async.waterfall(
        [
            // find the user
            function(done) {
                User.findById(req.params.uid, function(err, found) {
                        if (!found) {
                            req.flash('error', 'Could not find that user');
                            return res.redirect('/admin');
                        }
                        done(err, found);
                    });
            },
            function(found, done) {
                const uname = found.name;
                var giftmsg = '';
                var watchmsg = '';
                if (found.giftevents && found.giftevents.length === 0) {
                    giftmsg =
                            "We don't see any planned gifts yet for you. Is there something coming up this summer? Perhaps we can help you with Father's Day or summer birthdays.";
                        watchmsg = '<p>' + giftmsg + '</p>';
                    }
                } else if (found.giftevents && found.giftevents.length > 0) {
                    found.giftevents.forEach(function(giftevent) {

                        giftmsg +=
                            '\n some gift data ';
                        watchmsg +=
                            '<p> some gift data </p>';
                    });
                }
                const text = `Hi, ${uname} \n ${giftmsg} \n \n You can login to your dashboard to see more details (http://www.gifts-done.com/users/login). \n We hope you are having a fantastic day. If you have questions or comments, hit us up on the socials or reply to this email!`;
                const watchtext = `<p>Hi, ${uname} </p> ${watchmsg} <p>You can login to your dashboard to see more details (http://www.gifts-done.com/users/login). </p><p> We hope you are having a fantastic day. If you have questions or comments, hit us up on the socials or reply to this email!</p>`;

                const preheader = 'Checking in to help you with better gifting';
                const transporter = nodemailer.createTransport({
                    service: keys.mailFromService,
                    auth: {
                        user: keys.mailFromUser,
                        pass: keys.mailFromPass
                    }
                });
                ejs.renderFile(
                    path.join(
                        __dirname,
                        '../..',
                        'utils',
                        'mailers',
                        'sendUserUpdate',
                        'html.ejs'
                    ),
                    { found: found, preheader: preheader },
                    function(err, data) {
                        if (err) {
                            console.log(err);
                        } else {
                            var mainOptions = {
                                from: '"Jess"' + keys.mailFrom, // sender address
                                to: found.email, // list of receivers
                                subject: 'Update from Gifts Done', // Subject line
                                text: text,
                                html: data,
                                watchHtml: watchtext
                            };
                            console.log(
                                'html data ======================>',
                                mainOptions.html
                            );
                            transporter.sendMail(mainOptions, function(
                                err,
                                info
                            ) {
                                if (err) {
                                    console.log(err);
                                } else {
                                    req.flash('success', 'Success! ');
                                    res.redirect('/admin');
                                    console.log(
                                        'Message sent: ' + info.response
                                    );
                                }
                            });
                        }
                    }
                );
            }
        ],
        function(err) {
            if (err) return next(err);
            res.redirect('/admin');
        }
    );
};

The plain text and watch text are created right there in the controller, (they could be in separate functions, but I’m trying to keep this simple), but the html has to be rendered before it is sent. Here is an example of how the html file might look:

mailers/sendUserUpdate/html.ejs

<!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" style="font-family: 'Raleway', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Update from Gifts-Done</title>


<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
  body {
    padding: 0 !important;
  }
  h1 {
    font-weight: 800 !important; margin: 20px 0 5px !important;
  }
  h2 {
    font-weight: 800 !important; margin: 20px 0 5px !important;
  }
  h3 {
    font-weight: 800 !important; margin: 20px 0 5px !important;
  }
  h4 {
    font-weight: 800 !important; margin: 20px 0 5px !important;
  }
  h1 {
    font-size: 22px !important;
  }
  h2 {
    font-size: 18px !important;
  }
  h3 {
    font-size: 16px !important;
  }
  .container {
    padding: 0 !important; width: 100% !important;
  }
  .content {
    padding: 0 !important;
  }
  .content-wrap {
    padding: 10px !important;
  }
}
</style>
</head>

<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">

<span style="color: transparent; display: none !important; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">
    <%= preheader %>
</span>

<div class="content" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">

<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; " bgcolor="#fff">
    <tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
    <td style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; background-color: #FFf; margin: 0; padding: 0;" align="center" bgcolor="#FFF" valign="top">
			<img src="https://www.gifts-done.com/images/mail/gd_newsheader.png" border="0" alt="Gifts-Done Updates" align="center" /></td></tr>
	<tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
		<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">

<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin-top: 20px;">
       <tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
            <td class="content-block" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
Hi, <%= found.name || "gifts-done user" %>
		</td>
	</tr>

    <tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
        <td class="content-block" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
            <% if(found.gifts && found.gifts.length === 0){ %>
            <p>We don't see any planned gifts yet for you.</p>
            <% } %>
            <% if(found.gifts && found.gifts.length > 0){ %>
            <ul>
            <% found.gifts.forEach(function(gift){

                <li> some gift data %></li>
                <% }) %>
                <% } %>
            </ul>
			</td>
		</tr>
<tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
    <td class="content-block" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
        <a href="http://www.gifts-done.com/users/login" class="btn-secondary" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #2196f3; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #212121; margin: 0; border-color: #212121; border-style: solid; border-width: 10px 20px;">Go to your dashboard </a>
    </td>
</tr>
<tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
        We hope you are having a fantastic day. If you have questions or comments, hit us up on the socials or reply to this email!
    </td>
</tr></table></td>
</tr></table>
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; " bgcolor="#fff">
<colgroup span="2"></colgroup>
<tr style="background-color: #fff;" height="100" width="100%">
<td style="text-align: center"><a href="https://twitter.com/gifts_done"><img src="https://www.gifts-done.com/images/mail/tw.png"/></a></td>
<td style="text-align: center"><a href="https://www.instagram.com/gifts_done/"><img src="https://www.gifts-done.com/images/mail/ig.png"/></a>
</td>
</tr>
</table>
    <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; " bgcolor="#fff"><tr style="font-family:'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; background-color: #FFf; margin: 0; padding: 0;" align="center" bgcolor="#FFF" valign="top"><img src="https://www.gifts-done.com/images/mail/gd_newsfooter.png"/></td>
        </tr></table></div>

</body>
</html>

^^ I actually split this up into several files, see the “includes”? This means that my header and footer, for example, can be reused across many different emails. Here is what it looks like in my application.

mailers/sendUserUpdate/html.ejs

<% include ../header %>
<% include ../usergreeting %>
<% include ../usergiftinfo %>
<% include ../userdashbutton %>
<% include ../socials %>
<% include ../footer %>

Where, usergreeting.ejs is for example:

mailers/usergreeting.ejs

<tr style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
    <td class="content-block" style="font-family: 'Raleway',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
	    Hi, <%= found.name || "gifts-done user" %>
    </td>
</tr>

The result is something like this (code beside email):

code beside email

Code in Atom with example email

4. Follow the Damn Law

You may have noticed that I did not have an unsubscribe in my email coded above. That is fine because my email was relational in nature and did not have to include one, but it should, just to keep recipients comfortable.

According to the CAN-SPAM ACT, you have to include your address and a way for people to unsubscribe when you send primarily commercial mail (when it is not relational or transactional).

So, how do you include unsubscribe? This is not a trivial exercise - unsubscribe will require three additional considerations.

Unsubscribe in your data

Each one of your customers/emails/whatevers needs to have an indicator for the email they want from you. For example, if you are using mongoose you can add a boolean or an enum to your user schema, like so:

api/models/user.js

var mongoose = require('mongoose');

const userSchema = mongoose.Schema({
    email: {
        type: String,
        required: true,
        unique: true,
        match: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
    },
    role: { type: String, default: "user"},
    password: { type: String },
    unsubscribe: {type: Boolean, default: false}, // HERE!
    email_enum: { type: String,
        enum: ['weekly', 'all', 'only', 'none'],
        default: 'all'}, // OR HERE
    name: { type: String, match: /[a-zA-Z ]*/ },
    join: { type: Date, default: Date.now },
    lastlogin: { type: Date, default: Date.now },
    resetPasswordToken: String,
    resetPasswordExpires: Date,
});

module.exports = mongoose.model('User', userSchema);

Unsubscribe as an API endpoint

In order to adjust your unsubscribe, you will need to code an endpoint that makes a change. Here is a simple method for adjusting the boolean example above, assuming that the link used is something like, https://yoursite.com/users/:uid/unsubscribe.

snippets in api

// in api/routes/users
router.put(':uid/unsubscribe', userController.unsubscribe_user);

// in api/controllers/users
exports.unsubscribe_user = (req, res) => {
    User.findByIdAndUpdate(req.params.uid, {unsubscribe: true}, function(
        err,
        updatedUser
    ) {
        if (err) {
            req.flash('error', err.message);
            res.redirect('back');
        } else {
            res.redirect('/unsubscribed'); // default redirect is a get request
        }
    });
};

Unsubscribe in the email

This might be the easiest part. When you create the email, you can populate an unsubscribe link with the :uid and just plop that in, like so:

<a href="https://yoursite.com/users/<%= user._id %>/unsubscribe">Unsubscribe </a>

To Be Continued

As I learn more, I hope to add new posts to add on this.

Note - it may be reasonable to start out with a ready made system like that from MailChimp (which I am just now trying out on this blog) instead of rolling your own system! <That MailChimp link is my personal one, so if you sign up to use it, I could get a reward>

Also..shout out to the Moms Can Code community for support while I worked on emails almost every day.

This post is licensed under CC BY 4.0 by the author.