(Almost) Every application needs to send an email at some point. I’ll walk through using ZURB’s Foundation for Email to send great looking emails with the SendGrid API.
Source Code
You can download the source code and follow along or fork the repository on GitHub:
Getting Started
We’ll be using TypeScript (TS) - a superset of JavaScript that includes strong typing and great tooling. We are also going to be using Node.js along with the node package manager (npm).
You will need to install Node, which comes with npm. Then you can install TypeScript:
$ sudo npm install -g typescript
Next, install the sendgrid helper library (or SDK if you will) for your project.
If you have not create a project you will need to run npm init
to get one started.
$ npm install sendgrid --save
Note that I append the --save
flag so that the dependency is saved in our package.json
file.
Goals
Before I dive in, here are the goals that I have:
- Write a service layer to easily send HTML emails using the ZURB Foundation for Emails 2 project (formerly called Ink).
- Use the
Mail
helper provided by the sendgrid library. - The exposed API should be small and easy to use. I want to send a sweet email in a couple of lines of code with little ceremony.
Foundation for Emails 2
I am going to use Foundation for Emails 2 provided by ZURB. It provides a slick way to create email templates that will work in most major email clients. In other words, it takes the pain out of formatting nice looking email templates.
I’ll be using the Sass version.
First, let’s install the foundation cli:
$ sudo npm install -g foundation-cli
Next, create a new Foundation for Emails project:
$ foundation new --framework emails
Next, start up the server via:
$ cd {PROJECT}
$ npm start
After starting the service your browser should open to http://localhost:3000. The index lists all of the pre-built email templates that come with Foundations for Email. I suggest you pick one that you like, customize it, and remove the rest.
Here is a screen shot of the template that I am using:
Note that I have some substitutions defined in my template:
- -firstName-
- -inviter-
- -groupName-
These are placeholders where the sendgrid Mail helper will substitute in values that we specify.
Finally, let’s build our production-ready templates:
$ npm run build
App Structure
Here is a snapshot of my application’s structure:
├── dist
├── email
│ └── johnoneone
│ └── dist
│ └── src
├── package.json
└── src
└── services
└── email
├── email-service.ts
├── facades
│ ├── email.ts
│ ├── production-email.ts
│ └── test-email.ts
├── factory.ts
├── send-grid.d.ts
└── templates
├── invite.ts
└── template.ts
└── test
└── services
└── email
├── email-service.ts
├── facades
│ ├── email.ts
│ ├── production-email.ts
│ └── test-email.ts
├── factory.ts
└── templates
├── invite.ts
└── template.ts
A couple of things to note:
- The root dist directory is the output directory for our TypeScript that is in the src directory. This will be executed by Node. We’ll be focusing on the source code, though.
- The root email directory contains the Foundation for Emails project johnoneone. We will reference the dist directory when adding content to our emails using a template.
- The root src directory contains our TypeScript application source. I am using the Express framework for Node.js.
- Within the src directory is a services directory, and within that I have a directory for our email service.
- The email-service.ts file is the module for our
EmailService
class. We’ll review that shortly. - The factory.ts is a simple factory that will create email templates, such as the
SendInviteEmailTemplate
class. - The facades directory contains a base
Email
class (in the email.ts module) as well as theProductionEmail
andTestEmail
classes in production-email.ts and test-email.ts. - The templates directory contains a base
Template
abstract class as well all of the templates that we will define. Right now I have a single template defined calledSendInviteEmailTemplate
.
Class Diagram
To further model my solution I created a UML class diagram. I used StarUML to create the following:
Some design notes:
- The abstract
Email
class represents an email and is a facade for the SendGrid mail helper. TheProductionEmail
class will send the email as is. TheTestEmail
class is used when testing, and will delete theMail
object and create a new one that is sent to theTO_EMAIL
address. This is to prevent sending emails to clients when testing. - The
EmailTemplate
class represents a template. This is to use the ZURB Foundation for Emails templates that are built. The plan is that each template is built using Foundation for Emails and then an associated class is created that extendsEmailTemplate
. The implementation will provide the file name for the html template as well as substituting any values in the template. - The
EmailService
exposes theEmail
facade in a single API to the rest of my application. TheEmailService
can send simple/text emails as well as work with anEmailTemplate
to populate the content and subject. - The
EmailTemplateFactory
is a singleton that has static methods for getting a template.
Implementation
Let’s send off the invite email. This is as simple as:
import { EmailTemplateFactory } from './services/email/factory';
let template = EmailTemplateFactory.invite;
template.firstName = invite.firstName;
template.inviter = `${user.firstName} ${user.lastName}`;
template.email.addTo(
invite.email,
`${invite.firstName} ${invite.lastName}`.trim(),
);
template
.send()
.then(() => {
next();
})
.catch(next);
Note that I am using this in a REST API route method for Express, which is why you see the next()
method being invoked when my promises is resolved or rejected.
It took a little bit of tweaking, but I really like this implementation.
It’s simple and easy to use.
Just get a template from the EmailTemplateFactory
, set the substitution values, the recipient and then send it.
Email
Class
The Email
class represents an email that is being sent using SendGrid’s mail helper, which in turn uses their REST API.
The Email is a facade for the SendGrid mail helper.
//import config
import { ConfigurationFactory } from "../../../config/factory";
import { IConfiguration } from "../../../config/config";
//import sendgrid
import * as SendGrid from "sendgrid";
import {
SendGridContent,
SendGridEmail,
SendGridMail,
SendGridPersonalization,
SendGridResponse,
SendGridSubstitution
} from "../send-grid";
/**
* The base class for emails.
* @class Email
*/
export abstract class Email {
//constants
public static FROM_EMAIL: string = "no-reply@johnoneone.com";
public static FROM_NAME: string = "John 1:1";
public static TO_EMAIL: string = "blove@johnoneone.com";
public static TO_NAME: string = "Brian Love";
//the config
protected configuration: IConfiguration;
//the SendGrid API
protected sendGrid: any;
//the SendGrid Mail helper
protected _mail: any;
/**
* @constructor
*/
constructor() {
//get configuration object
this.configuration = ConfigurationFactory.config();
//store the SendGrid API
this.sendGrid = SendGrid(this.configuration.sendGrid.key);
//set default from email address(es)
this.setFromString(Email.FROM_EMAIL, Email.FROM_NAME);
}
/**
* Returns the Contents array.
* @method get contents
* @return {SendGridContent[]}
*/
public get contents(): SendGridContent[] {
return this.mail.getContents();
}
/**
* Returns the from Email object.
* @return {SendGridEmail}
*/
public get from(): SendGridEmail {
return this.mail.getFrom();
}
/**
* Set the from email and name.
* @method set from
* @param {SendGridEmail} from
*/
public set from(from: SendGridEmail) {
this.mail.setFrom(from);
}
/**
* Returns the populated SendGrid.mail.Email helper object.
* @method get mail
* @return {SendGridMail}
*/
public get mail(): SendGridMail {
//return existing mail object
if (this._mail !== undefined) {
return this._mail;
}
//set mail helper
this._mail = new SendGrid.mail.Mail();
return this._mail;
}
/**
* Returns the SendGrid Personalization object.
* @method get personalization
* @return {SendGridPersonalization}
*/
public get personalization(): SendGridPersonalization {
//verify personalization exists
if (this.mail.getPersonalizations() === undefined || this.mail.getPersonalizations().length === 0) {
this.mail.addPersonalization(new SendGrid.mail.Personalization());
}
//get first personalization by default
let personalizations = this.mail.getPersonalizations();
return personalizations[0];
}
/**
* Returns the subject for this email.
* @method get subject
* @return {string}
*/
public get subject(): string {
return this.mail.getSubject();
}
/**
* Set the subject for this email.
* @method set subject
* @param {string} subject
*/
public set subject(subject: string) {
this.mail.setSubject(subject);
}
/**
* Return the substitutions.
* @method get substitution
* @return {SendGridSubstitution[]}
*/
public get substitutions(): SendGridSubstitution[] {
return this.personalization.getSubstitutions();
}
/**
* Add content to this email.
* @method addContent
* @param {SendGridContent} content
* @return {Email}
*/
public addContent(content: SendGridContent): Email {
//add content to Mail helper
this.mail.addContent(content);
return this;
}
/**
* Add content to this email from a simple string. The default type is "text/html".
* @method addContentString
* @param {string} value
* @param {string} type
* @return {Email}
*/
public addContentString(value: string, type: string = "text/html"): Email {
//build content
let content: SendGridContent = {
type: type,
value: value
};
//add content to Mail helper
this.addContent(content);
return this;
}
/**
* Add to address using simple values.
* @method addTo
* @param {string} email
* @param {string} name
* @return {Email}
*/
public addTo(email: string, name?: string): Email {
//create Email
let to = new SendGrid.mail.Email(email);
if (name !== undefined) {
to.name = name;
}
//add to Mail helper
this.personalization.addTo(to);
return this;
}
/**
* Add a substitution in the email template.
* @method addSubstitution
* @param {string} key
* @param {string} value
* @return {Email}
*/
public addSubstitution(key: string, value: string): Email {
let substition = new SendGrid.mail.Substitution(key, value);
this.personalization.addSubstitution(substition);
return this;
}
/**
* Post-send hook.
* @method postSend
* @abstract
*/
abstract post();
/**
* Pre-send hook.
* @method preSend
* @abstract
*/
abstract pre();
/**
* Send the email.
* @method send
* @abstract
*/
abstract send(): Promise<SendGridResponse>;
/**
* Set from using simple values.
* @method setFromString
* @param {string} email
* @param {string} name
* @return {Email}
*/
public setFromString(email: string, name?: string): Email {
//create Email
let from = new SendGrid.mail.Email(email);
if (name !== undefined) {
from.name = name;
}
//set from property
this.from = from;
return this;
}
}
Some notes:
- I use a
ConfigurationFactory
to store the API key for my application. You can disregard this if you prefer, and just hard code the value from SendGrid in your code. - The
FROM_EMAIL
andFROM_NAME
static strings are the default values when sending an email. You can always override these. You will want to change this to match your needs. - The
TO_EMAIL
andTO_NAME
are the default when sending emails in test mode. - The
contents
accessor returns the array ofContent
s in the email. - The
from
mutator and accessor expose the fromEmail
object. - The
mail
accessor returns the singletonSendGrid.mail.Mail
instance. - The
personalization
accessor returns thePersonalization
object. This is a helper method for adding substitutions. - The
subject
mutator and accessor expose the subject string. - The
substitutions
accesor exposes the substitutions object. Note here that I have defined this as an array ofSubstitution
objects. This does not appear to be the case and I have submitted a PR for the TypeScript definition file. - The
addContent()
method enables the developer to directly addContent
to the email. - A helper method named
addContentString
allows you to add HTML string content to the email easily. - The
addTo()
method adds a new recipient to the email. - The
addSubstitution()
method adds a key/value pair for substituting values in your email template. - The
post()
andpre()
methods are abstract and must be implemented in an extending class. - The
setFromString()
is a helper method to set the fromEmail
object.
ProductionEmail
and TestEmail
The ProductionEmail
and TestEmail
classes extend the abstract Email
class.
These classes are used as appropriate in the EmailService
.
When we are testing we do not want to ever send an email to the intended recipient.
Rather, we send the mail to the TO_EMAIL
address.
EmailTemplate
Class
The EmailTemplate
class represents an email template that is generated using ZURB’s Foundation for Emails (formerly Ink) project.
//import email
import { Email } from "../facades/email";
import { EmailService } from "../email-service";
//import file system i/o api
import { existsSync, readFileSync } from "fs";
//import sendgrid
import { SendGridContent, SendGridResponse } from "../send-grid";
/**
* An email template.
* The templates are generated by ZURB Foundation for Emails.
* @class EmailTemplate
*/
export abstract class EmailTemplate {
//the email dist path
public static DIST_PATH: string = "email/johnoneone/dist";
//the email
public email: Email;
//the email service
public emailService: EmailService;
//the email subject
abstract subject: string;
//the mime type
public type: string = "text/html";
//the contents of the template
private _contents: SendGridContent;
//the template file name
private _fileName: string;
/**
* @constructor
*/
constructor() {
//create a new instance of the EmailService
this.emailService = new EmailService();
//store a reference to the Email
this.email = this.emailService.email;
}
/**
* Returns the SendGridContent object for this template.
* @method get content
* @return {SendGridContent}
*/
public get content(): SendGridContent {
//return content if it already exists
if (this._contents !== undefined) {
return this._contents;
}
//invoke pre-content hook
this.pre();
//build template file path
var path: string = `${EmailTemplate.DIST_PATH}/${this.fileName}`;
//verify template file exists
if (!existsSync(path)) {
throw new Error(`[EmailTemplate.content] The file does not exist {path: ${path}}.`);
}
//read file
let value = readFileSync(path).toString();
//build content
let content: SendGridContent = {
type: this.type,
value: value
};
//invoke post-content hook
this.post();
return content;
}
/**
* Returns the file name in the DIST_PATH directory for this template.
* @method get fileName
* @return {string}
*/
public get fileName(): string {
return this._fileName;
}
/**
* Set the file name in the DIST_PATH directory for this template.
* The contents of this file will be used for the email content.
* @method set fileName
* @param {string} fileName
*/
public set fileName(fileName: string) {
if (!fileName.match(/\.html$/i)) {
fileName += ".html";
}
this.fileName = fileName;
}
/**
* Post-content hook.
* @method post
* @abstract
*/
abstract post();
/**
* Pre-content hook.
* @method pre
* @abstract
*/
abstract pre();
/**
* Send this email template using the EmailService.
* @method send
* @abstract
*/
public send(): Promise<SendGridResponse> {
return this.emailService.populateFromTemplate(this).send();
}
}
Let me explain the abstract EmailTemplate
class:
- First I import the necessary functions and classes. Note that I am importing the
existsSync
andreadFileSync
functions from the file system Node package. - The static variable
DIST_PATH
is the relative path to the dist directory for my Foundation for Emails build directory. - The
email
andemailService
properties store reference to theEmail
object as well as ourEmailService
. - The
subject
string property is abstract and must be defined in an extending class. - The
contents
property is also abstract and must be implemented in an extending class. - The
fileName
string property is also abstract. The extending class will instruct theEmailTemplate
on which template file we are using. - The
post()
andpre()
hooks are abstract and will be invoked before and after the content is generated from the template. Thepre()
method gives us an opportunity to wire up any substitutions before we generate the content. Thepost()
method provides us with an opportunity to clean anything up. - The
send()
method uses theEmailService
to populate anEmail
using the template and to send it.
SendInviteEmailTemplate
Class
The SendInviteEmailTemplate
class is a sample implementation of the abstract EmailTemplate
class:
//import the base email class
import { EmailTemplate } from "./template";
/**
* Invite email template.
* @class SendInviteEmailTemplate
*/
export class SendInviteEmailTemplate extends EmailTemplate {
//the first name of the person being invited
public firstName: string = "";
//the full name of the inviter
public inviter: string = "";
/**
* Returns the email subject.
* @method get subject
* @return {string}
*/
public get subject(): string {
return `${this.inviter} has invited you to John 1:1 - a social app for Christians in small groups`;
}
/**
* Returns the file name in the DIST_PATH directory for this template.
* @method get fileName
* @return {string}
*/
public get fileName(): string {
return "invite.html";
}
/**
* Post-content hook.
* @method post
*/
public post() {
//do nothing
}
/**
* Pre-content hook.
* @method pre
*/
public pre() {
//add custom substitutions
this.email.addSubstitution("-firstName-", this.firstName);
this.email.addSubstitution("-inviter-", this.inviter);
}
}
This one is a bit easier to understand (hopefully):
- The
firstName
andinviter
string properties contain the values that will be substituted into the email template. The developer using this class will set the value of these properties before sending the email. - First, I override the accessor for the
subject
property to return a custom subject for the email. - Second, I override the accessor for the
fileName
property to return the template file name. - Finally, I implement the
pre()
andpost()
hooks. Thepre()
method wires up the substitutions.
EmailService
Class
The EmailService
class enables a developer to send off an email without using a template, as well as populating an email based on a template.
//import config
import { ConfigurationFactory } from "../../config/factory";
import { IConfiguration } from "../../config/config";
//import email
import { Email } from "./facades/email";
import { TestEmail } from "./facades/test-email";
import { ProductionEmail } from "./facades/production-email";
//import template
import { EmailTemplate } from "./templates/template";
//import sendgrid
import {
SendGridContent,
SendGridMail,
SendGridResponse
} from "./send-grid";
/**
* The exposed email service.
* @class EmailService
*/
export class EmailService {
//the email
public email: Email;
/**
* @constructor
*/
constructor() {
//get configuration
let configuration = ConfigurationFactory.config();
//set email facade based on configuration
if (configuration.isProduction()) {
this.email = new ProductionEmail();
} else {
this.email = new TestEmail();
}
}
/**
* Returns the subject for this email.
* @method get subject
* @return {string}
*/
public get subject(): string {
return this.email.subject;
}
/**
* Set the subject for this email.
* @method set subject
* @param {string} subject
*/
public set subject(subject: string) {
this.email.subject = subject;
}
/**
* Add content to this email.
* @method addContent
* @param {SendGridContent} content
* @return {Email}
*/
public addContent(content: SendGridContent): EmailService {
this.email.addContent(content);
return this;
}
/**
* Add content to this email from a simple string. The default type is "text/html".
* @param {string} value
* @param {string} type
* @return {Email}
*/
public addContentString(value?: string, type: string = "text/html"): EmailService {
this.email.addContentString(value, type);
return this;
}
/**
* Add to address using simple values.
* @method addTo
* @param {string} email
* @param {string} name
* @return {Email}
*/
public addTo(email: string, name?: string): EmailService {
this.email.addTo(email, name);
return this;
}
/**
* Add a substitution in the email template.
* @method addSubstitution
* @param {string} key
* @param {string} value
* @return {Email}
*/
public addSubstitution(key: string, value: string): EmailService {
this.email.addSubstitution(key, value);
return this;
}
/**
* Populate the email using a template.
* @method populateFromTemplate
*/
public populateFromTemplate(template: EmailTemplate): EmailService {
//add content to email from template
this.email.addContent(template.content);
//set the subject from the template
this.email.subject = template.subject;
return this;
}
/**
* Send the email.
* @method send
* @abstract
*/
public send(): Promise<SendGridResponse> {
return this.email.send();
}
/**
* Set from using simple values.
* @method setFromString
* @param {string} email
* @param {string} name
* @return {EmailService}
*/
public setFromString(email: string, name?: string): EmailService {
this.email.setFromString(email, name);
return this;
}
}
Some notes:
- Again, I am using a configuration object for determining if my application is running in production or test mode. This is based on an environment variable.
- The
email
property is either aProductionEmail
or aTestEmail
object. This is created in myconstructor()
function. - Several of the properties and methods that are in the
Email
class are exposed in theEmailService
, includingsubject
,addTo()
,addSubstitution()
andsetFromString()
. - The
populateFromTemplate()
method accepts anEmailTemplate
object and populate theEmail
content and subject from it. - The
send()
method invokes the mail helper’ssend()
method, returning thePromise
object.
Tests
I wrote tests using mocha, chai and mocha-typescript. You can run the test via:
$ npm test
Conclusion
When building this I had to duplicate a lot of the SendGrid interfaces from the node_modules/sendgrid/index.d.ts definition file as most of them are not publicly accessible. I created a send-grid.d.ts file that includes all of the definitions that I need.
As always, please feel free to contribute or to comment your suggestions!
Download
You can download the source code or fork the repository on GitHub: