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.
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.
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_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:
- Skip emails entirely - Set a flag to disable email sending in tests
- Use the dev server - Start it in CI, run tests, verify emails were "sent"
- 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?
- Check
SENDPIGEON_DEV=trueis set - Look for
[SendPigeon] Dev modein your app's logs - 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
- Read the SDK documentation for full API reference
- Set up email templates for cleaner code
- Configure webhooks for delivery tracking
- Compare local testing tools in 7 Best MailHog Alternatives for 2026
- Learn about email sandbox options for CI/CD and staging
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=trueand your code routes automatically
Summary
Local email testing is essential for safe development:
| Without Dev Server | With Dev Server |
|---|---|
| Risk of accidental sends | Emails caught locally |
| Burns API quota | Zero API calls |
| Slow feedback loop | Instant preview |
| Check spam folders | All 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