Skip to content

Repository files navigation

nodemailer-sendpulse-transport

NPM Downloads npm version License codecov Test Publish to NPM

A Nodemailer transport for sending transactional emails through the SendPulse SMTP API.

It integrates directly with Nodemailer's transport system while using SendPulse's HTTP API under the hood.

Supports:

  • Static SendPulse API keys
  • OAuth 2.0 client credentials
  • Automatic OAuth token caching
  • Automatic OAuth token refresh on 401
  • HTML and plain-text emails
  • to, cc, bcc, and replyTo
  • Attachments
  • SendPulse templates
  • Nodemailer-compatible SentMessageInfo

Framework Support

While nodemailer-sendpulse-transport is completely framework agnostic, it is fully supported and can be used in:

  • Arcstack
  • H3ravel

Installation

npm install nodemailer-sendpulse-transport nodemailer

Using pnpm:

pnpm add nodemailer-sendpulse-transport nodemailer

Using Yarn:

yarn add nodemailer-sendpulse-transport nodemailer

Usage

Create a SendPulseTransport and pass it to Nodemailer:

import nodemailer from 'nodemailer';
import { SendPulseTransport } from 'nodemailer-sendpulse-transport';

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
});

const mailer = nodemailer.createTransport(transport);

await mailer.sendMail({
  from: {
    name: 'Acme',
    address: 'hello@example.com',
  },

  to: 'user@example.com',

  subject: 'Hello from SendPulse',

  html: `
        <h1>Hello</h1>
        <p>This email was sent through SendPulse.</p>
    `,

  text: 'Hello. This email was sent through SendPulse.',
});

Authentication

SendPulse supports both static API keys and OAuth client credentials.

API Key

For applications using a SendPulse API key:

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
});

The API key is sent directly as a Bearer token:

Authorization: Bearer <api-key>

No additional authentication requests are made.

OAuth

You can alternatively authenticate using a SendPulse client ID and client secret:

const transport = new SendPulseTransport({
  clientId: process.env.SENDPULSE_CLIENT_ID!,
  clientSecret: process.env.SENDPULSE_CLIENT_SECRET!,
});

The transport automatically:

  1. Requests an access token from SendPulse.
  2. Caches the token in memory.
  3. Reuses the token for subsequent messages.
  4. Refreshes the token before its expiration.
  5. Requests a fresh token and retries once when SendPulse unexpectedly responds with 401.

OAuth credentials and tokens are scoped to the individual transport instance.

Transport Options

API key authentication

interface SendPulseApiKeyOptions {
  apiKey: string;
  baseUrl?: string;
}

OAuth authentication

interface SendPulseOAuthOptions {
  clientId: string;
  clientSecret: string;
  baseUrl?: string;
}

apiKey and OAuth credentials are mutually exclusive.

Custom API URL

The default SendPulse API URL is:

https://api.sendpulse.com

It can be overridden when necessary:

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
  baseUrl: 'https://api.sendpulse.com',
});

Sending Email

All regular Nodemailer message options continue to work through sendMail().

await mailer.sendMail({
  from: {
    name: 'Acme',
    address: 'hello@example.com',
  },

  to: [
    {
      name: 'John',
      address: 'john@example.com',
    },
    {
      name: 'Jane',
      address: 'jane@example.com',
    },
  ],

  cc: 'manager@example.com',

  bcc: 'audit@example.com',

  replyTo: {
    name: 'Acme Support',
    address: 'support@example.com',
  },

  subject: 'Your account is ready',

  html: `
        <h1>Welcome!</h1>
        <p>Your account is ready.</p>
    `,

  text: 'Welcome! Your account is ready.',
});

HTML Emails

SendPulse expects HTML content to be Base64 encoded when using its SMTP API.

You do not need to handle this manually.

Pass normal HTML to Nodemailer:

await mailer.sendMail({
  from: 'hello@example.com',
  to: 'user@example.com',
  subject: 'HTML email',

  html: `
        <h1>Hello</h1>
        <p>This will be encoded automatically.</p>
    `,
});

The transport handles the required encoding before sending the payload to SendPulse.

Attachments

Nodemailer attachments are supported.

await mailer.sendMail({
  from: 'hello@example.com',
  to: 'user@example.com',

  subject: 'Your invoice',

  text: 'Your invoice is attached.',

  attachments: [
    {
      filename: 'invoice.pdf',
      content: invoiceBuffer,
    },
  ],
});

String content is also supported:

await mailer.sendMail({
  from: 'hello@example.com',
  to: 'user@example.com',

  subject: 'Attachment example',

  text: 'See attachment.',

  attachments: [
    {
      filename: 'hello.txt',
      content: 'Hello from SendPulse.',
    },
  ],
});

Attachments are converted into the Base64 format expected by SendPulse.

Nodemailer-normalized attachments that are already Base64 encoded are preserved without being encoded a second time.

SendPulse Templates

SendPulse-specific options can be provided through the sendpulse property.

await mailer.sendMail({
  from: 'hello@example.com',
  to: 'user@example.com',

  subject: 'Welcome',

  sendpulse: {
    template: {
      id: 12345,

      variables: {
        name: 'John',
        verificationCode: '482901',
      },
    },
  },
});

Template options:

interface SendPulseTemplate {
  id: string | number;
  variables: Record<string, unknown>;
}

When a SendPulse template is provided, the transport sends the template instead of the message's html and text fields.

Automatic Plain Text

SendPulse's auto_plain_text option can be enabled through the transport-specific message options:

await mailer.sendMail({
  from: 'hello@example.com',
  to: 'user@example.com',

  subject: 'Hello',

  html: '<h1>Hello</h1>',

  sendpulse: {
    autoPlainText: true,
  },
});

By default:

autoPlainText: false;

Response

sendMail() follows Nodemailer's SentMessageInfo structure.

A successful response contains:

{
  (envelope, messageId, accepted, rejected, pending, response, sendpulseId);
}

Example:

const result = await mailer.sendMail({
  from: 'hello@example.com',
  to: 'user@example.com',
  subject: 'Hello',
  text: 'Hello world',
});

console.log(result.messageId);
console.log(result.accepted);
console.log(result.response);

The transport additionally exposes the SendPulse message ID as:

result.sendpulseId;

The transport response follows this shape:

interface SendPulseTransportInfo extends SentMessageInfo {
  sendpulseId: string;
}

On a successful request:

result.response;

will contain a value similar to:

SendPulse queued message abc123

Accepted and rejected recipients

SendPulse's send endpoint does not provide per-recipient acceptance information when a message is initially queued.

When SendPulse successfully accepts the message:

accepted;

contains the recipients submitted through the message envelope, while:

rejected;

and:

pending;

remain empty.

Error Handling

SendPulse API errors are propagated through Nodemailer.

try {
  await mailer.sendMail({
    from: 'hello@example.com',
    to: 'user@example.com',
    subject: 'Hello',
    text: 'Hello',
  });
} catch (error) {
  console.error(error);
}

HTTP errors include the SendPulse response status where available:

SendPulse API error (401): Unauthorized

or:

SendPulse API error (400): Sender email is not allowed

If SendPulse returns a successful HTTP response but reports that the message could not be sent, the provider's error message is thrown.

OAuth Token Handling

When OAuth authentication is used, access tokens are cached in memory.

This prevents every email from triggering a new authentication request.

For example:

const transport = new SendPulseTransport({
  clientId: process.env.SENDPULSE_CLIENT_ID!,
  clientSecret: process.env.SENDPULSE_CLIENT_SECRET!,
});

const mailer = nodemailer.createTransport(transport);

await mailer.sendMail({
  from: 'hello@example.com',
  to: 'one@example.com',
  subject: 'First email',
  text: 'First',
});

await mailer.sendMail({
  from: 'hello@example.com',
  to: 'two@example.com',
  subject: 'Second email',
  text: 'Second',
});

Both messages reuse the same OAuth token while it remains valid.

Concurrent messages

If several messages are sent simultaneously while a token is being requested, the transport shares the pending token request rather than requesting multiple tokens.

401 recovery

If SendPulse responds with 401 while using OAuth:

  1. The cached token is invalidated.
  2. A fresh OAuth token is requested.
  3. The original request is retried once.

A second authentication failure is returned immediately.

Static API keys are not retried after 401, because retrying the same invalid key would produce the same result.

Using Multiple Transport Instances

Each transport maintains its own authentication state.

const primary = new SendPulseTransport({
  apiKey: process.env.PRIMARY_SENDPULSE_API_KEY!,
});

const secondary = new SendPulseTransport({
  clientId: process.env.SECONDARY_SENDPULSE_CLIENT_ID!,
  clientSecret: process.env.SECONDARY_SENDPULSE_CLIENT_SECRET!,
});

Credentials and OAuth tokens are not shared between transport instances.

TypeScript

The package is designed for TypeScript and exposes the transport and SendPulse-specific message types.

import {
  SendPulseTransport,
  type SendPulseMailOptions,
  type SendPulseTransportInfo,
} from 'nodemailer-sendpulse-transport';

Example:

const message: SendPulseMailOptions = {
  from: 'hello@example.com',
  to: 'user@example.com',

  subject: 'Hello',

  html: '<h1>Hello</h1>',

  sendpulse: {
    autoPlainText: true,
  },
};

Nodemailer return typing

Depending on the Nodemailer type definitions in use, nodemailer.createTransport() may expose the result as Nodemailer's standard SentMessageInfo rather than preserving provider-specific extensions.

If you need TypeScript access to sendpulseId, the transporter can be explicitly typed:

import nodemailer, { type Transporter } from 'nodemailer';

import {
  SendPulseTransport,
  type SendPulseTransportInfo,
} from 'nodemailer-sendpulse-transport';

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
});

const mailer = nodemailer.createTransport(
  transport,
) as Transporter<SendPulseTransportInfo>;

Now:

const result = await mailer.sendMail({
  from: 'hello@example.com',
  to: 'user@example.com',
  subject: 'Hello',
  text: 'Hello',
});

console.log(result.sendpulseId);

is correctly recognized by TypeScript.

API Key Example

import nodemailer from 'nodemailer';
import { SendPulseTransport } from 'nodemailer-sendpulse-transport';

const mailer = nodemailer.createTransport(
  new SendPulseTransport({
    apiKey: process.env.SENDPULSE_API_KEY!,
  }),
);

await mailer.sendMail({
  from: {
    name: 'My App',
    address: 'hello@example.com',
  },

  to: 'user@example.com',

  subject: 'Welcome',

  html: `
        <h1>Welcome</h1>
        <p>Thanks for signing up.</p>
    `,

  text: 'Welcome. Thanks for signing up.',
});

OAuth Example

import nodemailer from 'nodemailer';
import { SendPulseTransport } from 'nodemailer-sendpulse-transport';

const mailer = nodemailer.createTransport(
  new SendPulseTransport({
    clientId: process.env.SENDPULSE_CLIENT_ID!,

    clientSecret: process.env.SENDPULSE_CLIENT_SECRET!,
  }),
);

await mailer.sendMail({
  from: {
    name: 'My App',
    address: 'hello@example.com',
  },

  to: 'user@example.com',

  subject: 'Welcome',

  html: `
        <h1>Welcome</h1>
        <p>Thanks for signing up.</p>
    `,
});

Why an HTTP Transport?

Nodemailer commonly sends email through an SMTP connection.

This transport instead converts the Nodemailer message into the payload expected by SendPulse's transactional SMTP REST API.

Nodemailer
    ↓
SendPulseTransport
    ↓
SendPulse SMTP API
    ↓
Recipient

This means your application can continue using the standard Nodemailer API while authentication, payload conversion, Base64 encoding, and SendPulse-specific behavior are handled by the transport.

Requirements

  • Node.js with native fetch support

  • Nodemailer

  • A SendPulse account

  • Either:

    • a SendPulse API key, or
    • SendPulse OAuth client credentials
  • A sender permitted by your SendPulse SMTP account

Development and testing

The project can be tested with Vitest.

npm test

or:

pnpm test

The test suite should cover:

  • API-key authentication
  • OAuth authentication
  • OAuth token caching
  • OAuth token refresh
  • 401 retry behavior
  • Address mapping
  • cc, bcc, and replyTo
  • HTML encoding
  • Plain-text messages
  • Attachments
  • SendPulse templates
  • SendPulse API errors

Checks

pnpm lint
pnpm typecheck
pnpm build

License

MIT

About

A Nodemailer transport for sending transactional emails through the SendPulse SMTP API.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages