Back to blog
DevelopmentTestingMailtrap AlternativeMailhog AlternativeMailcatcher AlternativeEmail Sandbox

Mailhog Alternative: Local Email Testing Without Docker

npx @sendpigeon-sdk/cli dev - one command, no Docker, no config. Catch emails locally with Node.js, Python, PHP, Go, or any SMTP client.

SendPigeon TeamDecember 28, 20256 min read

Every developer has a horror story. You're testing a feature locally, hit send, and realize you just emailed 500 real users from your dev environment.

This guide shows you how to catch all emails locally during development, so production inboxes stay safe.

TL;DR

No account needed. Works with any language or framework.

npx @sendpigeon-sdk/cli dev

Point your app's SMTP to localhost:4125. View caught emails at localhost:4100.

SendPigeon SDK users: set SENDPIGEON_DEV=true for automatic routing.See the tool page for quick setup.

The Problem

During development, you need to test email flows:

  • Password resets
  • Welcome emails
  • Order confirmations
  • Notifications

But sending real emails from localhost is risky:

  • Accidental sends - One wrong database seed and you email everyone
  • Rate limits - Burning through your quota on test emails
  • Deliverability - Test emails can hurt your sender reputation
  • Slow feedback - Waiting for emails to arrive, checking spam folders

The Solution

Catch emails locally instead of sending them to real inboxes. The email never leaves your machine - it's captured and displayed in a web UI.

There are several tools for this (Mailhog, Mailcatcher, Mailtrap, and others). SendPigeon Dev is our take on it:

npx @sendpigeon-sdk/cli dev

What you get:

  • SMTP server on port 4125 - works with any language or framework
  • Web UI at localhost:4100 - see all caught emails instantly
  • No install - runs via npx, nothing to set up
  • Works offline - no account or internet needed
Your App → SMTP (localhost:4125) → Web UI
                    ↓
           (no real email sent)

Setup

1. Start the Dev Server

In one terminal:

npx @sendpigeon-sdk/cli dev

Output:

SendPigeon Dev Server

  API: http://localhost:4100/v1/emails
  UI:  http://localhost:4100

2. Run Your App in Dev Mode

In another terminal, set SENDPIGEON_DEV=true:

SENDPIGEON_DEV=true npm run dev

The SDK automatically routes to localhost:4100 instead of production.

3. Send an Email

Trigger any email in your app - signup, password reset, etc.

4. View in the UI

Open http://localhost:4100. Your email appears instantly:

  • See HTML and plain text versions
  • Check subject, from, to addresses
  • No waiting for delivery
  • No checking spam folders

Using SMTP (Any Language/Framework)

The dev server also runs an SMTP server on port 4125 - just like Mailhog or Mailcatcher. This works with any language or framework that can send SMTP emails.

npx @sendpigeon-sdk/cli dev

Output:

SendPigeon Dev Server

  API:  http://localhost:4100/v1/emails
  SMTP: localhost:4125
  UI:   http://localhost:4100

Point your app's SMTP config to localhost:4125 and emails get caught automatically.

Node.js (Nodemailer)

import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: "localhost",
  port: 4125,
  secure: false,
});

await transporter.sendMail({
  from: "app@example.com",
  to: "user@example.com",
  subject: "Test email",
  html: "<p>Hello from Nodemailer!</p>",
});

Python

import smtplib
from email.mime.text import MIMEText

msg = MIMEText("<p>Hello from Python!</p>", "html")
msg["Subject"] = "Test email"
msg["From"] = "app@example.com"
msg["To"] = "user@example.com"

with smtplib.SMTP("localhost", 4125) as server:
    server.send_message(msg)

Ruby (Rails)

# config/environments/development.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address: "localhost",
  port: 4125
}

PHP

<?php
// Using PHPMailer
$mail = new PHPMailer();
$mail->isSMTP();
$mail->Host = 'localhost';
$mail->Port = 4125;
$mail->SMTPAuth = false;

$mail->setFrom('app@example.com');
$mail->addAddress('user@example.com');
$mail->Subject = 'Test email';
$mail->Body = '<p>Hello from PHP!</p>';

$mail->send();

Go

package main

import (
    "net/smtp"
)

func main() {
    msg := []byte("Subject: Test email\r\n\r\nHello from Go!")
    smtp.SendMail("localhost:4125", nil, "app@example.com",
        []string{"user@example.com"}, msg)
}

All emails appear instantly at http://localhost:4100 - no signup, no config files, no Docker.


How It Works (SDK Integration)

When SENDPIGEON_DEV=true is set, the SDK changes its behavior:

// Normal mode
const client = new SendPigeon("sp_live_xxx");
// → sends to api.sendpigeon.dev

// Dev mode (SENDPIGEON_DEV=true)
const client = new SendPigeon("sp_live_xxx");
// → sends to localhost:4100

You'll see a log confirming dev mode:

[SendPigeon] Dev mode → http://localhost:4100

Your code stays the same. No conditional logic needed.


Package.json Scripts

For teams, add scripts to your package.json:

{
  "scripts": {
    "dev": "next dev",
    "dev:email": "sendpigeon dev",
    "dev:full": "concurrently \"npm run dev:email\" \"SENDPIGEON_DEV=true npm run dev\""
  },
  "devDependencies": {
    "@sendpigeon-sdk/cli": "^1.0.0",
    "concurrently": "^8.0.0"
  }
}

Now npm run dev:full starts everything at once.


CI/CD Considerations

In CI, you might want to:

  1. Skip emails entirely - Set a flag to disable email sending in tests
  2. Use the dev server - Start it in CI, run tests, verify emails were "sent"
  3. Mock at the SDK level - For unit tests, mock the SendPigeon client

Example test setup:

// vitest.setup.ts
import { vi } from "vitest";

vi.mock("sendpigeon", () => ({
  SendPigeon: vi.fn().mockImplementation(() => ({
    send: vi.fn().mockResolvedValue({ data: { id: "test_123" }, error: null }),
  })),
}));

Best Practices

Use Environment Variables

# .env.local (development)
SENDPIGEON_DEV=true
SENDPIGEON_API_KEY=sp_test_xxx

# .env.production (never set SENDPIGEON_DEV)
SENDPIGEON_API_KEY=sp_live_xxx

Separate Test and Live Keys

Even in dev mode, use a test API key (sp_test_xxx). Test keys disconnect from AWS SES entirely - no real emails can be sent, even if SENDPIGEON_DEV accidentally gets unset.

Clear Emails Regularly

The dev server stores up to 100 emails in memory. Click "Clear All" when testing fresh flows.


Troubleshooting

Emails not appearing?

  1. Check SENDPIGEON_DEV=true is set
  2. Look for [SendPigeon] Dev mode in your app's logs
  3. Verify dev server is running on port 4100

Port already in use?

npx @sendpigeon-sdk/cli dev --port 4200

Then set baseUrl explicitly:

const client = new SendPigeon("sp_test_xxx", {
  baseUrl: "http://localhost:4200",
});

Next Steps


Why SendPigeon Dev?

If you've used Mailhog, Mailcatcher, or Mailtrap before - this works the same way. The difference:

  • One command - npx @sendpigeon-sdk/cli dev, nothing to install
  • Works with any stack - SMTP on port 4125, same as other tools
  • SendPigeon SDK bonus - set SENDPIGEON_DEV=true and your code routes automatically

Summary

Local email testing is essential for safe development:

Without Dev ServerWith Dev Server
Risk of accidental sendsEmails caught locally
Burns API quotaZero API calls
Slow feedback loopInstant preview
Check spam foldersAll in one UI

One command to start, one env var to enable. No more accidentally emailing real users from localhost.

npx @sendpigeon-sdk/cli dev