laramail
v1.4.3
Published
Laravel-style mailer for Node.js — Mail.fake() + Mail.assertSent() for zero-setup email testing, provider switching via env var, works with Express/Fastify/any framework.
Maintainers
Readme
laramail
AdonisJS mailer, but framework-agnostic — works with Express, Fastify, or any Node.js app.
Test emails without Mailtrap or mocking setup. Switch providers with one env var. Send with Mail.to(user).send(new WelcomeEmail()).
// Zero-setup email testing — no SMTP server, no network, no mocks.
Mail.fake();
await Mail.to('[email protected]').send(new WelcomeEmail(user));
Mail.assertSent(WelcomeEmail, (mail) => mail.hasTo('[email protected]'));How laramail Compares
| Feature | laramail | nodemailer | @sendgrid/mail | resend |
|---------|:--------:|:----------:|:--------------:|:------:|
| Mail.fake() + Mail.assertSent() | ✅ | ❌ | ❌ | ❌ |
| Mailable classes (OOP email objects) | ✅ | ❌ | ❌ | ❌ |
| Switch provider via MAIL_DRIVER env | ✅ | ❌ | ❌ | ❌ |
| Provider failover (auto chain) | ✅ | ❌ | ❌ | ❌ |
| Queue support (Bull / BullMQ) | ✅ | ❌ | ❌ | ❌ |
| Rate limiting (sliding window) | ✅ | ❌ | ❌ | ❌ |
| Staging redirect (Mail.alwaysTo()) | ✅ | ❌ | ❌ | ❌ |
| Works with Express, Fastify, any framework | ✅ | ✅ | ✅ | ✅ |
Installation
npm install laramailAdd providers and engines as needed — only install what you use:
npm install @sendgrid/mail # SendGrid
npm install @aws-sdk/client-ses # AWS SES
npm install mailgun.js form-data # Mailgun
npm install resend # Resend
npm install postmark # Postmark
npm install handlebars # Template engine
npm install ejs # Template engine
npm install pug # Template engine
npm install marked juice # Markdown emails
npm install bullmq # Queue supportQuick Start
import { Mail } from 'laramail';
// 1. Configure once
Mail.configure({
default: 'smtp',
from: { address: '[email protected]', 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 },
},
},
});
// 2. Send emails
await Mail.to('[email protected]')
.subject('Welcome!')
.html('<h1>Hello World!</h1>')
.send();Switch providers by changing the driver — no code changes:
mailers: {
sendgrid: { driver: 'sendgrid', apiKey: process.env.SENDGRID_API_KEY },
ses: { driver: 'ses', region: 'us-east-1', accessKeyId: '...', secretAccessKey: '...' },
mailgun: { driver: 'mailgun', domain: '...', apiKey: '...' },
resend: { driver: 'resend', apiKey: '...' },
postmark: { driver: 'postmark', serverToken: '...' },
}Mailable Classes
Create reusable, testable email classes — just like Laravel:
import { Mailable } from 'laramail';
class WelcomeEmail extends Mailable {
constructor(private user: { name: string }) {
super();
}
build() {
return this
.subject(`Welcome, ${this.user.name}!`)
.html(`<h1>Hello ${this.user.name}!</h1>`);
}
}
await Mail.to('[email protected]').send(new WelcomeEmail(user));Testing with Mail.fake()
Test emails without sending — Laravel-style assertions:
beforeEach(() => Mail.fake());
afterEach(() => Mail.restore());
it('sends welcome email', async () => {
await Mail.to('[email protected]').send(new WelcomeEmail('John'));
Mail.assertSent(WelcomeEmail);
Mail.assertSent(WelcomeEmail, (mail) =>
mail.hasTo('[email protected]') && mail.subjectContains('Welcome')
);
Mail.assertSentCount(WelcomeEmail, 1);
Mail.assertNotSent(PasswordResetEmail);
});Assertions:
| Method | What it checks |
|--------|---------------|
| Mail.assertSent(Klass) | Class was sent at least once |
| Mail.assertSent(Klass, fn) | Class was sent and fn returns true for at least one message |
| Mail.assertSentCount(Klass, n) | Class was sent exactly N times |
| Mail.assertNotSent(Klass) | Class was NOT sent |
| Mail.assertNothingSent() | No emails sent at all |
AssertableMessage helpers (use inside the fn callback):
| Method | What it checks |
|--------|---------------|
| hasTo(email) | Recipient address |
| hasCc(email) | CC recipient |
| hasBcc(email) | BCC recipient |
| hasSubject(subject) | Exact subject match |
| subjectContains(text) | Subject partial match (case-insensitive) |
| htmlContains(text) | HTML body content |
| textContains(text) | Plain text content |
| hasAttachment(filename) | Attachment by filename |
| hasPriority(level) | 'high', 'normal', or 'low' |
| hasHeader(name, value?) | Email header |
Full API including queue assertions → docs/testing.md
Template Engines
Use Handlebars, EJS, or Pug for email templates:
Mail.configure({
// ...mailer config
templates: {
engine: 'handlebars', // or 'ejs' or 'pug'
viewsPath: './views/emails',
cache: true,
},
});
await Mail.to('[email protected]')
.subject('Welcome!')
.template('welcome')
.data({ name: 'John', appName: 'My App' })
.send();Markdown Emails
Write emails in Markdown with built-in components:
import { MarkdownMailable } from 'laramail';
class WelcomeEmail extends MarkdownMailable {
build(): this {
return this.subject('Welcome!').markdown(`
# Hello, {{name}}!
Thanks for joining.
[button url="https://example.com" color="primary"]Get Started[/button]
[panel]Need help? Contact [email protected][/panel]
`, { name: this.user.name });
}
}Components: [button url="..." color="primary|success|error"], [panel]...[/panel], [table]...[/table]
Provider Failover
Automatic failover to backup providers with retries and monitoring:
Mail.configure({
default: 'smtp',
mailers: { smtp: { ... }, sendgrid: { ... }, ses: { ... } },
failover: {
chain: ['sendgrid', 'ses'],
maxRetriesPerProvider: 2,
retryDelay: 1000,
onFailover: (event) => console.log(`${event.failedMailer} → ${event.nextMailer}`),
},
});Queue Support
Background sending with Bull or BullMQ:
Mail.configure({
// ...mailer config
queue: {
driver: 'bullmq',
connection: { host: 'localhost', port: 6379 },
retries: 3,
backoff: { type: 'exponential', delay: 1000 },
},
});
await Mail.to('[email protected]').queue(new WelcomeEmail(user)); // Immediate
await Mail.to('[email protected]').later(60, new WelcomeEmail(user)); // 60s delay
await Mail.to('[email protected]').at(scheduledDate, new WelcomeEmail(user)); // Scheduled
await Mail.processQueue(); // WorkerEmail Events
Hook into the email lifecycle:
Mail.onSending((event) => {
console.log(`Sending to ${event.options.to}`);
event.options.headers = { ...event.options.headers, 'X-Tracking': '123' };
// return false to cancel
});
Mail.onSent((event) => console.log(`Sent! ID: ${event.response.messageId}`));
Mail.onFailed((event) => console.error(`Failed: ${event.error}`));Rate Limiting
Per-provider sliding window rate limiting:
Mail.configure({
// ...mailer config
rateLimit: { maxPerWindow: 100, windowMs: 60000 },
});
// Per-mailer override
mailers: {
smtp: {
driver: 'smtp', host: '...',
rateLimit: { maxPerWindow: 10, windowMs: 1000 },
},
}When exceeded, returns { success: false } — never throws.
Log Transport
Use the log driver during development — emails are printed to console instead of sent:
Mail.configure({
default: 'log',
from: { address: '[email protected]', name: 'Dev' },
mailers: {
log: { driver: 'log' },
},
});
await Mail.to('[email protected]').subject('Test').html('<p>Hi</p>').send();
// Prints formatted email to console — no SMTP neededCustom Providers
Register your own mail provider with Mail.extend():
import { Mail } from 'laramail';
Mail.extend('custom-api', (config) => ({
async send(options) {
const res = await fetch('https://api.example.com/send', {
method: 'POST',
body: JSON.stringify(options),
});
return { success: res.ok, messageId: (await res.json()).id };
},
}));
Mail.configure({
default: 'api',
from: { address: '[email protected]', name: 'App' },
mailers: { api: { driver: 'custom-api' } },
});Staging Redirect (alwaysTo)
Redirect all emails to a single address — perfect for staging environments:
Mail.alwaysTo('[email protected]');
// All emails now go to [email protected], CC/BCC cleared
// Call Mail.alwaysTo(undefined) to disable
// Or via config:
Mail.configure({
// ...
alwaysTo: '[email protected]',
});Email Preview
Preview rendered emails without sending:
const preview = await Mail.to('[email protected]')
.subject('Hello')
.html('<p>Hi</p>')
.priority('high')
.preview();
console.log(preview.html, preview.headers);Complete Fluent API
await Mail.to('[email protected]')
.subject('Complete Example')
.html('<h1>Hello!</h1>')
.text('Hello!')
.from('[email protected]')
.cc(['[email protected]'])
.bcc('[email protected]')
.replyTo('[email protected]')
.attachments([{ filename: 'report.pdf', path: './report.pdf' }])
.priority('high')
.headers({ 'X-Custom': 'value' })
.send();CLI Tools
npx laramail queue:work # Process queued emails
npx laramail queue:status # Show queue job counts
npx laramail queue:clear -s failed # Clear failed jobs
npx laramail queue:retry # Retry failed jobs
npx laramail preview --mailable ./src/mail/WelcomeEmail.ts
npx laramail send:test --to [email protected]
npx laramail make:mailable WelcomeEmail
npx laramail make:mailable NewsletterEmail --markdown
npx laramail config:check # Validate configuration
npx laramail config:check --test # Test provider connectionsConfiguration File
// laramail.config.ts
import { defineConfig } from 'laramail';
export default defineConfig({
default: 'smtp',
from: { address: '[email protected]', 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 },
},
},
});Why laramail?
If you've used Laravel's Mail, you know how elegant it is:
// Laravel (PHP)
Mail::to($user->email)->send(new WelcomeEmail($user));laramail brings this same elegance to Node.js:
// laramail (TypeScript)
await Mail.to(user.email).send(new WelcomeEmail(user));Lightweight by design — base package is ~25MB (SMTP only). Add providers as needed.
Contributing
git clone https://github.com/impruthvi/laramail.git
cd laramail
npm install
npm run build
npm testSee CONTRIBUTING.md for guidelines.
License
MIT © Pruthvi
Support
If laramail helps you, give it a star! It helps others discover the project.
