Pup

v2.x

ExtrasEmail

At the most basic level, in order to send email from your application, you will need to configure your MAIL_URL environment variable. This will add support for sending email via SMTP. To perform sends, you can use the email package which is included in Pup by default (see details in Transactional email templates below)—and uses SMTP sending—or, install a third-party package if you prefer to use an email provider's API directly.

Unless you're building an app that will send marketing email (like newsletters), most of the email you're sending will be classified as transactional email.

By default, Pup is set up to automatically configure your MAIL_URL environment variable in development. This is done by pulling the private.MAIL_URL value from your /settings-development.json file in /imports/startup/server/email.js. We've chosen to limit this to development as it's likely you'll want to set environment variables in production directly via your hosting platform.

If not, removing the if (Meteor.isDevelopment) check will ensure that this is set based on the settings file you load when you start your server.

Example MAIL_URL Configuration in settings-<env>.json
{
  [...]
  "private": {
    "MAIL_URL": "smtp://username:password@hostname.com:587"
  }
}
/startup/server/email.js
import { Meteor } from 'meteor/meteor';

if (Meteor.isDevelopment) {
  if (Meteor.settings.private && Meteor.settings.private.MAIL_URL) {
    process.env.MAIL_URL = Meteor.settings.private.MAIL_URL;
  } else {
    console.warn('[Pup] Woof! Email settings are not configured. Emails will not be sent. See https://cleverbeagle.com/pup/v1/the-basics/email for configuration instructions.');
  }
}

Sending Transactional Emails

To aid in the process of sending transactional email from your application, Pup comes with a set of examples and helper functions for performing sends. By default, Pup provides both HTML and text-based templates for the following "transactions:"

  • Welcoming new users when they sign up
  • Verifying a user's email address when they sign up
  • Resetting a user's password

For each of the above, an HTML and text-based template is provided. These can be found in /private/email-templates.

These templates are designed to "snap in" to the base template located at /private/email-templates/base.html. The idea here is that you have one consistent template for all of your emails and swap in the {{{content}}} you'd like for each email. This is automated for you utilizing Pup's sendEmail method. The template name you pass to this method is assumed to be the content that will be rendered into the base template.

Handlebars support

Each of the provided example templates is written using the Handlebars templating language. Before a send is performed, compilation of each template is performed by the sendEmail() module located at /modules/server/sendEmail.js. This module is accessible throughout the server, so you can use it to send other transactional emails, too! As a bonus, all of the functions used inside of sendEmail() to perform compilations are directly accessible in the application, so you can use them directly if you prefer.

Automatic CSS inlining

To make compilation of HTML templates a bit friendlier, when using sendEmail(), any CSS styles contained in the <style></style> tag of the template's <head></head> element are automatically inlined for you.

Example Usage of sendEmail
/* eslint-disable consistent-return */

import { Meteor } from 'meteor/meteor';
import normalizeMeteorUserData from './normalizeMeteorUserData';
import sendEmail from '../../../modules/server/sendEmail';

const getEmailOptions = (user) => {
  try {
    const firstName = user.profile.name.first;
    const { productName } = Meteor.settings.public;

    return {
      to: user.emails[0].address,
      from: Meteor.settings.private.supportEmail,
      subject: `[${Meteor.settings.public.productName}] Welcome, ${firstName}!`,
      template: 'welcome',
      templateVars: {
        title: `Welcome, ${firstName}!`,
        subtitle: `Here's how to get started with ${productName}.`,
        productName,
        firstName,
        welcomeUrl: Meteor.absoluteUrl('documents'), // e.g., returns http://localhost:3000/documents
      },
    };
  } catch (exception) {
    throw new Error(`[sendWelcomeEmail.getEmailOptions] ${exception.message}`);
  }
};

const validateOptions = (options) => {
  try {
    if (!options) throw new Error('options object is required.');
    if (!options.user) throw new Error('options.user is required.');
  } catch (exception) {
    throw new Error(`[sendWelcomeEmail.validateOptions] ${exception.message}`);
  }
};

export default (options) => {
  try {
    validateOptions(options);
    const user = normalizeMeteorUserData({ user: options.user });
    const emailOptions = getEmailOptions(user);

    sendEmail(emailOptions).catch((error) => {
      throw new Error(error);
    });
  } catch (exception) {
    throw new Error(`[sendWelcomeEmail] ${exception.message}`);
  }
};

Defining Custom Templates

As you start to build out your product, it's inevitable that you'll want to define and send email using custom templates. To help with this, Pup has included the sendEmail() method located at /imports/modules/server/send-email.js which serves two purposes in one:

  • Helps to automatically compile a template located in /private/email-templates from Handlebars into HTML or plain text and inject it into the base template located at /private/email-templates/base.html.
  • Send the compiled template using Meteor's Email.send() method (exposed by the email Atmosphere package included with Pup).

Example email sent using Pup's sendEmail method.
Example email sent using Pup's sendEmail method.

As an example, on the right, we've displayed an HTML and text version of a template called message-notification. This template could be used to notify a user when they've received a new message in our product.

Notice that in both the HTML version and text version of our email, we're using special placeholder tags for variables like this: {{example}}. In both emails, we need to replace these—Handlebars expressions—with some actual content:

  • {{title}} The title to give to our base template.
  • {{subtitle}} The subtitle to give to our base template.
  • {{firstName}} The first name of our user.
  • {{messageUrl}} The url of the message we're notifying the user about.
  • {{productName}} The name of our product.

With our template defined, we can send email using it by passing the name of our template to the sendEmail() method, along with an additional parameter templateVars containing replacement values for each of our Handlebars expressions. To make it easier to perform other tasks after an email has sent, sendEmail() is wrapped by a JavaScript Promise, enabling us to chain .then() callbacks to run additional code after the end (or to easily .catch() errors).

Example – /private/email-templates/message-notification.html
<p style="margin-top:0px;">Hey, {{firstName}}!</p>

<p>You've received a new message. To view it, click the button below:</p>

<p><a href="{{messageUrl}}" class="btn-primary" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler">View Message</a></p>

<p style="margin-bottom:0px;">
Cheers, <br />
{{productName}} Team
</p>
Example – /private/email-templates/message-notification.txt
Hey, {{firstName}}!

You've received a new message. To view it, click the link below:

[View Message]({{messageUrl}})

Cheers,
{{productName}} Team
Example Send of Our Message Notification Template
sendEmail({
  to: 'user1@test.com',
  from: 'MessageBot',
  subject: `[MessageBot] You received a new message!`,
  template: 'message-notification',
  templateVars: {
    title: 'You\'ve received a new message, Joe!',
    subtitle: 'What does it say?',
    applicationName: 'MessageBot',
    firstName: 'Joe',
    messageUrl: Meteor.absoluteUrl('messages/123'),
  },
})
.catch((error) => {
  throw new Meteor.Error('500', `${error}`);
});

Accounts Emails

As part of the accounts workflow in Pup, two additional emails are available and sent on your behalf: email verification and reset password.

Both are sent via Meteor's accounts system with the former being sent whenever a new user signs up and the latter when a user attempts to recover their password.

By default, these emails are text-based (and frankly, quite boring), so we've spiced them up a bit using HTML and text templates (both of which live in /private/email-templates as separate .html and .txt files). Because this process is handled by Meteor, in order to use our custom templates we need to tap into Meteor's Accounts.emailTemplates configuration.

To load our custom templates, we use three modules included in Pup: getPrivateFile(), templateToHTML(), and templateToText(). The first is responsible for grabbing the raw contents of a file (relative to Pup's /private directory) as a string, while the former two are responsible for converting the string retrieved by that method from Handlebars markup—the language of choice for defining email templates in Pup—into either HTML or plain text, respectively.

In the code on the right, we can see that each of the templates verifyEmail and resetPassword are defined using an object with three methods: subject(), html(), and text(). The first returns the actual subject line of the email, while html() is used to be the HTML version of the email and text() is the text version (some users disable HTML email or rely on clients that don't support it).

For both emails, we load up both the .html and .txt template (both Handlebars templates) from the /private/email-templates.js directory using getPrivateFile(). Note that when we convert a template to either HTML or text using templateToHTML and templateToText, we pass the template as a string as the first argument and then an object of properties mapped to Handlebars variables in that template.

/startup/server/accounts/emailTemplates.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import getPrivateFile from '../../../modules/server/get-private-file';
import templateToHTML from '../../../modules/server/handlebars-email-to-html';
import templateToText from '../../../modules/server/handlebars-email-to-text';

const { emailTemplates } = Accounts;
const { productName } = Meteor.settings.public;

emailTemplates.siteName = productName;
emailTemplates.from = Meteor.settings.private.supportEmail;

emailTemplates.verifyEmail = {
  subject() {
    return `[${productName}] Verify Your Email Address`;
  },
  html(user, url) {
    return templateToHTML(getPrivateFile('email-templates/verify-email.html'), {
      title: 'Let\'s Verify Your Email',
      subtitle: `Verify your email to start using ${productName}.`,
      productName,
      firstName: user.profile.name.first,
      verifyUrl: url.replace('#/', ''),
    });
  },
  text(user, url) {
    const urlWithoutHash = url.replace('#/', '');
    if (Meteor.isDevelopment) console.info(`[Pup] Verify Email Link: ${urlWithoutHash}`); // eslint-disable-line
    return templateToText(getPrivateFile('email-templates/verify-email.txt'), {
      productName,
      firstName: user.profile.name.first,
      verifyUrl: urlWithoutHash,
    });
  },
};

emailTemplates.resetPassword = {
  subject() {
    return `[${productName}] Reset Your Password`;
  },
  html(user, url) {
    return templateToHTML(getPrivateFile('email-templates/reset-password.html'), {
      title: 'Let\'s Reset Your Password',
      subtitle: 'A password reset was requested for this email address.',
      firstName: user.profile.name.first,
      productName,
      emailAddress: user.emails[0].address,
      resetUrl: url.replace('#/', ''),
    });
  },
  text(user, url) {
    const urlWithoutHash = url.replace('#/', '');
    if (Meteor.isDevelopment) console.info(`Reset Password Link: ${urlWithoutHash}`); // eslint-disable-line
    return templateToText(getPrivateFile('email-templates/reset-password.txt'), {
      firstName: user.profile.name.first,
      productName,
      emailAddress: user.emails[0].address,
      resetUrl: urlWithoutHash,
    });
  },
};