npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bernierllc/email-manager

v0.6.0

Published

Complete email management suite with template management, scheduling, analytics, and provider orchestration (Node.js only - use @bernierllc/email-manager-client for browser)

Downloads

486

Readme

@bernierllc/email-manager

A comprehensive email management service that orchestrates email sending, template management, scheduling, and analytics. This service integrates with @bernierllc/email-sender, @bernierllc/template-engine, and @bernierllc/magic-link to provide a complete email solution.

⚠️ Node.js Only

This package is Node.js-only and cannot be bundled for browser use due to Node.js dependencies (nodemailer, etc.).

For browser/frontend applications, use @bernierllc/email-manager-client instead, which provides the same API via HTTP calls.

Features

🚀 Email Orchestration

  • Multi-provider support with automatic failover
  • Template-based emails with dynamic content rendering
  • Email scheduling with retry logic
  • Batch email processing for bulk operations

📝 Template Management

  • CRUD operations for email templates
  • Template versioning and lifecycle management
  • Variable validation and syntax checking
  • Category-based organization

🔧 Provider Management

  • Multi-provider configuration (SendGrid, Mailgun, AWS SES, SMTP, Postmark)
  • Provider health monitoring and status tracking
  • Rate limiting and usage tracking
  • Automatic failover between providers

📊 Analytics & Reporting

  • Email performance tracking (opens, clicks, bounces)
  • Delivery reports and bounce analysis
  • Provider performance metrics
  • Historical data and trend analysis

Scheduling System

  • Queue-based scheduling with precise timing
  • Retry logic for failed deliveries
  • Schedule management (cancel, modify)
  • Background processing with automatic cleanup

Installation

npm install @bernierllc/email-manager

Dependencies

This package requires the following BernierLLC core packages:

npm install @bernierllc/email-sender @bernierllc/template-engine @bernierllc/magic-link

Quick Start

import { EmailManager, EmailManagerConfig } from '@bernierllc/email-manager';

// Configure providers
const config: EmailManagerConfig = {
  providers: [
    {
      id: 'sendgrid',
      name: 'SendGrid',
      type: 'sendgrid',
      config: { apiKey: process.env.SENDGRID_API_KEY },
      isActive: true,
      priority: 1
    }
  ],
  analytics: { enabled: true },
  scheduling: { enabled: true }
};

// Initialize manager
const emailManager = new EmailManager(config);

// Send email
const result = await emailManager.sendEmail({
  to: '[email protected]',
  subject: 'Hello World',
  html: '<h1>Hello!</h1><p>This is a test email.</p>',
  text: 'Hello! This is a test email.'
});

console.log('Email sent:', result.success);

Core API

Email Operations

Send Email

const result = await emailManager.sendEmail({
  to: ['[email protected]', '[email protected]'],
  cc: ['[email protected]'],
  subject: 'Important Update',
  html: '<h1>Update</h1><p>Important information...</p>',
  text: 'Update: Important information...',
  priority: 'high',
  metadata: { campaign: 'newsletter' }
});

Send Templated Email

const result = await emailManager.sendTemplatedEmail(
  'welcome-email',
  { 
    user: { name: 'John Doe' },
    company: 'MyApp',
    confirmUrl: 'https://myapp.com/confirm?token=abc123'
  },
  ['[email protected]']
);

Schedule Email

const scheduleTime = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
const result = await emailManager.scheduleEmail({
  to: '[email protected]',
  subject: 'Reminder',
  html: '<p>This is your scheduled reminder</p>'
}, scheduleTime);

Template Management

Create Template

const template = await emailManager.createTemplate({
  id: 'welcome-email',
  name: 'Welcome Email',
  subject: 'Welcome {{ user.name }}!',
  htmlTemplate: '<h1>Welcome {{ user.name }}!</h1><p>Thanks for joining {{ company }}!</p>',
  textTemplate: 'Welcome {{ user.name }}! Thanks for joining {{ company }}!',
  variables: [
    { name: 'user.name', type: 'string', required: true },
    { name: 'company', type: 'string', required: true }
  ],
  category: 'onboarding',
  version: '1.0.0',
  isActive: true
});

List Templates

const templates = await emailManager.listTemplates({
  page: 1,
  pageSize: 20,
  category: 'onboarding',
  active: true,
  search: 'welcome'
});

Update Template

const result = await emailManager.updateTemplate('welcome-email', {
  subject: 'Welcome to our platform, {{ user.name }}!',
  isActive: false
});

Provider Management

Add Provider

const result = await emailManager.addProvider({
  id: 'mailgun-backup',
  name: 'Mailgun Backup',
  type: 'mailgun',
  config: {
    apiKey: process.env.MAILGUN_API_KEY,
    domain: process.env.MAILGUN_DOMAIN
  },
  isActive: true,
  priority: 2,
  rateLimit: {
    maxPerMinute: 60,
    maxPerHour: 1000,
    maxPerDay: 10000
  }
});

Get Provider Status

const status = await emailManager.getProviderStatus('sendgrid');
console.log('Provider health:', status.isHealthy);
console.log('Current usage:', status.currentUsage);

Test Provider Connection

const testResult = await emailManager.testConnection('sendgrid');
console.log('Connection test:', testResult.success, testResult.message);

Analytics & Reporting

Get Email Statistics

const stats = await emailManager.getEmailStats('email-message-id');
console.log('Delivery rate:', stats.deliveryRate);
console.log('Open rate:', stats.openRate);
console.log('Click rate:', stats.clickRate);

Get Delivery Report

const report = await emailManager.getDeliveryReport({
  startDate: new Date('2024-01-01'),
  endDate: new Date('2024-01-31'),
  provider: 'sendgrid'
});
console.log('Total sent:', report.totalSent);
console.log('Delivery rate:', report.deliveryRate);

Get Analytics Summary

const summary = await emailManager.getAnalyticsSummary(30); // Last 30 days
console.log('Overview:', summary.overview);
console.log('Top providers:', summary.topProviders);
console.log('Daily stats:', summary.dailyStats);

Scheduling Management

Cancel Scheduled Email

const result = await emailManager.cancelScheduledEmail('schedule-id');
console.log('Cancelled:', result.success);

Configuration

EmailManagerConfig

interface EmailManagerConfig {
  providers: EmailProvider[];          // Email provider configurations
  templates?: EmailTemplate[];         // Pre-loaded templates
  scheduling?: SchedulingConfig;       // Scheduler settings
  analytics?: AnalyticsConfig;         // Analytics settings
  retry?: RetryConfig;                // Retry policy settings
}

Provider Configuration

interface EmailProvider {
  id: string;                         // Unique provider ID
  name: string;                       // Display name
  type: 'sendgrid' | 'mailgun' | 'ses' | 'smtp' | 'postmark';
  config: ProviderConfig;             // Provider-specific config
  isActive: boolean;                  // Enable/disable provider
  priority: number;                   // Provider priority (lower = higher priority)
  rateLimit?: RateLimit;              // Rate limiting settings
}

Template Configuration

interface EmailTemplate {
  id: string;                         // Unique template ID
  name: string;                       // Display name
  subject: string;                    // Email subject (supports variables)
  htmlTemplate: string;               // HTML template content
  textTemplate?: string;              // Plain text template content
  variables: TemplateVariable[];      // Template variables
  category?: string;                  // Template category
  version: string;                    // Template version
  isActive: boolean;                  // Enable/disable template
}

Provider Types

SendGrid

{
  type: 'sendgrid',
  config: {
    apiKey: 'your-sendgrid-api-key'
  }
}

Mailgun

{
  type: 'mailgun',
  config: {
    apiKey: 'your-mailgun-api-key',
    domain: 'your-domain.mailgun.org'
  }
}

AWS SES

{
  type: 'ses',
  config: {
    accessKeyId: 'your-access-key',
    secretAccessKey: 'your-secret-key',
    region: 'us-east-1'
  }
}

SMTP

{
  type: 'smtp',
  config: {
    host: 'smtp.gmail.com',
    port: 587,
    secure: false,
    auth: {
      user: '[email protected]',
      pass: 'your-password'
    }
  }
}

Postmark

{
  type: 'postmark',
  config: {
    serverToken: 'your-postmark-server-token'
  }
}

Template Variables

Templates support dynamic content through variables:

<!-- HTML Template -->
<h1>Welcome {{ user.name }}!</h1>
<p>Your account with {{ company }} is ready.</p>
<a href="{{ confirmUrl }}">Confirm Email</a>

<!-- Text Template -->
Welcome {{ user.name }}!
Your account with {{ company }} is ready.
Confirm your email: {{ confirmUrl }}

Variables are defined in the template configuration:

variables: [
  { name: 'user.name', type: 'string', required: true },
  { name: 'company', type: 'string', required: true },
  { name: 'confirmUrl', type: 'string', required: true }
]

Error Handling

The email manager provides comprehensive error handling:

const result = await emailManager.sendEmail(emailData);

if (!result.success) {
  console.error('Failed to send email:');
  result.errors?.forEach(error => {
    console.error(`${error.code}: ${error.message}`);
    if (error.recipient) {
      console.error(`Recipient: ${error.recipient}`);
    }
  });
}

Event Tracking

Track email events for analytics:

// Track email opened (typically from email tracking pixel)
emailManager.trackEmailEvent('opened', 'email-id', '[email protected]');

// Track email clicked (typically from link tracking)
emailManager.trackEmailEvent('clicked', 'email-id', '[email protected]', {
  url: 'https://example.com/clicked-link'
});

// Track email bounced (typically from webhook)
emailManager.trackEmailEvent('bounced', 'email-id', '[email protected]', {
  reason: 'Invalid email address'
});

Webhook Integration

Set up webhooks with your email providers to track events:

// Express.js webhook endpoint example
app.post('/webhook/sendgrid', (req, res) => {
  const events = req.body;
  
  events.forEach(event => {
    const { event: eventType, email, sg_message_id } = event;
    
    switch (eventType) {
      case 'delivered':
        emailManager.trackEmailEvent('delivered', sg_message_id, email);
        break;
      case 'open':
        emailManager.trackEmailEvent('opened', sg_message_id, email);
        break;
      case 'click':
        emailManager.trackEmailEvent('clicked', sg_message_id, email, { url: event.url });
        break;
      case 'bounce':
        emailManager.trackEmailEvent('bounced', sg_message_id, email, { reason: event.reason });
        break;
    }
  });
  
  res.status(200).send('OK');
});

Magic Link Integration

Generate secure authentication links:

// Generate magic link
const magicLink = await emailManager.generateMagicLink(
  '[email protected]',
  'login',
  { userId: '123', redirectUrl: '/dashboard' }
);

// Send magic link email
await emailManager.sendTemplatedEmail('magic-link-email', {
  magicLink: magicLink.url,
  user: { email: '[email protected]' }
}, ['[email protected]']);

// Verify magic link (in your authentication endpoint)
const verification = await emailManager.verifyMagicLink(token);
if (verification.valid) {
  // Log user in
  console.log('User authenticated:', verification.payload);
}

Performance Optimization

Template Caching

Templates are cached after compilation for better performance.

Provider Pooling

Email providers reuse connections where possible.

Batch Processing

Process multiple emails efficiently:

const emails = [
  { to: '[email protected]', subject: 'Email 1', html: '<p>Content 1</p>' },
  { to: '[email protected]', subject: 'Email 2', html: '<p>Content 2</p>' },
  // ... more emails
];

const results = await Promise.all(
  emails.map(email => emailManager.sendEmail(email))
);

Rate Limiting

Configure rate limits per provider to respect API limits:

{
  rateLimit: {
    maxPerMinute: 100,
    maxPerHour: 1000,
    maxPerDay: 10000
  }
}

System Status

Get comprehensive system statistics:

const stats = emailManager.getSystemStats();

console.log('Templates:', stats.templates);
// { total: 5, active: 4, inactive: 1, byCategory: { onboarding: 2, marketing: 3 } }

console.log('Providers:', stats.providers);
// { total: 2, active: 2, inactive: 0, byType: { sendgrid: 1, mailgun: 1 } }

console.log('Scheduler:', stats.scheduler);
// { isRunning: true, totalInQueue: 3, scheduled: 3, overdue: 0 }

Shutdown

Always properly shutdown the email manager:

emailManager.shutdown();

This stops the scheduler and cleans up resources.


Enhanced Features

The EnhancedEmailManager extends the base EmailManager with calendar invites, batch sending, subscription management, unsubscribe flows, and more. All enhanced features are available through a single import:

import { EnhancedEmailManager, EnhancedEmailManagerConfig } from '@bernierllc/email-manager';

Calendar Invites

Send calendar invitations (ICS) and cancellations as email attachments. The manager generates valid ICS content using the @bernierllc/email-calendar package and attaches it automatically.

Send a Calendar Invite

import { EnhancedEmailManager, CalendarEvent } from '@bernierllc/email-manager';

const event: CalendarEvent = {
  uid: '[email protected]',
  title: 'Q2 Business Review',
  description: 'Quarterly review of business metrics.',
  start: new Date('2025-07-15T14:00:00Z'),
  end: new Date('2025-07-15T15:30:00Z'),
  location: 'Conference Room A',
  organizer: { email: '[email protected]', name: 'Alice Johnson' },
  attendees: [
    { email: '[email protected]', name: 'Bob Smith', role: 'REQ-PARTICIPANT' },
    { email: '[email protected]', name: 'Carol Lee', role: 'OPT-PARTICIPANT' },
  ],
};

const result = await manager.sendCalendarInvite(
  event,
  ['[email protected]', '[email protected]'],
);

console.log('Invite sent:', result.success);

You can customize the subject and body:

const result = await manager.sendCalendarInvite(event, recipients, {
  subject: 'You are invited: Q2 Business Review',
  htmlBody: '<h2>Q2 Review</h2><p>Please join us for the quarterly review.</p>',
  textBody: 'Please join us for the Q2 quarterly review.',
  metadata: { campaign: 'internal-meetings' },
});

Cancel a Calendar Event

import { CalendarAttendee } from '@bernierllc/email-manager';

const organizer: CalendarAttendee = { email: '[email protected]', name: 'Alice' };
const attendees: CalendarAttendee[] = [
  { email: '[email protected]', name: 'Bob' },
  { email: '[email protected]', name: 'Carol' },
];

const result = await manager.sendCalendarCancel(
  '[email protected]', // UID of the original event
  organizer,
  attendees,
  {
    subject: 'Cancelled: Q2 Business Review',
    htmlBody: '<p>The Q2 Business Review has been cancelled.</p>',
  },
);

See examples/calendar-invites.ts for a complete working example.


Batch Sending

Send large volumes of email with built-in concurrency control, rate limiting, retry logic, and progress tracking. Powered by @bernierllc/email-batch-sender.

Basic Batch Send

import { BatchEmailInput } from '@bernierllc/email-manager';

const emails: BatchEmailInput[] = [
  {
    toEmail: '[email protected]',
    subject: 'Newsletter - July',
    htmlContent: '<h1>Newsletter</h1><p>Hello Alice...</p>',
    textContent: 'Newsletter\n\nHello Alice...',
  },
  {
    toEmail: '[email protected]',
    subject: 'Newsletter - July',
    htmlContent: '<h1>Newsletter</h1><p>Hello Bob...</p>',
  },
];

const result = await manager.sendBatch(emails);
console.log(`Sent: ${result.succeeded}/${result.total}, Failed: ${result.failed}`);

Batch Send with Rate Limiting and Retry

import { BatchSenderOptions } from '@bernierllc/email-manager';

const options: BatchSenderOptions = {
  concurrency: 5,               // Up to 5 emails in parallel
  rateLimit: {
    maxPerSecond: 10,            // Respect provider rate limits
  },
  retry: {
    maxRetries: 3,               // Retry failed sends
    initialDelayMs: 1000,        // 1 second initial delay
    backoffMultiplier: 2,        // Exponential backoff
  },
  failureStrategy: 'continue',  // Keep sending even if some fail
};

const result = await manager.sendBatch(emails, options);

Batch Send with Progress Tracking

const result = await manager.sendBatch(emails, {
  concurrency: 3,
  onProgress: (progress) => {
    const pct = ((progress.completed / progress.total) * 100).toFixed(0);
    console.log(`Progress: ${pct}% - ${progress.succeeded} sent, ${progress.failed} failed`);
  },
});

Abort on Failure

For critical emails where partial delivery is unacceptable:

const result = await manager.sendBatch(criticalEmails, {
  concurrency: 1,
  failureStrategy: 'abort',  // Stop immediately on first failure
  retry: { maxRetries: 5 },
});

if (result.aborted) {
  console.error('Batch was aborted due to a failure');
}

See examples/batch-sending.ts for complete examples.


Subscription Management

Manage subscriber lists, add/remove subscribers, and handle bulk imports. The subscription system uses @bernierllc/email-subscription under the hood.

Create Subscriber Lists

const newsletter = await manager.createSubscriberList('Monthly Newsletter', {
  description: 'Monthly product newsletter subscribers',
  metadata: { category: 'marketing' },
});

console.log('List ID:', newsletter.id);

Add Subscribers

// Individual subscriber
await manager.addSubscriber(newsletter.id, {
  email: '[email protected]',
  name: 'Alice Johnson',
});

// Bulk add (skips duplicates and suppressed emails)
const result = await manager.addSubscribers(newsletter.id, [
  { email: '[email protected]', name: 'User One' },
  { email: '[email protected]', name: 'User Two' },
  { email: '[email protected]', name: 'User Three' },
]);

console.log(`Added: ${result.added}, Skipped: ${result.skipped}`);

Look Up and Remove Subscribers

// Look up a subscriber
const subscriber = await manager.getSubscriber(newsletter.id, '[email protected]');
if (subscriber) {
  console.log('Status:', subscriber.status);
}

// Remove a subscriber
const removed = await manager.removeSubscriber(newsletter.id, '[email protected]');

Delete a List

const deleted = await manager.deleteSubscriberList(newsletter.id);

Unsubscribe Flows

Generate signed unsubscribe URLs and process unsubscribe requests. Requires the subscription.unsubscribe configuration.

Configuration

const config: EnhancedEmailManagerConfig = {
  providers: [/* ... */],
  subscription: {
    enabled: true,
    unsubscribe: {
      baseUrl: 'https://example.com/unsubscribe',
      secret: process.env.UNSUBSCRIBE_SECRET!,
      expiresIn: 30 * 24 * 60 * 60, // 30 days
    },
  },
};

Generate Unsubscribe URL

const url = manager.generateUnsubscribeUrl('[email protected]', listId);
// Returns: https://example.com/unsubscribe?token=<signed-jwt>

Process Unsubscribe Request

In your web server's unsubscribe endpoint:

app.get('/unsubscribe', async (req, res) => {
  const token = req.query.token as string;

  try {
    await manager.processUnsubscribe(token);
    res.send('You have been unsubscribed successfully.');
  } catch (error) {
    res.status(400).send('Invalid or expired unsubscribe link.');
  }
});

Send Email with List-Unsubscribe Headers

Automatically generate and attach RFC 2369 / RFC 8058 List-Unsubscribe headers:

const result = await manager.sendWithUnsubscribe(
  {
    to: '[email protected]',
    subject: 'Monthly Newsletter',
    html: '<p>Newsletter content...</p>',
  },
  listId,                  // Subscriber list ID
  '[email protected]',     // Recipient for token generation
  true,                    // Enable one-click unsubscribe (RFC 8058)
);

See examples/subscription-management.ts for the full lifecycle.


Suppression

Check if an email address is suppressed before sending. Emails become suppressed when a subscriber unsubscribes or when bounces/complaints are processed.

// Check global suppression
const isSuppressed = await manager.isSuppressed('[email protected]');

// Check list-specific suppression
const isListSuppressed = await manager.isSuppressed('[email protected]', listId);

if (isSuppressed) {
  console.log('Skipping send - email is suppressed');
}

Custom Subscription Store

By default, the manager uses an in-memory subscription store. For production, replace it with a database-backed implementation:

import { SubscriptionStore } from '@bernierllc/email-manager';

class DatabaseSubscriptionStore implements SubscriptionStore {
  // Implement all SubscriptionStore methods using your database
  // (createList, getList, addSubscriber, etc.)
}

const store = new DatabaseSubscriptionStore();
manager.setSubscriptionStore(store);

Custom Headers

Add custom email headers for threading, read receipts, and other purposes by including them in the metadata.headers field.

Email Threading

const result = await manager.sendEmail({
  to: '[email protected]',
  subject: 'Re: Project Discussion',
  html: '<p>I agree with the proposed approach.</p>',
  metadata: {
    headers: {
      'In-Reply-To': '<[email protected]>',
      'References': '<[email protected]>',
    },
  },
});

List-Unsubscribe Headers (Standalone)

Use the createListUnsubscribeHeaders utility directly:

import { createListUnsubscribeHeaders } from '@bernierllc/email-manager';

const headers = createListUnsubscribeHeaders(
  'https://example.com/unsubscribe?id=123',   // HTTPS unsubscribe URL
  'mailto:[email protected]?subject=unsub',    // Optional mailto fallback
  true,                                         // One-click unsubscribe (RFC 8058)
);

const result = await manager.sendEmail({
  to: '[email protected]',
  subject: 'Weekly Digest',
  html: '<p>Your digest...</p>',
  metadata: { headers },
});

MDN (Read Receipt) Request

const result = await manager.sendEmail({
  to: '[email protected]',
  subject: 'Contract for Review',
  html: '<p>Please confirm receipt of the attached contract.</p>',
  metadata: {
    headers: {
      'Disposition-Notification-To': '[email protected]',
    },
  },
});

See examples/attachments-and-headers.ts for complete examples.


Attachments

Send emails with file attachments, inline images, or both.

File Attachment

const result = await manager.sendEmail({
  to: '[email protected]',
  subject: 'Report Attached',
  html: '<p>Please find the report attached.</p>',
  attachments: [
    {
      filename: 'report.pdf',
      content: fs.readFileSync('/path/to/report.pdf'),
      contentType: 'application/pdf',
    },
  ],
});

String Content Attachment

const csvData = 'Name,Email\nAlice,[email protected]\nBob,[email protected]\n';

const result = await manager.sendEmail({
  to: '[email protected]',
  subject: 'User Export',
  html: '<p>User export attached.</p>',
  attachments: [
    {
      filename: 'users.csv',
      content: csvData,  // String content is also supported
      contentType: 'text/csv',
    },
  ],
});

Inline / Embedded Image

Reference inline images using a Content-ID (CID):

const result = await manager.sendEmail({
  to: '[email protected]',
  subject: 'Order Confirmation',
  html: `
    <img src="cid:company-logo" alt="Logo" />
    <h2>Order Confirmed!</h2>
  `,
  attachments: [
    {
      filename: 'logo.png',
      content: fs.readFileSync('/path/to/logo.png'),
      contentType: 'image/png',
      contentDisposition: 'inline',
      cid: 'company-logo',  // Matches src="cid:company-logo" in HTML
    },
  ],
});

Multiple Attachments

const result = await manager.sendEmail({
  to: '[email protected]',
  subject: 'Brand Kit Assets',
  html: '<p>Attached are the brand assets.</p>',
  attachments: [
    {
      filename: 'guidelines.pdf',
      content: fs.readFileSync('/path/to/guidelines.pdf'),
      contentType: 'application/pdf',
    },
    {
      filename: 'logo.png',
      content: fs.readFileSync('/path/to/logo.png'),
      contentType: 'image/png',
    },
    {
      filename: 'palette.json',
      content: JSON.stringify({ primary: '#0066CC' }, null, 2),
      contentType: 'application/json',
    },
  ],
});

See examples/attachments-and-headers.ts for complete examples.


Capability Matrix & Smart Routing

The email manager includes a full capability matrix that maps every feature to every provider, enabling smart routing decisions and graceful degradation.

Capability Matrix

Each feature is classified per provider with one of four source types:

| Source | Meaning | |--------|---------| | provider | Natively supported by the email provider's API | | platform | Implemented by the platform (polyfilled locally) | | enhanced | Built on top of provider primitives with added value | | unsupported | Not available for this provider |

Provider Feature Support

| Feature | SendGrid | Mailgun | Postmark | SES | SMTP | |---------|----------|---------|----------|-----|------| | sendEmail | provider | provider | provider | provider | provider | | batchSend | provider | provider | provider | provider | platform | | providerTemplates | provider | provider | provider | provider | unsupported | | localTemplates | platform | platform | platform | platform | platform | | calendarInvites | platform | platform | platform | platform | platform | | calendarEventMgmt | platform | platform | platform | platform | platform | | webhooksReceive | provider | provider | provider | provider | unsupported | | webhookNormalization | enhanced | enhanced | enhanced | enhanced | unsupported | | deliveryTracking | provider | provider | provider | provider | unsupported | | inboundEmailParsing | enhanced | enhanced | enhanced | enhanced | unsupported | | subscriptionMgmt | enhanced | enhanced | platform | platform | platform | | suppressionLists | provider | provider | provider | provider | platform | | unsubscribeUrlGeneration | platform | platform | platform | platform | platform | | scheduledSend | provider | provider | platform | platform | platform | | openClickTracking | provider | provider | provider* | unsupported | unsupported | | emailContentParsing | platform | platform | platform | platform | platform | | emailHeaderMgmt | platform | platform | platform | platform | platform | | attachments | provider | provider | provider | provider | provider | | advancedAttachmentProcessing | platform | platform | platform | platform | platform | | dkimSpf | provider | provider | provider | provider | unsupported | | linkBranding | provider | provider | unsupported | unsupported | unsupported | | retryResilience | platform | platform | platform | platform | platform | | multiProviderFailover | platform | platform | platform | platform | platform |

*Postmark supports open tracking only; click tracking is not available.

Notable provider-specific limitations:

  • SendGrid scheduledSend: 72-hour maximum scheduling window
  • Mailgun scheduledSend: 3-day maximum scheduling window
  • SES webhooksReceive / deliveryTracking: Requires SNS topic configuration

Routing Configuration

Capability-aware routing is opt-in. Enable it by adding a routing section to your config:

import { EmailManager, EmailManagerConfig } from '@bernierllc/email-manager';

const config: EmailManagerConfig = {
  providers: [
    { id: 'sg', name: 'SendGrid', type: 'sendgrid', config: { apiKey: '...' }, isActive: true, priority: 1 },
    { id: 'mg', name: 'Mailgun', type: 'mailgun', config: { apiKey: '...', domain: '...' }, isActive: true, priority: 2 },
  ],
  routing: {
    strategy: 'primary-failover',
    featureOverrides: {
      // Always use platform implementation for local templates
      localTemplates: { behavior: 'platform-always' },
      // Throw an error if no native provider supports the feature
      openClickTracking: { behavior: 'error' },
    },
  },
  degradation: {
    logLevel: 'warn',
    includeDocLinks: true,
    docsBaseUrl: 'https://docs.example.com/email',
  },
};

const manager = new EmailManager(config);

Feature Override Behaviors

| Behavior | Description | |----------|-------------| | native-first | (Default) Prefer providers with native or enhanced support; fall back to platform if none found | | platform-always | Always use the platform implementation, even if the provider supports it natively | | error | Throw FeatureUnsupportedError if no provider has native or enhanced support |

Degradation Behavior

When a feature is routed to a provider via platform polyfill (not native), the system records degradation information:

  • platform source features (e.g., SMTP batchSend): Emails are sent sequentially rather than in a true batch. The SendResult.routing field indicates degraded: true with a degradationReason.
  • unsupported features (e.g., SMTP webhooksReceive): The router skips the provider entirely and tries the next in priority order. If no provider supports the feature, a FeatureUnsupportedError is thrown.
  • Degradation logging: Controlled by degradation.logLevel ('silent' | 'info' | 'warn' | 'error'). When includeDocLinks is true, log messages include links to degradation documentation.

Redis Resolver Setup

By default, the capability matrix is resolved from an in-memory snapshot. For shared or dynamic capability data across multiple instances, use the Redis-backed resolver:

# Set the environment variable to enable Redis resolver auto-detection
export EMAIL_MANAGER_REDIS_URL="redis://localhost:6379"

The factory (createCapabilityResolver) automatically detects the environment variable and creates a SafeCapabilityResolver that wraps a Redis resolver with an in-memory fallback. If ioredis is not installed or Redis is unavailable, it falls back to the in-memory resolver silently.

Redis Resolver Options

import { createCapabilityResolver } from '@bernierllc/email-manager';

const resolver = await createCapabilityResolver({
  namespace: 'myapp:capabilities', // Redis key namespace (default: 'email-manager:capabilities')
  ttl: 3600,                       // Cache TTL in seconds (default: varies by implementation)
});

You can also provide your own resolver implementation:

const config: EmailManagerConfig = {
  providers: [/* ... */],
  routing: {
    strategy: 'primary-failover',
    capabilityResolver: myCustomResolver, // Must implement CapabilityResolver interface
  },
};

RoutingMetadata in Responses

When routing is active, SendResult includes a routing field:

const result = await manager.sendEmail(emailData);

if (result.routing) {
  console.log('Provider used:', result.routing.provider);
  console.log('Feature:', result.routing.feature);
  console.log('Source:', result.routing.source);       // 'provider' | 'platform' | 'enhanced' | 'unsupported'
  console.log('Degraded:', result.routing.degraded);   // true if platform polyfill was used
  console.log('Reason:', result.routing.degradationReason);
  console.log('Tried:', result.routing.attemptedProviders); // providers checked before selecting
}

The RoutingMetadata interface:

interface RoutingMetadata {
  provider: string;
  feature: string;
  source: 'provider' | 'platform' | 'enhanced' | 'unsupported';
  degraded: boolean;
  degradationReason?: string;
  attemptedProviders?: string[];
}

Migration Guide from v0.4.x

The capability matrix and routing system is fully backward compatible. Existing code continues to work without changes:

  • No routing config: When routing is omitted from the config, the manager uses the same provider selection logic as before (priority-based failover).
  • No degradation config: When degradation is omitted, degradation events are not logged.
  • Opt-in: To enable capability-aware routing, add a routing section to your config. The RoutingMetadata field on SendResult only appears when routing is active.
  • No new required dependencies: The Redis resolver is optional and only activated when EMAIL_MANAGER_REDIS_URL is set and ioredis is installed.

Re-exported Types

For convenience, the email-manager re-exports types and utilities from its sub-packages so consumers do not need direct dependencies:

| Package | Re-exported Items | |---------|-------------------| | @bernierllc/email-calendar | CalendarEvent, CalendarAttendee, CalendarMethod, createCalendarInvite, createCalendarCancel, toCalendarAttachment | | @bernierllc/email-batch-sender | BatchSenderOptions, BatchResult, BatchEmailInput, BatchProgress, BatchSender, createBatchSender | | @bernierllc/email-subscription | SubscriberList, Subscriber, SubscriptionStore, AddSubscriberInput, CreateListOptions, BulkAddResult, UnsubscribeManager, SuppressionManager, InMemorySubscriptionStore | | @bernierllc/email-headers | EmailHeaders, ThreadingHeaders, ListUnsubscribeHeaders, createListUnsubscribeHeaders | | @bernierllc/email-attachments | AttachmentInput, AttachmentLimits, AttachmentValidationResult |

License

Copyright (c) 2025 Bernier LLC. All rights reserved.

See Also