DEV Community

Cover image for Nodemailer: Send through multiple service providers
Tejas Kumthekar for Courier

Posted on • Edited on • Originally published at courier.com

Nodemailer: Send through multiple service providers

Early stage companies are constantly evolving their product to fit the market they operate in. They reach customers to keep them engaged using a high magnitude vector that contributes to their success. Architecting the communications strategy for your product thus becomes an important problem to tackle, which in turn can cause second-order effects like having to trade off the speed of product development iterations. The decisions you would have to make along the path can be hard, confusing and prone to change, and the last thing you want to invest precious time and energy in is these non-core features of your product.

In this blog post, we start by diving into Nodemailer, a module that helps send emails from your Node.js backend, and then we steer our way into writing a transport layer plugin that can help you switch downstream email service providers purely by configuration instead of tedious and sometimes massive code changes.

Projects that have stood the test of time

In the fast growing tech industry, there’s a special place in the hall of fame for projects that have stood the test of time. Nodemailer is one such project that deserves the well-earned spot. Launched in 2010, not long after Node.js’ inception, Nodemailer has been a go-to dependency for sending email messages for Node.js users. With 14k+ stars and 438k+ usages on GitHub and ~1.48M weekly downloads on NPM as of this writing, Node.js developer community has benefited immensely from the project.

Apart from a wide variety of features provided by Nodemailer, it comes with zero dependencies and has been evolving with a security-first mindset. While some of us might think how hard it can be to have zero dependencies, let's remember we’re in a Node.js world where there is a package for every little thing you could possibly imagine. Don’t believe it? Brace yourself while looking at https://www.npmjs.com/package/is-odd and https://www.npmjs.com/package/is-even. Yep that’s right - there’s a dependency that can help determine if a number is odd or even. No more complex arithmetic, right?

On another note, Nodemailer’s security-first development also deserves a round of applause. Those of you who have had to frequently run audits and rack your brains figuring out how to resolve vulnerabilities would agree. Moreover, considering the scale at which projects like Nodemailer have been adopted, even a single vulnerability can have huge negative impacts on the software of the world. One recent example is a vulnerability in the netmask npm package that could potentially play a hand in malware delivery.

Extensibility and elegant abstractions are the cruxes

Nodemailer is highly extensible via plugins that can operate on the mail object (pre-processing step), the mail stream (processing step) or the transports (sending step). For the scope of this blog post, we’re mostly concerned about the sending step. SMTP is the main transport in Nodemailer for delivering messages - but can be extended using the transport plugin to a different transport mechanism. For example, if you already use Amazon SES, you can use the SES transport plugin instead of SMTP, which is built-in. Like SES, there are several email service providers out there like SendGrid, Mailgun, Postmark and SparkPost to name a few. In order to cater to the Node.js (and Nodemailer) audience, most of these providers do have a transport plugin of its own should you need to integrate with your tech stack. You can explore through all such plugins with a simple “Nodemailer transport” search query on the NPM registry.

A definite question that always springs to mind is why there are so many email service providers out there and how to choose the one that fits best with your communication strategy. Unsurprisingly, such analysis paralysis is fairly ubiquitous in today’s SaaS world. Calling it a SaaS paradox of choice would hardly be an overstatement. Unless you have theoretically infinite resources at your disposal (read: you’re at a giant like Google or Facebook), these choices can slow down the speed of development significantly and don’t bode well with the “move fast and break things” mantra if you have to go back and forth between email service providers and rewrite your backend stack to accommodate the changeset.

nodemailer-1
Source: https://xkcd.com/2224/

Classic build vs buy mindset

In one of our blog posts, we talked about the build vs buy mindset and how it makes a difference, especially if you are a startup - read about it here if you’re curious. The mindset not only makes a difference for your speed of iteration, but also keeps you potentially ahead in the game especially if and when your competitors are adopting buy over build for non-core features of their product. One of the best examples is hosting on AWS vs maintaining on-premises cloud infrastructure - outsourcing cloud hosting to AWS gives you the speed of iteration with no cost of infrastructure maintenance. Imagine all of your competitors are on AWS but you aren’t - it’d certainly make a tremendous negative difference in the long run. While this certainly does not necessarily apply to all the companies out there, it does hold truth for the most. “Disruptive economic events like COVID have caused many people to step back and think about how they want to change strategically. And many have come to the conclusion that they do not want to own and run their own data centers.”, says AWS CFO Brian Olsavsky https://www.theregister.com/2021/07/30/amazon_q2_2021/.

An effective communication strategy plays a critical role in maximizing the odds of product success. It's a no-brainer that you have to have a way to reach out to users via Email - but that’s not going to be enough. Customer engagement via text messaging, in-app messaging or direct messaging (think Slack, Whatsapp, MS Teams etc.) has become a necessity in today’s hyper-competitive market. Sooner or later (definitely sooner than you think), having these multiple channels of communication are going to make a difference to your business. As you would scale your product or company, notifications infrastructure has to scale as well in order to handle queuing, scheduling, routing, rendering, and ensuring delivery of messages across multiple channels. Companies like LinkedIn have spent millions to build this infrastructure in-house - depicted quite well in LinkedIn’s Air Traffic Controller blog post.

Continuing my mini-rant on the SaaS analysis paralysis, as you’d have probably seen it coming, the paralysis exists on text messaging too - Twilio, Sinch, Plivo and Vonage to name a few providers in the arena. Managing multiple channels and providers to suit the communication strategy you’ve embraced is not a cakewalk. Lets circle back to the never-ending list of email providers and their corresponding Nodemailer transport plugin implementations out there. An easy way to fight the paradox is with a layer of abstraction, such that you can change providers without having to rewrite the code to switch the transport plugins. That’s exactly where Courier comes into picture with its API layer of abstraction and a configurable UI to switch email providers without having to change the code on your end. Needless to say, Courier should expose its own Nodemailer transport plugin which abstracts using a specific email provider plugin. Having this abstraction would imply no vendor lock-in as far as downstream delivery is concerned.

Show me the code!

Let’s dive right into writing the plugin! Nodemailer plugin creation (specifically the transport section) and a Mailgun implementation would be our reference points. While the core documentation is exhaustive, we’ll keep it relatively simple for the first version of our plugin.

TL;DR: https://github.com/tk26/nodemailer-courier-transport is the code we’ll end up writing.

Step 1 - Create a new NPM package and adding courier-node as a dependency

Here's the Node.js module for communicating with the Courier REST API. The source code behind the package is MIT licensed at courier-node repo.

Step 2 - Configuration prerequisites

You will need to get a Courier API key to get started. You can sign up and create one for free at courier.com. Courier being a middleware, you’d have to configure an email provider of your choice that’d do the actual delivery of the email to the recipient. Stuck in the paradox of choice? We got you! Here’s an article we published earlier this year that could be helpful in figuring out. Thereafter, you’d have to create a notification template with email as the channel. Here’s how to do it using SendGrid.

Step 3 - Writing the transport core

Transports have a send() method and name and version properties. Transport object gets passed to the createTransport() method to create the transporter object.

const transport = (options) => {
  const courier = CourierClient({ authorizationToken: options.apiKey });

  const courierSend = input => courier.send({
    eventId: input.eventId,
    recipientId: input.userId,
    profile: {
    email: input.to,
    },
  });

  return {
    name: "Courier",
    version: packageData.version,
    send: send(courierSend),
  };
};
Enter fullscreen mode Exit fullscreen mode

That’s pretty much it for the basic version of the plugin. We’ve pushed the code to https://github.com/tk26/nodemailer-courier-transport. feel free to play around with it and reach out with any questions/feedback. It’s open sourced under MIT license so feel free to contribute to the open source project.

Step 4 - Using the code in your tech stack

Here’s how things would look in your Node.js backend -

const nodemailer = require("nodemailer");
const courierTransport = require("nodemailer-courier-transport");

// This is your API key that you retrieve from courier.com account (free up to 10k monthly sends)
const auth = {
  apiKey: "ABCDEFGHIJKLMN",
};

const nodemailerCourier = nodemailer.createTransport(courierTransport(auth));

nodemailerCourier.sendMail(
  {
    eventId: "courier-event-id", // Configured in Courier UI
    to: "recipient@domain.com",
    userId: "unique-user-id",
  },
  (err, info) => {
    if (err) {
    console.log(`Error: ${err}`);
    } else {
    console.log(`Response: ${info}`);
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

Note: We haven’t published the package yet as it is still in a nascent stage. Let us know if you are looking to contribute to the package or want us to publish it so you can use it in your backend. :)

Key takeaways

Let's summarize the key takeaways from the post. First and foremost - writing and maintaining open source projects that stand the test of time is hard, and projects like Nodemailer are great examples to learn from. Buying over building non-core product features is one of the most important trade-offs you can do to grow your company faster. The sooner you do the trade-off, the higher the returns (and in turn the competitive advantage) you gain. Last but not least, communication strategy is a key pillar in your company’s success - make sure you get it right without compromising on the speed of iteration.

If you’re looking for an easy to integrate, scalable and a reliable communication strategy - sign up for a free Courier account. You build the next big thing and let us handle the communications for you! 🚀

Top comments (0)