@impruthvi/nodemail

@impruthvi/nodemail brings the simplicity and elegance of Laravel's Mail system to the Node.js ecosystem with full TypeScript support.
🎯 Vision
A lightweight, developer-friendly email library where you can:
- Switch email providers by just changing environment variables
- Use elegant, class-based Mailable patterns
- Keep your package lightweight (install only what you need)
- Write clean, maintainable email code
Inspired by Laravel's Mail system.
✨ Features
✅ Available Now (v1.1.0)
- 🎯 Multiple Providers - SMTP (Nodemailer), SendGrid, AWS SES, Mailgun, Resend, Postmark
- 🎨 Template Engines - Handlebars, EJS, Pug support with dynamic loading
- 📝 Mailable Classes - Reusable email definitions with template support
- 📋 Markdown Mail - Write emails in Markdown with components (button, panel, table)
- 📦 Queue Support - Background email sending with Bull/BullMQ
- 🔄 Provider Failover - Automatic failover chain with retries, delays, and callbacks
- 🔔 Email Events -
sending, sent, failed lifecycle hooks for logging, analytics, and cancellation
- 🔍 Email Preview - Preview rendered emails without sending (debug templates, verify headers)
- ⏱️ Rate Limiting - Per-provider rate limiting with sliding window algorithm
- 🧪 Testing Utilities - Mail::fake() for testing (Laravel-style assertions)
- 🎨 CLI Tools - Queue management, email preview, code generation, config validation
- 🪶 Lightweight - Only ~25MB with SMTP, install additional providers as needed
- 🔒 Type-Safe - Full TypeScript support with strict typing
- ✨ Complete Fluent API - Chain to(), subject(), html(), template(), data(), cc(), bcc(), attachments(), headers()
- ⚡ Dynamic Loading - Providers and templates loaded only when installed (peerDependencies)
- 🛡️ Error Handling - Graceful degradation with helpful error messages
🚧 Coming Soon
- 🔔 Notifications - Multi-channel notification system
- 🌍 i18n Support - Multi-language emails
- 🚀 More Providers - Mailtrap and others
📦 Installation
npm install @impruthvi/nodemail
Or install a specific version:
npm install @impruthvi/nodemail@1.1.0
Lightweight by default! Only includes SMTP support (~25MB).
Adding Providers (Optional)
Currently Supported:
npm install @sendgrid/mail
npm install @aws-sdk/client-ses
npm install mailgun.js form-data
npm install resend
npm install postmark
Adding Template Engines (Optional)
Currently Supported:
npm install handlebars
npm install ejs
npm install pug
Adding Markdown Mail Support (Optional)
npm install marked juice
🚀 Quick Start
SMTP (Nodemailer)
import { Mail } from 'nodemail';
Mail.configure({
default: 'smtp',
from: {
address: 'noreply@example.com',
name: 'My App',
},
mailers: {
smtp: {
driver: 'smtp',
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
},
});
await Mail.to('user@example.com').subject('Welcome!').html('<h1>Hello World!</h1>').send();
SendGrid
import { Mail } from '@impruthvi/nodemail';
Mail.configure({
default: 'sendgrid',
from: { address: 'noreply@example.com', name: 'My App' },
mailers: {
sendgrid: {
driver: 'sendgrid',
apiKey: process.env.SENDGRID_API_KEY,
},
},
});
AWS SES
import { Mail } from '@impruthvi/nodemail';
Mail.configure({
default: 'ses',
from: { address: 'noreply@example.com', name: 'My App' },
mailers: {
ses: {
driver: 'ses',
region: 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
},
});
Mailgun
import { Mail } from '@impruthvi/nodemail';
Mail.configure({
default: 'mailgun',
from: { address: 'noreply@yourdomain.com', name: 'My App' },
mailers: {
mailgun: {
driver: 'mailgun',
domain: process.env.MAILGUN_DOMAIN,
apiKey: process.env.MAILGUN_API_KEY,
region: 'us',
},
},
});
Resend
import { Mail } from '@impruthvi/nodemail';
Mail.configure({
default: 'resend',
from: { address: 'noreply@yourdomain.com', name: 'My App' },
mailers: {
resend: {
driver: 'resend',
apiKey: process.env.RESEND_API_KEY,
},
},
});
Postmark
import { Mail } from '@impruthvi/nodemail';
Mail.configure({
default: 'postmark',
from: { address: 'noreply@yourdomain.com', name: 'My App' },
mailers: {
postmark: {
driver: 'postmark',
serverToken: process.env.POSTMARK_SERVER_TOKEN,
},
},
});
🎨 Template Engines
Using Handlebars
import { Mail } from '@impruthvi/nodemail';
Mail.configure({
default: 'smtp',
from: { address: 'noreply@example.com', name: 'My App' },
mailers: {
},
templates: {
engine: 'handlebars',
viewsPath: './views/emails',
extension: '.hbs',
cache: true,
},
});
await Mail.to('user@example.com')
.subject('Welcome!')
.template('welcome')
.data({ name: 'John', appName: 'My App' })
.send();
Template file (views/emails/welcome.hbs):
<h1>Welcome, {{name}}!</h1>
<p>Thank you for joining {{appName}}.</p>
Using EJS
Mail.configure({
templates: {
engine: 'ejs',
viewsPath: './views/emails',
extension: '.ejs',
},
});
await Mail.to('customer@example.com')
.subject('Your Invoice')
.template('invoice')
.data({ items: [...], total: 99.99 })
.send();
Using Pug
Mail.configure({
templates: {
engine: 'pug',
viewsPath: './views/emails',
cache: true,
},
});
await Mail.to('user@example.com')
.subject('Notification')
.template('notification')
.data({ title: 'Update', message: 'New features!' })
.send();
📋 Markdown Mail
Write beautiful emails in Markdown with built-in components. Requires npm install marked juice.
MarkdownMailable
import { MarkdownMailable, Mail } from '@impruthvi/nodemail';
class WelcomeEmail extends MarkdownMailable {
constructor(
private user: { name: string },
private appName: string
) {
super();
}
build(): this {
return this.subject(`Welcome to ${this.appName}!`)
.from('noreply@example.com')
.markdown(
`# Welcome, {{name}}!
Thank you for joining **{{appName}}**.
[button url="https://example.com/start" color="primary"]Get Started[/button]
[panel]Need help? Contact support@example.com[/panel]`,
{
name: this.user.name,
appName: this.appName,
}
);
}
}
await Mail.to('user@example.com').send(new WelcomeEmail(user, 'My App'));
Components
Button - Call-to-action buttons with color variants:
[button url="https://example.com" color="primary"]Click Here[/button]
[button url="https://example.com" color="success"]Confirm[/button]
[button url="https://example.com" color="error"]Delete[/button]
Panel - Bordered callout sections:
[panel]
**Important:** This is a highlighted notice.
[/panel]
Table - Styled table wrapper:
[table]
| Name | Price |
|-------|--------|
| Item | $9.99 |
[/table]
Custom Themes
class BrandedEmail extends MarkdownMailable {
build(): this {
return this.subject('Update').markdown('# News\n\nLatest updates...').theme({
css: 'h1 { color: #e94560; } .button-primary { background: #e94560; }',
headerHtml: '<img src="https://example.com/logo.png" alt="Logo">',
footerHtml: '<p>© 2026 Company</p>',
});
}
}
Markdown Configuration
Mail.configure({
markdown: {
theme: {
css: '/* custom global CSS */',
headerHtml: '<img src="logo.png">',
footerHtml: '<p>Footer</p>',
},
customCss: '.button { border-radius: 8px; }',
},
});
📦 Queue Support
Send emails in the background with Bull or BullMQ. Requires npm install bullmq (or bull).
Mail.configure({
queue: {
driver: 'bullmq',
connection: { host: 'localhost', port: 6379 },
retries: 3,
backoff: { type: 'exponential', delay: 1000 },
},
});
await Mail.to('user@example.com').subject('Welcome!').html('<h1>Welcome!</h1>').queue();
await Mail.to('user@example.com').later(60, new WelcomeEmail(user));
await Mail.to('user@example.com').at(new Date('2026-12-25'), new ChristmasEmail());
await Mail.processQueue();
🎨 CLI Tools
The CLI provides commands for queue management, email preview, code generation, and configuration validation.
Quick Reference
npx nodemail queue:work
npx nodemail queue:status
npx nodemail queue:clear --status failed
npx nodemail queue:retry
npx nodemail preview --mailable ./src/mail/WelcomeEmail.ts
npx nodemail send:test --to you@example.com
npx nodemail make:mailable WelcomeEmail
npx nodemail make:mailable NewsletterEmail --markdown
npx nodemail config:check
npx nodemail config:check --test
Configuration File
Create a nodemail.config.ts in your project root:
import { defineConfig } from '@impruthvi/nodemail';
export default defineConfig({
default: 'smtp',
from: { address: 'noreply@example.com', name: 'My App' },
mailers: {
smtp: {
driver: 'smtp',
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
},
},
queue: {
driver: 'bullmq',
connection: { host: 'localhost', port: 6379 },
},
});
For styled terminal output, install chalk: npm install chalk
🔄 Provider Failover
Automatically fail over to backup providers when the primary provider fails. Supports retries, delays, and monitoring callbacks.
Global Failover Configuration
Mail.configure({
default: 'smtp',
from: { address: 'noreply@example.com', name: 'My App' },
mailers: {
smtp: {
driver: 'smtp',
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
},
sendgrid: {
driver: 'sendgrid',
apiKey: process.env.SENDGRID_API_KEY,
},
ses: {
driver: 'ses',
region: 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
},
failover: {
chain: ['sendgrid', 'ses'],
maxRetriesPerProvider: 2,
retryDelay: 1000,
failoverDelay: 500,
},
});
await Mail.to('user@example.com').subject('Hello!').html('<h1>Hello!</h1>').send();
Per-mailer Failover Override
Override the global failover config for a specific mailer:
Mail.configure({
default: 'smtp',
mailers: {
smtp: {
driver: 'smtp',
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
failover: {
chain: ['postmark'],
maxRetriesPerProvider: 3,
},
},
postmark: {
driver: 'postmark',
serverToken: process.env.POSTMARK_SERVER_TOKEN,
},
},
});
onFailover Callback
Monitor failover events for logging or alerting:
Mail.configure({
failover: {
chain: ['sendgrid', 'ses'],
onFailover: (event) => {
console.log(`Failover: ${event.failedMailer} → ${event.nextMailer}`);
console.log(`Error: ${event.error}`);
console.log(`Attempt: ${event.attemptIndex}, Time: ${event.timestamp}`);
},
},
});
Response Metadata
After sending, the response includes failover details:
const result = await Mail.to('user@example.com').subject('Hello!').html('<h1>Hello!</h1>').send();
console.log(result.provider);
console.log(result.failoverUsed);
console.log(result.failoverAttempts);
FailoverConfig Reference
chain | string[] | (required) | Ordered list of backup mailer names |
maxRetriesPerProvider | number | 1 | Retries per provider before moving to the next |
retryDelay | number | 0 | Delay (ms) between retries on the same provider |
failoverDelay | number | 0 | Delay (ms) before switching to the next provider |
onFailover | (event: FailoverEvent) => void | — | Callback fired on each failover transition |
🔔 Email Events
Hook into the email lifecycle for logging, analytics, or to modify/cancel emails before sending.
Mail.onSending((event) => {
console.log(`Sending to ${event.options.to} via ${event.mailer}`);
event.options.headers = { ...event.options.headers, 'X-Tracking': '123' };
});
Mail.onSent((event) => {
console.log(`Sent! ID: ${event.response.messageId}`);
});
Mail.onFailed((event) => {
console.error(`Failed: ${event.error}`);
});
Mail.onSending((event) => {
if (event.options.to === 'blocked@example.com') return false;
});
Mail.clearListeners();
Event Methods
Mail.onSending(listener) | Register listener before send (can cancel with return false, can mutate event.options) |
Mail.onSent(listener) | Register listener after successful send |
Mail.onFailed(listener) | Register listener on send failure |
Mail.clearListeners() | Remove all event listeners |
Events work in both real and fake mode. Listener errors are silently caught and never break email delivery. See docs/email-events.md for the full guide.
🔍 Email Preview
Preview emails without sending — useful for debugging templates, verifying headers, and building preview UIs.
const preview = await Mail.to('user@example.com')
.subject('Hello')
.html('<p>Hi</p>')
.priority('high')
.preview();
console.log(preview.html);
console.log(preview.headers);
const preview = await Mail.preview(new WelcomeMail().to('user@example.com'));
Preview applies full preprocessing (markdown, templates, priority headers) but fires no events and stores no messages. Works in both real and fake mode. See docs/email-preview.md for the full guide.
⏱️ Rate Limiting
Per-provider rate limiting to prevent exceeding email provider API limits. Uses an in-memory sliding window algorithm.
Mail.configure({
default: 'smtp',
from: { address: 'noreply@example.com', name: 'App' },
mailers: {
smtp: { driver: 'smtp', host: 'localhost', port: 587 },
},
rateLimit: { maxPerWindow: 100, windowMs: 60000 },
});
Mail.configure({
default: 'smtp',
from: { address: 'noreply@example.com', name: 'App' },
mailers: {
smtp: {
driver: 'smtp',
host: 'localhost',
port: 587,
rateLimit: { maxPerWindow: 10, windowMs: 1000 },
},
sendgrid: {
driver: 'sendgrid',
apiKey: '...',
rateLimit: { maxPerWindow: 100, windowMs: 1000 },
},
},
});
const result = await Mail.to('user@example.com').subject('Hi').html('<p>Hi</p>').send();
Mail.configure({
rateLimit: {
maxPerWindow: 10,
windowMs: 1000,
onRateLimited: (event) => {
console.log(`Rate limited on ${event.mailer}, retry in ${event.retryAfterMs}ms`);
},
},
});
Rate-limited sends don't fire sending/sent/failed events. See docs/rate-limiting.md for the full guide.
📨 Complete Fluent API
await Mail.to('user@example.com')
.subject('Complete Example')
.html('<h1>Hello!</h1>')
.text('Hello!')
.from('custom@example.com')
.cc(['manager@example.com', 'team@example.com'])
.bcc('archive@example.com')
.replyTo('support@example.com')
.attachments([
{ filename: 'report.pdf', path: './files/report.pdf' },
{ filename: 'image.png', content: buffer },
])
.priority('high')
.headers({ 'X-Custom-Header': 'value' })
.send();
📝 Mailable Classes
Create reusable email classes with Laravel-like syntax:
import { Mailable } from '@impruthvi/nodemail';
class WelcomeEmail extends Mailable {
constructor(
private user: { name: string; email: string },
private appName: string
) {
super();
}
build() {
return this.subject(`Welcome to ${this.appName}!`).view('welcome', {
name: this.user.name,
email: this.user.email,
appName: this.appName,
});
}
}
await Mail.to('user@example.com').send(new WelcomeEmail(user, 'My App'));
await new WelcomeEmail(user, 'My App').to('user@example.com').send();
🧪 Testing with Mail::fake()
Test your emails without actually sending them - just like Laravel's Mail::fake():
import { Mail, Mailable } from '@impruthvi/nodemail';
class WelcomeEmail extends Mailable {
constructor(public userName: string) {
super();
}
build() {
return this.subject(`Welcome, ${this.userName}!`).html(`<h1>Hello ${this.userName}!</h1>`);
}
}
describe('User Registration', () => {
beforeEach(() => {
Mail.fake();
});
afterEach(() => {
Mail.restore();
});
it('sends welcome email on registration', async () => {
await Mail.to('user@example.com').send(new WelcomeEmail('John'));
Mail.assertSent(WelcomeEmail);
Mail.assertSent(WelcomeEmail, (mail) => {
return mail.hasTo('user@example.com') && mail.subjectContains('Welcome');
});
Mail.assertSentCount(WelcomeEmail, 1);
Mail.assertNotSent(PasswordResetEmail);
});
it('does not send email when validation fails', async () => {
Mail.assertNothingSent();
});
});
Available Assertions
Mail.fake() | Enable fake mode (store emails instead of sending) |
Mail.restore() | Restore real mailer |
Mail.assertSent(Mailable) | Assert mailable was sent |
Mail.assertSent(Mailable, callback) | Assert with custom conditions |
Mail.assertSentCount(Mailable, count) | Assert sent exactly N times |
Mail.assertNotSent(Mailable) | Assert mailable was NOT sent |
Mail.assertNothingSent() | Assert no emails were sent |
Mail.assertQueued(Mailable) | Assert mailable was queued |
Mail.assertNothingQueued() | Assert nothing was queued |
Mail.sent() | Get all sent messages |
Mail.sent(Mailable) | Get sent messages of specific type |
Mail.hasSent() | Check if any messages were sent |
Mail.hasQueued() | Check if any messages were queued |
Mail.simulateFailures(n) | Simulate failures for the first N sends |
Mail.resetFailures() | Clear failure simulation state |
AssertableMessage Methods
When inspecting sent messages, you can use these helper methods:
const sent = Mail.sent(WelcomeEmail)[0];
sent.hasTo('user@example.com');
sent.hasCc('cc@example.com');
sent.hasBcc('bcc@example.com');
sent.hasSubject('Welcome!');
sent.subjectContains('Welcome');
sent.htmlContains('Hello');
sent.textContains('Hello');
sent.isMarkdown();
sent.getMarkdown();
sent.markdownContains('[button');
sent.wasFailoverUsed();
sent.getProvider();
sent.getFailoverAttempts();
sent.hasAttachments();
sent.hasAttachment('file.pdf');
sent.hasPriority('high');
sent.getPriority();
sent.hasHeader('X-Custom');
sent.hasHeader('X-Custom', 'value');
sent.getTo();
sent.getSubject();
sent.getHtml();
🛠️ Current Status
Phase 1: Project Setup ✅ Complete
- TypeScript 5.6 configuration
- ESLint 9 (flat config)
- Modern tooling setup
- Package structure
- Core type definitions
- Lightweight architecture (peerDependencies)
Phase 2: Core Implementation ✅ Complete
- ✅ Mail Manager & Facade
- ✅ SMTP Provider (nodemailer)
- ✅ SendGrid Provider (@sendgrid/mail)
- ✅ AWS SES Provider (@aws-sdk/client-ses)
- ✅ Message builder with complete fluent API
- ✅ Configuration system
- ✅ Error handling & graceful degradation
Phase 3: Additional Providers ✅ Complete
- ✅ Mailgun Provider (mailgun.js)
- ✅ Resend Provider (resend)
- ✅ Postmark Provider (postmark)
- ✅ Dynamic loading for all providers
- ✅ Comprehensive provider tests
Phase 4: Template Engines & Mailable ✅ Complete
- ✅ Template engines (Handlebars, EJS, Pug)
- ✅ Laravel-like Mailable classes with template support
- ✅ Complete fluent API (cc, bcc, replyTo, attachments, headers)
- ✅ Dynamic template loading with caching
Phase 5: Testing Utilities ✅ Complete (v0.5.0)
- ✅ Mail::fake() for testing
- ✅ assertSent(), assertNotSent(), assertNothingSent()
- ✅ assertQueued(), assertNothingQueued()
- ✅ AssertableMessage with inspection methods
Phase 6: Queue Management ✅ Complete (v0.6.0)
- ✅ QueueManager with Bull and BullMQ drivers
- ✅ Immediate, delayed, and scheduled sending
- ✅ Automatic retries with configurable backoff
- ✅ MailFake queue assertion support
Phase 7: Markdown Mail ✅ Complete (v0.7.0)
- ✅ MarkdownMailable base class
- ✅ MarkdownRenderer with CSS inlining
- ✅ Components: button, panel, table
- ✅ Default responsive email theme
- ✅ Custom themes and CSS support
- ✅ AssertableMessage markdown assertions
Phase 8: Provider Failover ✅ Complete (v1.0.0)
- ✅ FailoverManager with automatic provider chain
- ✅ Configurable retries per provider (
maxRetriesPerProvider)
- ✅ Retry and failover delays (
retryDelay, failoverDelay)
- ✅
onFailover callback for monitoring/logging
- ✅ Per-mailer failover overrides
- ✅ MailFake
simulateFailures() / resetFailures() for testing
- ✅ Response metadata:
provider, failoverUsed, failoverAttempts
- ✅ 269 passing tests
Post v1.0.0: Incremental Features ✅ Complete (v1.0.1)
- ✅ Embedded Images (CID support) for inline images in HTML
- ✅ Email Priority Levels (
high, normal, low) with X-Priority headers
- ✅ Email Events System (
sending, sent, failed lifecycle hooks)
- ✅ Email Preview — render emails without sending (debug templates, verify headers)
- ✅ Rate Limiting — per-provider sliding window algorithm
- ✅ 522 passing tests
Phase 9: Enhanced CLI ✅ Complete (v1.1.0)
- ✅ CLI tool with 8 commands for queue management, email preview, and code generation
- ✅
queue:work - Start processing queued emails with concurrency control
- ✅
queue:status - Show queue job counts
- ✅
queue:clear - Clear jobs by status (completed, failed, delayed, waiting)
- ✅
queue:retry - Retry failed jobs with optional limit
- ✅
preview - Preview email in browser without sending
- ✅
send:test - Send test email to verify configuration
- ✅
make:mailable - Generate Mailable class (with markdown/template options)
- ✅
config:check - Validate configuration with optional connection testing
- ✅
defineConfig helper for TypeScript autocomplete in config files
- ✅ Auto-detection of nodemail.config.ts/js config files
Phase 10+ 🚧 Coming Soon
- 🔔 Notifications - Multi-channel notification system
- 🌍 i18n Support - Multi-language emails
- 🚀 More Providers - Mailtrap and others
🤝 Contributing
This project is in early development. Contributions, ideas, and feedback are welcome!
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature)
- Commit your changes (
git commit -m 'Add some amazing feature')
- Push to the branch (
git push origin feature/amazing-feature)
- Open a Pull Request
Development Setup
git clone https://github.com/impruthvi/nodemail.git
cd nodemail
npm install
npm run build
npm run lint
npm run format
💡 Why nodemail?
Inspired by Laravel, Built for Node.js
If you've used Laravel's Mail system, you know how elegant it is:
Mail::to($user->email)->send(new WelcomeEmail($user));
@impruthvi/nodemail brings this same elegance to Node.js/TypeScript:
await Mail.to(user.email).send(new WelcomeEmail(user));
Lightweight by Design
Unlike other packages that bundle everything:
- Base package: ~25MB (SMTP only)
- Add providers as needed:
npm install @sendgrid/mail
- No bloat: Only install what you use
📊 Package Philosophy
- Modular: Install only the providers you need
- Type-Safe: Full TypeScript support with strict typing
- Developer-Friendly: Clean, intuitive API
- Production-Ready: Built with best practices
- Well-Tested: 269 passing tests with 85%+ coverage
📄 License
MIT © Pruthvi
🙏 Acknowledgments
Inspired by Laravel's Mail system - bringing elegant email handling to Node.js.
📦 Postinstall Message
After installation, nodemail displays a short thank-you message in your terminal. To suppress it, set one of these environment variables:
NODEMAIL_NO_POSTINSTALL=1 npm install @impruthvi/nodemail
⭐ If you like this idea, please star the repo! It helps gauge interest and motivates development.
🚀 Want to contribute? Check out the issues labeled good first issue or help wanted.