@bernierllc/email-sender
v5.2.0
Published
Email sending with provider-agnostic interface and SendGrid advanced features (templates, verified senders, rate limiting)
Readme
@bernierllc/email-sender
A core utility for basic email sending with provider-agnostic interface.
Features
- Provider-Agnostic: Support for multiple email providers (SMTP, SendGrid, SES, Mailgun, Postmark)
- Staging Email Safety: Prevent accidental email sends in non-production environments
- TypeScript: Full TypeScript support with strict typing
- Validation: Comprehensive email and configuration validation
- Attachments: Support for file attachments
- Batch Sending: Send multiple emails efficiently
- Error Handling: Robust error handling and reporting
- Testing: Comprehensive test coverage
Installation
npm install @bernierllc/email-senderNote: This package uses peer dependencies. You must install them separately:
# For SMTP provider
npm install nodemailer
# For AWS SES provider
npm install aws-sdkQuick Start
Basic Usage
import { EmailSender } from '@bernierllc/email-sender';
// Configure SMTP provider
const config = {
provider: 'smtp' as const,
host: 'smtp.gmail.com',
port: 587,
secure: false,
username: '[email protected]',
password: 'your-app-password',
fromEmail: '[email protected]',
fromName: 'Your App',
};
// Create email sender
const emailSender = new EmailSender(config);
// Send an email
const result = await emailSender.sendEmail({
toEmail: '[email protected]',
toName: 'John Doe',
subject: 'Welcome to our app!',
htmlContent: '<h1>Welcome!</h1><p>Thank you for joining us.</p>',
textContent: 'Welcome! Thank you for joining us.',
});
if (result.success) {
console.log('Email sent successfully:', result.messageId);
} else {
console.error('Failed to send email:', result.errorMessage);
}SendGrid Configuration
import { EmailSender } from '@bernierllc/email-sender';
const config = {
provider: 'sendgrid' as const,
apiKey: 'your-sendgrid-api-key',
fromEmail: '[email protected]',
fromName: 'Your App',
};
const emailSender = new EmailSender(config);AWS SES Configuration
import { EmailSender } from '@bernierllc/email-sender';
const config = {
provider: 'ses' as const,
apiKey: 'your-aws-access-key',
secretKey: 'your-aws-secret-key',
region: 'us-east-1',
fromEmail: '[email protected]',
fromName: 'Your App',
sandbox: false, // Set to true if in SES sandbox mode
};
const emailSender = new EmailSender(config);Mailgun Configuration
import { EmailSender } from '@bernierllc/email-sender';
const config = {
provider: 'mailgun' as const,
apiKey: 'your-mailgun-api-key',
domain: 'yourdomain.com',
region: 'us', // or 'eu' for European region
fromEmail: '[email protected]',
fromName: 'Your App',
};
const emailSender = new EmailSender(config);Postmark Configuration
import { EmailSender } from '@bernierllc/email-sender';
const config = {
provider: 'postmark' as const,
apiKey: 'your-postmark-api-key',
accountToken: 'your-postmark-account-token', // Optional, for template operations
fromEmail: '[email protected]',
fromName: 'Your App',
};
const emailSender = new EmailSender(config);API Reference
EmailSender
The main class for sending emails.
Constructor
new EmailSender(config: EmailProviderConfig)Methods
sendEmail(email: EmailMessage): Promise<SendResult>
Send a single email.
const result = await emailSender.sendEmail({
toEmail: '[email protected]',
subject: 'Test Email',
htmlContent: '<h1>Hello</h1>',
});sendBatch(emails: EmailMessage[]): Promise<SendResult[]>
Send multiple emails in a batch.
const results = await emailSender.sendBatch([
{ toEmail: '[email protected]', subject: 'Email 1', htmlContent: '<h1>Email 1</h1>' },
{ toEmail: '[email protected]', subject: 'Email 2', htmlContent: '<h1>Email 2</h1>' },
]);validateConfiguration(): EmailValidationResult
Validate the provider configuration.
const validation = emailSender.validateConfiguration();
if (!validation.isValid) {
console.error('Configuration errors:', validation.errors);
}validateEmailMessage(email: EmailMessage): EmailValidationResult
Validate an email message before sending.
const validation = emailSender.validateEmailMessage(email);
if (!validation.isValid) {
console.error('Email validation errors:', validation.errors);
}testConnection(): Promise<boolean>
Test the connection to the email provider.
const isConnected = await emailSender.testConnection();
if (isConnected) {
console.log('Connection successful');
} else {
console.error('Connection failed');
}getCapabilities(): EmailProviderCapabilities
Get the capabilities of the current provider.
const capabilities = emailSender.getCapabilities();
console.log('Supports attachments:', capabilities.supportsAttachments);
console.log('Max batch size:', capabilities.maxBatchSize);Types
EmailMessage
interface EmailMessage {
toEmail: string;
toName?: string;
fromEmail?: string;
fromName?: string;
subject: string;
htmlContent?: string;
textContent?: string;
replyTo?: string;
headers?: Record<string, string>;
attachments?: EmailAttachment[];
metadata?: Record<string, any>;
}EmailAttachment
interface EmailAttachment {
filename: string;
content: string; // Base64 encoded
contentType?: string;
disposition?: 'attachment' | 'inline';
}SendResult
interface SendResult {
success: boolean;
messageId?: string;
errorMessage?: string;
providerResponse?: any;
metadata?: Record<string, any>;
}EmailProviderConfig
interface EmailProviderConfig {
provider: 'sendgrid' | 'ses' | 'mailgun' | 'postmark' | 'smtp';
apiKey?: string;
region?: string;
endpoint?: string;
fromEmail: string;
fromName?: string;
replyTo?: string;
webhookUrl?: string;
webhookSecret?: string;
sandbox?: boolean;
// SMTP specific options
host?: string;
port?: number;
secure?: boolean;
username?: string;
password?: string;
}Provider Capabilities
Each provider exposes its capabilities through the getCapabilities() method. The capabilities interface includes both legacy boolean flags and an expanded features map for fine-grained capability inspection.
EmailProviderCapabilities Interface
interface EmailProviderCapabilities {
// Legacy boolean flags (always available)
supportsBatch: boolean;
supportsTemplates: boolean;
supportsWebhooks: boolean;
supportsAttachments: boolean;
supportsCustomHeaders: boolean;
supportsDeliveryTracking: boolean;
maxBatchSize: number;
maxAttachmentSize: number;
// Expanded feature map (optional, populated by capability-aware providers)
features?: Record<string, CapabilityEntry>;
}CapabilityEntry
Each entry in the features map describes how a specific feature is supported:
type CapabilitySource = 'provider' | 'platform' | 'enhanced' | 'unsupported';
interface CapabilityEntry {
source: CapabilitySource; // How the feature is implemented
description: string; // Human-readable description
limitations?: string; // Known limitations (e.g., "72hr max scheduling window")
degradation?: { // Present when platform polyfill has a degradation strategy
strategy: string;
description: string;
docUrl: string;
} | null;
overridable: boolean; // Whether the entry can be overridden at runtime
}Per-Provider Capability Summary
| Provider | Batch | Templates | Webhooks | Attachments | Custom Headers | Delivery Tracking | |----------|-------|-----------|----------|-------------|----------------|-------------------| | SendGrid | Yes (personalizations) | Yes (dynamic templates) | Yes (Event Webhook) | Yes | Yes | Yes | | Mailgun | Yes (recipient variables) | Yes (stored templates) | Yes | Yes | Yes | Yes | | Postmark | Yes (batch API) | Yes (server-side) | Yes | Yes | Yes | Yes | | SES | Yes (SendBulkEmail) | Yes (email templates) | Yes (via SNS) | Yes (raw email) | Yes | Yes (via SNS) | | SMTP | Sequential* | No server-side | No | Yes (MIME) | Yes | No |
*SMTP batch sending is handled sequentially by the platform, not natively by the provider.
Declaring Capabilities in Custom Providers
When implementing a custom provider, declare capabilities by returning the appropriate values from getCapabilities():
import { EmailProvider, EmailProviderCapabilities, CapabilityEntry } from '@bernierllc/email-sender';
class CustomProvider extends EmailProvider {
getCapabilities(): EmailProviderCapabilities {
return {
supportsBatch: true,
supportsTemplates: false,
supportsWebhooks: true,
supportsAttachments: true,
supportsCustomHeaders: true,
supportsDeliveryTracking: true,
maxBatchSize: 500,
maxAttachmentSize: 10 * 1024 * 1024, // 10 MB
// Optional: declare fine-grained features for capability-aware routing
features: {
sendEmail: {
source: 'provider',
description: 'Send email via custom API',
degradation: null,
overridable: false,
},
batchSend: {
source: 'provider',
description: 'Batch send via custom API bulk endpoint',
degradation: null,
overridable: true,
},
scheduledSend: {
source: 'unsupported',
description: 'Custom provider does not support scheduled send',
degradation: null,
overridable: false,
},
},
};
}
}When the features map is present, the @bernierllc/email-manager capability router uses it for intelligent provider selection. When absent, the router falls back to the static capability matrix.
Zod Validation
Both CapabilityEntry and DegradationInfo have Zod schemas for runtime validation:
import { capabilityEntrySchema, degradationInfoSchema } from '@bernierllc/email-sender';
const result = capabilityEntrySchema.safeParse(entry);
if (!result.success) {
console.error('Invalid capability entry:', result.error.issues);
}Staging Email Safety
Prevent accidental email sends in non-production environments by using the staging safety feature.
Staging with Whitelist (Block Non-Whitelisted)
import { EmailSender } from '@bernierllc/email-sender';
const emailSender = new EmailSender({
provider: 'smtp' as const,
host: 'smtp.example.com',
port: 587,
username: 'user',
password: 'pass',
fromEmail: '[email protected]',
stagingSafety: {
enabled: true,
environment: 'staging',
whitelist: ['[email protected]', '[email protected]', '[email protected]'],
blockNonWhitelisted: true,
logBlocked: true,
},
});
// This will send (whitelisted)
await emailSender.sendEmail({
toEmail: '[email protected]',
subject: 'Test Email',
htmlContent: '<p>This email will be sent</p>',
});
// This will be blocked (not whitelisted)
const result = await emailSender.sendEmail({
toEmail: '[email protected]',
subject: 'Test Email',
htmlContent: '<p>This email will be blocked</p>',
});
console.log(result.success); // false
console.log(result.errorMessage); // "Email blocked by safety manager: ..."Staging with Rewrite (Redirect to Test Email)
const emailSender = new EmailSender({
provider: 'smtp' as const,
host: 'smtp.example.com',
port: 587,
username: 'user',
password: 'pass',
fromEmail: '[email protected]',
stagingSafety: {
enabled: true,
environment: 'staging',
whitelist: ['[email protected]'], // Admin emails go through normally
rewriteTo: '[email protected]', // All other emails redirected here
blockNonWhitelisted: false,
logBlocked: true,
},
});
// This will be sent to [email protected] instead
await emailSender.sendEmail({
toEmail: '[email protected]',
subject: 'Test Email',
htmlContent: '<p>This will be sent to [email protected]</p>',
});Production (All Emails Allowed)
const emailSender = new EmailSender({
provider: 'smtp' as const,
host: 'smtp.example.com',
port: 587,
username: 'user',
password: 'pass',
fromEmail: '[email protected]',
stagingSafety: {
enabled: true,
environment: 'production',
whitelist: [], // Not used in production
blockNonWhitelisted: false,
logBlocked: false,
},
});
// All emails are allowed in production
await emailSender.sendEmail({
toEmail: '[email protected]',
subject: 'Production Email',
htmlContent: '<p>This email will be sent</p>',
});Check Recipient Safety Before Sending
const safetyResult = emailSender.checkRecipientSafety('[email protected]');
console.log('Safety check result:', safetyResult);
if (safetyResult && !safetyResult.allowed) {
console.log('Email would be blocked:', safetyResult.reason);
}Dynamic Configuration Based on Environment
const environment = process.env.NODE_ENV || 'development';
const emailSender = new EmailSender({
provider: 'smtp' as const,
host: 'smtp.example.com',
port: 587,
username: 'user',
password: 'pass',
fromEmail: '[email protected]',
stagingSafety: {
enabled: environment !== 'production',
environment: environment as 'development' | 'staging' | 'production',
whitelist: environment === 'staging'
? ['[email protected]', '[email protected]']
: [],
rewriteTo: environment === 'development' ? '[email protected]' : undefined,
blockNonWhitelisted: environment === 'staging',
logBlocked: true,
},
});
console.log('Current environment:', environment);
console.log('Safety enabled:', emailSender.isSafetyEnabled());
console.log('Safety config:', emailSender.getSafetyConfig());Examples
Send Email with Attachment
import { EmailSender } from '@bernierllc/email-sender';
import fs from 'fs';
const emailSender = new EmailSender(config);
const attachment = {
filename: 'report.pdf',
content: fs.readFileSync('report.pdf').toString('base64'),
contentType: 'application/pdf',
disposition: 'attachment' as const,
};
const result = await emailSender.sendEmail({
toEmail: '[email protected]',
subject: 'Monthly Report',
htmlContent: '<h1>Monthly Report</h1><p>Please find the attached report.</p>',
attachments: [attachment],
});Send Email with Custom Headers
const result = await emailSender.sendEmail({
toEmail: '[email protected]',
subject: 'Priority Message',
htmlContent: '<h1>Important Update</h1>',
headers: {
'X-Priority': '1',
'X-Custom-Header': 'custom-value',
},
});Batch Email with Different Content
const emails = [
{
toEmail: '[email protected]',
toName: 'User One',
subject: 'Personalized Email 1',
htmlContent: '<h1>Hello User One!</h1>',
},
{
toEmail: '[email protected]',
toName: 'User Two',
subject: 'Personalized Email 2',
htmlContent: '<h1>Hello User Two!</h1>',
},
];
const results = await emailSender.sendBatch(emails);Error Handling
try {
const result = await emailSender.sendEmail(email);
if (result.success) {
console.log('Email sent successfully');
} else {
console.error('Email failed:', result.errorMessage);
// Check if it's a validation error
const validation = emailSender.validateEmailMessage(email);
if (!validation.isValid) {
console.error('Validation errors:', validation.errors);
}
}
} catch (error) {
console.error('Unexpected error:', error);
}Provider Support
Currently Supported
- SMTP: Full support with nodemailer
- SendGrid: Full support with SendGrid API
- AWS SES: Full support with AWS SDK (requires
aws-sdkpeer dependency) - Mailgun: Full support with Mailgun API
- Postmark: Full support with Postmark API
Adding New Providers
To add a new provider, extend the EmailProvider base class:
import { EmailProvider, EmailMessage, SendResult } from '@bernierllc/email-sender';
export class CustomProvider extends EmailProvider {
async sendEmail(email: EmailMessage): Promise<SendResult> {
// Implement your provider logic here
}
async sendBatch(emails: EmailMessage[]): Promise<SendResult[]> {
// Implement batch sending logic
}
async getDeliveryStatus(messageId: string): Promise<DeliveryStatus | null> {
// Implement delivery status checking
}
async processWebhook(payload: any, signature: string): Promise<WebhookEvent[]> {
// Implement webhook processing
}
}Testing
Run the test suite:
npm testRun tests with coverage:
npm run test:coverageContributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
Integration Status
- Logger integration: not-applicable - Email-sender is a low-level utility that doesn't require @bernierllc/logger integration. Consumer applications should handle logging as needed.
- Docs-Suite: ready - Complete TypeDoc/JSDoc documentation available. Markdown-based README with extensive examples.
- NeverHub integration: not-applicable - Email-sender is a stateless utility focused on email delivery. @bernierllc/neverhub-adapter is not needed for this use case.
Graceful Degradation
This package is designed to work standalone without external dependencies beyond the configured email provider. All functionality is self-contained with clear error handling and validation.
License
ISC License - see LICENSE file for details.
Support
For support and questions, please open an issue on GitHub or contact Bernier LLC.
