Skip to content

impruthvi/laramail

Repository files navigation

laramail

npm version License: MIT TypeScript Tests Coverage Documentation

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'));

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 laramail

Add 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 support

Quick Start

import { 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: '...' },
}

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('user@example.com').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('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

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('user@example.com')
  .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 support@example.com[/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('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();                                                 // Worker

Email 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: '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 needed

Custom 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: 'noreply@example.com', name: 'App' },
  mailers: { api: { driver: 'custom-api' } },
});

Staging Redirect (alwaysTo)

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',
});

Email Preview

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);

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'])
  .bcc('archive@example.com')
  .replyTo('support@example.com')
  .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 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

Configuration File

// 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 },
    },
  },
});

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 test

See CONTRIBUTING.md for guidelines.

License

MIT © Pruthvi

Support


If laramail helps you, give it a star! It helps others discover the project.

About

A unified mail service for Node.js/TypeScript with Laravel-like simplicity. Supports SMTP, SendGrid, AWS SES, Mailgun, Resend, Postmark, templates, queues, and provider failover — just change env vars, no code changes needed.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages