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-client

v0.6.1

Published

Browser-safe HTTP client for @bernierllc/email-manager - use this in frontend applications

Readme

@bernierllc/email-manager-client

Browser-safe HTTP client for @bernierllc/email-manager

Use this package in frontend/browser applications to interact with email-manager via HTTP API calls. This package has zero Node.js dependencies and can be safely bundled for browser use.

Why This Package Exists

The @bernierllc/email-manager package uses Node.js-only dependencies (like nodemailer) that cannot be bundled for browser use. This client package provides the same API surface but makes HTTP calls to a backend server instead.

Installation

npm install @bernierllc/email-manager-client

Quick Start

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

const client = new EmailManagerClient({
  baseUrl: 'https://api.example.com/email',
  apiKey: 'your-api-key'
});

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

if (result.success) {
  console.log('Email sent:', result.messageId);
}

Configuration

interface EmailManagerClientConfig {
  baseUrl: string;                       // Required: Backend API base URL
  apiKey?: string;                       // Optional: API key for authentication
  timeout?: number;                      // Optional: Request timeout in ms (default: 30000)
  headers?: Record<string, string>;      // Optional: Additional headers
}

Backend Setup

You need a backend server that exposes the email-manager API. The backend should:

  1. Import and use @bernierllc/email-manager (Node.js-only)
  2. Expose HTTP endpoints that match the client API
  3. Handle authentication/authorization

Example Backend Route (Next.js API Route)

// app/api/email/send/route.ts
import { EnhancedEmailManager } from '@bernierllc/email-manager';
import { NextRequest, NextResponse } from 'next/server';

const emailManager = new EnhancedEmailManager(/* config */);

export async function POST(request: NextRequest) {
  try {
    const emailData = await request.json();
    const result = await emailManager.sendEmail(emailData);
    return NextResponse.json(result);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to send email', message: error.message },
      { status: 500 }
    );
  }
}

API Reference

Email Sending

sendEmail(email)

Send an email immediately.

const result = await client.sendEmail({
  to: '[email protected]',
  subject: 'Welcome!',
  html: '<h1>Welcome</h1>',
  text: 'Welcome',
  priority: 'high',
  metadata: { campaign: 'onboarding' }
});
// => { success: true, messageId: 'msg_abc123', provider: 'sendgrid', sentAt: '2025-01-15T...' }

scheduleEmail(email, scheduledFor)

Schedule an email for future delivery.

const result = await client.scheduleEmail(
  { to: '[email protected]', subject: 'Reminder', html: '<p>Don\'t forget!</p>' },
  new Date('2025-03-01T09:00:00Z')
);
// => { success: true, scheduleId: 'sch_xyz', scheduledFor: '2025-03-01T09:00:00.000Z', emailId: '...', status: 'scheduled' }

cancelScheduledEmail(scheduleId)

const result = await client.cancelScheduledEmail('sch_xyz');
// => { success: true, message: 'Scheduled email cancelled' }

getScheduledEmail(scheduleId) / listScheduledEmails(options?)

const email = await client.getScheduledEmail('sch_xyz');
const emails = await client.listScheduledEmails({ page: 1, pageSize: 20 });

Calendar Methods

sendCalendarInvite(event, recipients, options?)

Send a calendar invitation (iCal/ICS) to recipients.

const result = await client.sendCalendarInvite(
  {
    title: 'Sprint Planning',
    description: 'Weekly sprint planning session',
    start: '2025-02-01T14:00:00Z',
    end: '2025-02-01T15:00:00Z',
    location: 'Conference Room A',
    organizer: { email: '[email protected]', name: 'Team Lead' },
    attendees: [
      { email: '[email protected]', name: 'Developer 1' },
      { email: '[email protected]', name: 'Developer 2' }
    ]
  },
  ['[email protected]', '[email protected]'],
  { subject: 'You are invited: Sprint Planning' }
);
// => { success: true, messageId: 'msg_cal_001', eventUid: 'evt_abc123' }

cancelCalendarEvent(eventUid, organizer, attendees)

Cancel a previously sent calendar event and notify all attendees.

const result = await client.cancelCalendarEvent(
  'evt_abc123',
  { email: '[email protected]', name: 'Team Lead' },
  [{ email: '[email protected]' }, { email: '[email protected]' }]
);
// => { success: true, eventUid: 'evt_abc123' }

Batch Sending

sendBatch(emails, options?)

Send multiple emails in a single operation with configurable concurrency and failure handling.

const result = await client.sendBatch(
  [
    { to: '[email protected]', subject: 'Update', html: '<p>News for you</p>' },
    { to: '[email protected]', subject: 'Update', html: '<p>News for you</p>' },
    { to: '[email protected]', subject: 'Update', html: '<p>News for you</p>' }
  ],
  {
    concurrency: 5,
    rateLimit: { maxPerSecond: 10, maxPerMinute: 300 },
    failureStrategy: 'continue',   // 'continue' | 'abort-on-first' | 'abort-on-threshold'
    failureThreshold: 0.5          // Abort if 50% fail (used with 'abort-on-threshold')
  }
);
// => {
//   total: 3,
//   succeeded: 2,
//   failed: 1,
//   results: [
//     { recipient: '[email protected]', success: true, messageId: 'msg_1', attempts: 1 },
//     { recipient: '[email protected]', success: true, messageId: 'msg_2', attempts: 1 },
//     { recipient: '[email protected]', success: false, error: 'Invalid address', attempts: 2 }
//   ],
//   duration: 1234,
//   aborted: false
// }

Failure strategies:

| Strategy | Behavior | |----------|----------| | continue | Send all emails regardless of individual failures | | abort-on-first | Stop immediately on the first failure | | abort-on-threshold | Stop when the failure rate exceeds failureThreshold |


Template Management

createTemplate(template) / getTemplate(id) / updateTemplate(id, template) / deleteTemplate(id)

const result = await client.createTemplate({
  name: 'welcome',
  subject: 'Welcome, {{name}}!',
  htmlTemplate: '<h1>Welcome {{name}}</h1>',
  variables: [{ name: 'name', type: 'string', required: true }],
  isActive: true
});

const template = await client.getTemplate('tmpl_abc');
await client.updateTemplate('tmpl_abc', { subject: 'Updated subject' });
await client.deleteTemplate('tmpl_abc');

listTemplates(options?) / validateTemplate(template)

const list = await client.listTemplates({ page: 1, pageSize: 10, category: 'marketing' });
// => { templates: [...], total: 42, page: 1, pageSize: 10 }

const validation = await client.validateTemplate({ htmlTemplate: '<p>{{missing}}</p>' });
// => { isValid: false, errors: ['Unknown variable: missing'] }

Provider Management

addProvider(provider) / getProvider(id) / updateProvider(id, provider) / deleteProvider(id)

const result = await client.addProvider({
  name: 'Primary SendGrid',
  type: 'sendgrid',
  config: { apiKey: '...' },
  isActive: true,
  priority: 1,
  rateLimit: { maxPerMinute: 100, maxPerHour: 3000, maxPerDay: 50000 }
});

listProviders() / getProviderStatus(id) / testProvider(id)

const providers = await client.listProviders();
// => { providers: [...], total: 3 }

const status = await client.getProviderStatus('prov_123');
// => { id: 'prov_123', isActive: true, isHealthy: true, lastHealthCheck: '...', currentUsage: { minute: 5, hour: 120, day: 2400 } }

const test = await client.testProvider('prov_123');
// => { success: true, message: 'Connection successful' }

Analytics

getEmailStats(emailId) / getDeliveryReport(options?) / getBounceReport(options?)

const stats = await client.getEmailStats('msg_abc');
// => { emailId: 'msg_abc', sent: 1000, delivered: 985, opened: 450, clicked: 120, bounced: 15, deliveryRate: 0.985, openRate: 0.45, clickRate: 0.12 }

const report = await client.getDeliveryReport({
  startDate: '2025-01-01T00:00:00Z',
  endDate: '2025-01-31T23:59:59Z',
  provider: 'sendgrid'
});

const bounces = await client.getBounceReport({
  startDate: '2025-01-01T00:00:00Z',
  endDate: '2025-01-31T23:59:59Z'
});

Subscription Management

createSubscriberList(name, description?)

Create a new subscriber list for organizing recipients.

const list = await client.createSubscriberList('Weekly Newsletter', 'Subscribers to our weekly digest');
// => { id: 'list_abc', name: 'Weekly Newsletter', description: '...', subscriberCount: 0, createdAt: '...', updatedAt: '...' }

listSubscriberLists(options?)

const result = await client.listSubscriberLists({ limit: 20, offset: 0 });
// => { items: [{ id: 'list_abc', name: 'Weekly Newsletter', subscriberCount: 150, ... }], total: 3 }

deleteSubscriberList(listId)

const result = await client.deleteSubscriberList('list_abc');
// => { success: true, message: 'List deleted' }

addSubscriber(listId, subscriber)

await client.addSubscriber('list_abc', {
  email: '[email protected]',
  name: 'Jane Reader'
});

removeSubscriber(listId, email)

await client.removeSubscriber('list_abc', '[email protected]');

getSubscriber(listId, email)

const subscriber = await client.getSubscriber('list_abc', '[email protected]');
// => { email: '[email protected]', name: 'Jane Reader', status: 'active', subscribedAt: '2025-01-10T...' }
// Returns null if the subscriber is not found on the list.

Unsubscribe

generateUnsubscribeUrl(email, listId)

Generate a secure, tokenized unsubscribe URL for inclusion in email footers.

const { url } = await client.generateUnsubscribeUrl('[email protected]', 'list_abc');
// => { url: 'https://api.example.com/unsubscribe?token=eyJhbGciOi...' }

processUnsubscribe(token)

Process an unsubscribe request using the token extracted from the URL.

await client.processUnsubscribe('eyJhbGciOi...');
// Removes the subscriber from the associated list. No return value.

Suppression Management

The suppression list prevents emails from being sent to specific addresses, regardless of their subscription status. Suppressions can be scoped globally or to a specific list.

getSuppressions(options?)

const result = await client.getSuppressions({ reason: 'hard_bounce' });
// => {
//   items: [
//     { email: '[email protected]', reason: 'hard_bounce', scope: 'global', createdAt: '2025-01-05T...' }
//   ],
//   total: 1
// }

addSuppression(email, reason, scope?)

await client.addSuppression('[email protected]', 'complaint', 'global');

removeSuppression(email, scope?)

const result = await client.removeSuppression('[email protected]', 'global');
// => { success: true }

isSuppressed(email, listId?)

Check whether an email address is on the suppression list before attempting to send.

const suppressed = await client.isSuppressed('[email protected]');
// => true | false

const suppressedOnList = await client.isSuppressed('[email protected]', 'list_abc');
// => true | false

Webhook Processing

processWebhookEvent(provider, payload)

Forward incoming webhook payloads from email providers to the backend for processing. The backend will normalize the provider-specific format and update delivery statuses.

// SendGrid webhook
await client.processWebhookEvent('sendgrid', [
  { event: 'delivered', email: '[email protected]', timestamp: 1706000000, sg_message_id: 'msg_123' },
  { event: 'open', email: '[email protected]', timestamp: 1706000060, sg_message_id: 'msg_123' }
]);

// Mailgun webhook
await client.processWebhookEvent('mailgun', {
  signature: { timestamp: '1706000000', token: 'abc', signature: 'def' },
  'event-data': { event: 'delivered', recipient: '[email protected]' }
});

// Postmark webhook
await client.processWebhookEvent('postmark', {
  RecordType: 'Delivery',
  MessageID: 'msg_456',
  Recipient: '[email protected]',
  DeliveredAt: '2025-01-23T12:00:00Z'
});

Capability Matrix

The client package exports types for working with the capability matrix and routing metadata that the backend includes in API responses.

Capability Types

import type {
  ClientCapabilitySource,    // 'provider' | 'platform' | 'enhanced'
  ClientCapabilityEntry,     // Individual capability entry with source, description, limitations
  ClientDegradationInfo,     // Degradation strategy and description
  ClientRoutingMetadata,     // Routing info attached to SendResult
  ClientProviderName,        // 'sendgrid' | 'mailgun' | 'postmark' | 'ses' | 'smtp'
  ClientFeatureName,         // String key from the capability matrix
} from '@bernierllc/email-manager-client';

ClientCapabilityEntry

Describes how a specific provider supports a feature:

interface ClientCapabilityEntry {
  source: ClientCapabilitySource;   // 'provider' | 'platform' | 'enhanced'
  description: string;               // Human-readable description
  limitations?: string;              // Known limitations
  degradation?: ClientDegradationInfo | null;
  overridable: boolean;              // Whether this entry can be overridden at runtime
}

interface ClientDegradationInfo {
  strategy: string;       // e.g., 'platform-managed', 'sequential-batch'
  description: string;    // What the degradation means
  docUrl?: string;        // Link to degradation documentation
}

RoutingMetadata in HTTP Responses

When the backend has capability-aware routing enabled, the SendResult returned from sendEmail() includes a routing field:

const result = await client.sendEmail({
  to: '[email protected]',
  subject: 'Hello',
  html: '<p>Hello</p>',
});

if (result.routing) {
  console.log('Feature:', result.routing.feature);
  console.log('Provider:', result.routing.selectedProvider);
  console.log('Source:', result.routing.source);        // 'provider' | 'platform' | 'enhanced'
  console.log('Degraded:', result.routing.degraded);    // true if platform polyfill was used
  console.log('Alternatives:', result.routing.alternativeProviders);
  console.log('Resolved at:', result.routing.resolvedAt);

  if (result.routing.degradation) {
    console.log('Degradation strategy:', result.routing.degradation.strategy);
    console.log('Degradation detail:', result.routing.degradation.description);
  }
}

ClientRoutingMetadata Interface

interface ClientRoutingMetadata {
  feature: string;                         // The feature that was routed (e.g., 'sendEmail')
  selectedProvider: string;                // Provider ID that handled the request
  source: ClientCapabilitySource;          // How the provider supports this feature
  degraded: boolean;                       // Whether the feature was degraded
  degradation?: ClientDegradationInfo | null;
  alternativeProviders: string[];          // Other providers that could handle this feature
  resolvedAt: string;                      // ISO date string of when routing was resolved
}

The routing field is only present when the backend has routing enabled. If the backend does not use capability-aware routing, result.routing will be undefined.


Error Handling

The client uses a hierarchy of typed error classes with ES2022 Error.cause support for full error chain preservation.

Error Classes

| Class | Code | Retryable | Description | |-------|------|-----------|-------------| | EmailManagerClientError | varies | varies | Base error for all API errors | | NetworkError | NETWORK_ERROR | yes | Network connectivity failure | | TimeoutError | TIMEOUT_ERROR | yes | Request exceeded timeout | | RateLimitError | RATE_LIMITED | yes | Too many requests (429) | | UnauthorizedError | UNAUTHORIZED | no | Invalid or missing API key (401) | | NotFoundError | NOT_FOUND | no | Resource does not exist (404) |

Error Codes

All errors carry a machine-readable code property:

  • NETWORK_ERROR - Network failure
  • TIMEOUT_ERROR - Request timed out
  • API_ERROR - General API error
  • BAD_REQUEST - Invalid request (400)
  • UNAUTHORIZED - Authentication failure (401)
  • FORBIDDEN - Insufficient permissions (403)
  • NOT_FOUND - Resource not found (404)
  • RATE_LIMITED - Rate limit exceeded (429)
  • SERVER_ERROR - Server-side error (5xx)

Usage

import {
  EmailManagerClient,
  EmailManagerClientError,
  NetworkError,
  TimeoutError,
  RateLimitError,
  NotFoundError
} from '@bernierllc/email-manager-client';

try {
  await client.sendEmail(emailData);
} catch (error) {
  if (error instanceof EmailManagerClientError) {
    console.error('Code:', error.code);         // e.g. 'RATE_LIMITED'
    console.error('Status:', error.statusCode);  // e.g. 429
    console.error('Context:', error.context);    // Additional details

    // Check retryability
    if (error.isRetryable()) {
      const retryAfter = error.getRetryAfter(); // Suggested delay in ms
      console.log(`Retry after ${retryAfter}ms`);
    }

    // Inspect error chain
    if (error.cause) {
      console.error('Caused by:', (error.cause as Error).message);
    }
  }
}

Retry Pattern

async function sendWithRetry(client: EmailManagerClient, email: EmailData, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await client.sendEmail(email);
    } catch (error) {
      if (error instanceof EmailManagerClientError && error.isRetryable() && attempt < maxRetries) {
        const delay = error.getRetryAfter() ?? (1000 * Math.pow(2, attempt));
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      throw error;
    }
  }
}

TypeScript Types

All types are exported for use in your application:

import type {
  // Configuration
  EmailManagerClientConfig,

  // Email
  EmailData,
  SendResult,
  ScheduleResult,
  ScheduledEmail,
  CancelResult,

  // Templates
  EmailTemplate,
  TemplateResult,
  TemplateList,
  ValidationResult,

  // Providers
  EmailProvider,
  ProviderResult,
  ProviderList,
  ProviderStatus,
  TestResult,

  // Analytics
  EmailStats,
  DeliveryReport,
  BounceReport,
  ReportOptions,
  ListOptions,
  DeleteResult,

  // Calendar
  ClientCalendarEvent,
  CalendarInviteResult,
  CalendarCancelResult,

  // Batch
  ClientBatchSendOptions,
  ClientBatchResult,

  // Subscriptions
  ClientSubscriberList,
  ClientSubscriber,
  ClientSuppression,

  // Errors
  EmailManagerClientErrorOptions,
  EmailManagerClientErrorCodeType,
} from '@bernierllc/email-manager-client';

Browser Compatibility

This package uses the native fetch API, which is available in:

  • Modern browsers (Chrome, Firefox, Safari, Edge)
  • Node.js 18+ (with native fetch)
  • For older environments, use a fetch polyfill

Differences from email-manager

  1. No Node.js dependencies - Can be bundled for browser
  2. HTTP-based - Makes API calls instead of direct function calls
  3. Date handling - Uses ISO date strings instead of Date objects in some places
  4. Attachments - Uses base64 strings instead of Buffer objects
  5. Requires backend - Needs a server running email-manager

Examples

Runnable example files are available in the examples/ directory:

License

Bernier LLC - Restricted License