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('user@example.com').send(new WelcomeEmail(user));
Mail.assertSent(WelcomeEmail, (mail) => mail.hasTo('user@example.com'));| 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 | ✅ | ✅ | ✅ | ✅ |
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 supportimport { Mail } from 'laramail';
// 1. Configure once
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 },
},
},
});
// 2. Send emails
await Mail.to('user@example.com')
.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: '...' },
}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('user@example.com').send(new WelcomeEmail(user));Test emails without sending — Laravel-style assertions:
beforeEach(() => Mail.fake());
afterEach(() => Mail.restore());
it('sends welcome email', async () => {
await Mail.to('user@example.com').send(new WelcomeEmail('John'));
Mail.assertSent(WelcomeEmail);
Mail.assertSent(WelcomeEmail, (mail) =>
mail.hasTo('user@example.com') && 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
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('user@example.com')
.subject('Welcome!')
.template('welcome')
.data({ name: 'John', appName: 'My App' })
.send();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 support@example.com[/panel]
`, { name: this.user.name });
}
}Components: [button url="..." color="primary|success|error"], [panel]...[/panel], [table]...[/table]
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}`),
},
});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('user@example.com').queue(new WelcomeEmail(user)); // Immediate
await Mail.to('user@example.com').later(60, new WelcomeEmail(user)); // 60s delay
await Mail.to('user@example.com').at(scheduledDate, new WelcomeEmail(user)); // Scheduled
await Mail.processQueue(); // WorkerHook 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}`));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.
Use the log driver during development — emails are printed to console instead of sent:
Mail.configure({
default: 'log',
from: { address: 'dev@example.com', name: 'Dev' },
mailers: {
log: { driver: 'log' },
},
});
await Mail.to('user@example.com').subject('Test').html('<p>Hi</p>').send();
// Prints formatted email to console — no SMTP neededRegister 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: 'noreply@example.com', name: 'App' },
mailers: { api: { driver: 'custom-api' } },
});Redirect all emails to a single address — perfect for staging environments:
Mail.alwaysTo('dev-team@example.com');
// All emails now go to dev-team@example.com, CC/BCC cleared
// Call Mail.alwaysTo(undefined) to disable
// Or via config:
Mail.configure({
// ...
alwaysTo: 'dev-team@example.com',
});Preview rendered emails without sending:
const preview = await Mail.to('user@example.com')
.subject('Hello')
.html('<p>Hi</p>')
.priority('high')
.preview();
console.log(preview.html, preview.headers);await Mail.to('user@example.com')
.subject('Complete Example')
.html('<h1>Hello!</h1>')
.text('Hello!')
.from('custom@example.com')
.cc(['manager@example.com'])
.bcc('archive@example.com')
.replyTo('support@example.com')
.attachments([{ filename: 'report.pdf', path: './report.pdf' }])
.priority('high')
.headers({ 'X-Custom': 'value' })
.send();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 you@example.com
npx laramail make:mailable WelcomeEmail
npx laramail make:mailable NewsletterEmail --markdown
npx laramail config:check # Validate configuration
npx laramail config:check --test # Test provider connections// laramail.config.ts
import { defineConfig } from 'laramail';
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 },
},
},
});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.
git clone https://github.com/impruthvi/laramail.git
cd laramail
npm install
npm run build
npm testSee CONTRIBUTING.md for guidelines.
MIT © Pruthvi
If laramail helps you, give it a star! It helps others discover the project.