@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-clientQuick 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:
- Import and use
@bernierllc/email-manager(Node.js-only) - Expose HTTP endpoints that match the client API
- 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 | falseWebhook 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 failureTIMEOUT_ERROR- Request timed outAPI_ERROR- General API errorBAD_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
- No Node.js dependencies - Can be bundled for browser
- HTTP-based - Makes API calls instead of direct function calls
- Date handling - Uses ISO date strings instead of Date objects in some places
- Attachments - Uses base64 strings instead of Buffer objects
- Requires backend - Needs a server running email-manager
Examples
Runnable example files are available in the examples/ directory:
calendar-workflow.ts- Send invites and cancel eventsbatch-operations.ts- Batch send with failure strategiessubscription-lifecycle.ts- Lists, subscribers, suppressions, unsubscribewebhook-handler.ts- Process webhook events from providers
License
Bernier LLC - Restricted License
