nodemailer-sendpulse-transport
v0.1.0
Published
A Nodemailer transport for sending transactional emails through the SendPulse SMTP API.
Downloads
174
Maintainers
Readme
nodemailer-sendpulse-transport
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, andreplyTo- 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 nodemailerUsing pnpm:
pnpm add nodemailer-sendpulse-transport nodemailerUsing Yarn:
yarn add nodemailer-sendpulse-transport nodemailerUsage
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: '[email protected]',
},
to: '[email protected]',
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:
- Requests an access token from SendPulse.
- Caches the token in memory.
- Reuses the token for subsequent messages.
- Refreshes the token before its expiration.
- 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.comIt 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: '[email protected]',
},
to: [
{
name: 'John',
address: '[email protected]',
},
{
name: 'Jane',
address: '[email protected]',
},
],
cc: '[email protected]',
bcc: '[email protected]',
replyTo: {
name: 'Acme Support',
address: '[email protected]',
},
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: '[email protected]',
to: '[email protected]',
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: '[email protected]',
to: '[email protected]',
subject: 'Your invoice',
text: 'Your invoice is attached.',
attachments: [
{
filename: 'invoice.pdf',
content: invoiceBuffer,
},
],
});String content is also supported:
await mailer.sendMail({
from: '[email protected]',
to: '[email protected]',
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: '[email protected]',
to: '[email protected]',
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: '[email protected]',
to: '[email protected]',
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: '[email protected]',
to: '[email protected]',
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 abc123Accepted 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: '[email protected]',
to: '[email protected]',
subject: 'Hello',
text: 'Hello',
});
} catch (error) {
console.error(error);
}HTTP errors include the SendPulse response status where available:
SendPulse API error (401): Unauthorizedor:
SendPulse API error (400): Sender email is not allowedIf 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: '[email protected]',
to: '[email protected]',
subject: 'First email',
text: 'First',
});
await mailer.sendMail({
from: '[email protected]',
to: '[email protected]',
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:
- The cached token is invalidated.
- A fresh OAuth token is requested.
- 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: '[email protected]',
to: '[email protected]',
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: '[email protected]',
to: '[email protected]',
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: '[email protected]',
},
to: '[email protected]',
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: '[email protected]',
},
to: '[email protected]',
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
↓
RecipientThis 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
fetchsupportNodemailer
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 testor:
pnpm testThe test suite should cover:
- API-key authentication
- OAuth authentication
- OAuth token caching
- OAuth token refresh
401retry behavior- Address mapping
cc,bcc, andreplyTo- HTML encoding
- Plain-text messages
- Attachments
- SendPulse templates
- SendPulse API errors
Checks
pnpm lint
pnpm typecheck
pnpm build