@brijesh575/email-core
v1.0.0
Published
Production-grade, multi-tenant email service with pluggable providers
Maintainers
Readme
@brijesh/email
A production-grade, multi-tenant email service for Node.js with pluggable providers, retry/fallback, rate limiting, and Handlebars templating.
Features
- Multi-provider — SMTP (Nodemailer), AWS SES, SendGrid, or any custom provider
- Multi-tenant — per-tenant provider, credentials, templates, and rate limits
- Layered config — global → environment → tenant → per-request overrides
- Provider fallback — automatic failover to a backup provider
- Retry with backoff — configurable exponential retry on failure
- In-memory rate limiting — per-tenant abuse prevention
- Template engine — Handlebars with compile caching
- Hooks/middleware —
beforeSend,afterSend,onError - Preview mode — log emails in development instead of sending
- Health check —
emailClient.healthCheck()for monitoring - Pluggable logger — default console logger with secret masking
- ESM + CJS — dual output with full TypeScript types
- Zero lock-in — optional peer deps for SES and SendGrid
Installation
npm install @brijesh/emailOptional providers
# AWS SES
npm install @aws-sdk/client-ses
# SendGrid
npm install @sendgrid/mailQuick Start
import { EmailClient } from "@brijesh/email";
const client = new EmailClient({
defaultFrom: "[email protected]",
provider: {
type: "smtp",
smtp: {
host: "smtp.example.com",
port: 587,
auth: { user: "user", pass: "pass" },
},
},
});
await client.send({
to: "[email protected]",
subject: "Hello!",
text: "Welcome to our app.",
});
await client.destroy();Multi-Tenant Usage
const client = new EmailClient({
defaultFrom: "[email protected]",
provider: {
type: "smtp",
smtp: { host: "smtp.platform.com", port: 587, auth: { user: "u", pass: "p" } },
},
tenants: {
acme: {
provider: {
type: "ses",
ses: { region: "us-east-1", from: "[email protected]" },
},
from: "[email protected]",
templateDir: "./templates/acme",
rateLimit: { enabled: true, maxPerMinute: 100, maxPerHour: 5000 },
},
startup: {
provider: {
type: "sendgrid",
sendGrid: { apiKey: "SG.xxx", from: "[email protected]" },
},
from: "[email protected]",
},
},
});
// Sends via SES with Acme's credentials
await client.send({
tenantId: "acme",
to: "[email protected]",
subject: "Welcome",
template: "welcome",
data: { name: "John" },
});
// Register tenant at runtime
client.registerTenant("enterprise", {
provider: { type: "smtp", smtp: { host: "mail.ent.com", port: 465, secure: true, auth: { user: "a", pass: "b" } } },
from: "[email protected]",
});Provider Setup
SMTP
{
type: "smtp",
smtp: {
host: "smtp.example.com",
port: 587,
secure: false,
auth: { user: "user", pass: "pass" },
pool: true, // connection pooling
maxConnections: 5,
},
}AWS SES
Requires @aws-sdk/client-ses as a peer dependency.
{
type: "ses",
ses: {
region: "us-east-1",
accessKeyId: "AKIA...", // optional if using IAM roles
secretAccessKey: "secret...",
from: "[email protected]",
},
}SendGrid
Requires @sendgrid/mail as a peer dependency.
{
type: "sendgrid",
sendGrid: {
apiKey: "SG.xxxx",
from: "[email protected]",
},
}Custom Provider
import type { EmailProvider, NormalizedEmailOptions, SendResult } from "@brijesh/email";
class MyProvider implements EmailProvider {
readonly name = "my-provider";
async send(options: NormalizedEmailOptions): Promise<SendResult> {
// your logic
return {
messageId: "custom-id",
provider: this.name,
accepted: options.to,
rejected: [],
timestamp: new Date(),
};
}
}
// Use it
{
type: "custom",
custom: new MyProvider(),
}Templates (Handlebars)
Place .hbs files in your template directory:
templates/
welcome.hbs # HTML template
welcome.text.hbs # Plain text (optional)<!-- templates/welcome.hbs -->
<h1>Welcome, {{name}}!</h1>
<p>Thanks for joining {{company}}.</p>await client.send({
to: "[email protected]",
subject: "Welcome",
template: "welcome",
data: { name: "Alice", company: "Acme" },
});Attachments
await client.send({
to: "[email protected]",
subject: "Invoice",
text: "Please find attached.",
attachments: [
{ filename: "invoice.pdf", path: "/path/to/invoice.pdf" },
{ filename: "data.csv", content: Buffer.from("a,b,c\n1,2,3") },
],
});Provider Fallback
const client = new EmailClient({
defaultFrom: "[email protected]",
provider: { type: "ses", ses: { region: "us-east-1", from: "[email protected]" } },
fallbackProvider: { type: "smtp", smtp: { host: "smtp.backup.com", port: 587 } },
});If the primary provider fails after retries, the fallback provider is tried automatically.
Retry Configuration
{
retry: {
enabled: true,
maxAttempts: 3,
initialDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2,
},
}Rate Limiting
{
rateLimit: {
enabled: true,
maxPerMinute: 60,
maxPerHour: 1000,
},
}Throws RateLimitError when limits are exceeded.
Hooks
const client = new EmailClient({
// ...
hooks: {
beforeSend: [(options) => {
// Modify options, add tracking headers, etc.
return { ...options, headers: { ...options.headers, "X-Track": "abc" } };
}],
afterSend: [(result, options) => {
console.log(`Sent ${result.messageId} to ${options.to}`);
}],
onError: [(error, options) => {
console.error(`Failed to send to ${options.to}: ${error.message}`);
}],
},
});Preview Mode
In development, log emails instead of sending:
const client = new EmailClient({
preview: true,
defaultFrom: "dev@localhost",
provider: { type: "smtp", smtp: { host: "localhost", port: 1025 } },
});Health Check
const health = await client.healthCheck();
// { status: "healthy", providers: { ... }, timestamp: Date }Environment Variables
The library reads .env variables as fallback for SMTP when no provider is configured:
| Variable | Description |
|---|---|
| NODE_ENV | development, staging, production |
| SMTP_HOST | SMTP server host |
| SMTP_PORT | SMTP server port (default: 587) |
| SMTP_SECURE | Use TLS (true/false) |
| SMTP_USER | SMTP username |
| SMTP_PASS | SMTP password |
| SMTP_FROM | Default from address |
Error Handling
import { EmailError, ProviderError, ConfigError, ValidationError, RateLimitError } from "@brijesh/email";
try {
await client.send({ /* ... */ });
} catch (err) {
if (err instanceof ValidationError) {
console.log("Invalid input:", err.field);
} else if (err instanceof RateLimitError) {
console.log("Rate limited:", err.tenantId);
} else if (err instanceof ProviderError) {
console.log("Provider failed:", err.provider, err.cause);
}
}All errors use the standard cause chain — never leak credentials.
API Reference
new EmailClient(config?)
Creates a new client instance.
client.send(options)
Send an email. Returns Promise<SendResult>.
client.healthCheck()
Returns Promise<HealthCheckResult>.
client.registerTenant(id, config)
Register or update a tenant at runtime.
client.removeTenant(id)
Remove a tenant and clean up its resources.
client.destroy()
Gracefully shut down all providers and caches.
Build
npm run build # ESM + CJS + types via tsup
npm test # Vitest
npm run lint # TypeScript checkLicense
MIT
