Local Email Testing Without Mailtrap
Catch and preview emails locally with zero config. A free, offline alternative to Mailtrap, Mailhog, and mailcatcher.
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.
Quick setup:
- Use the SendPigeon SDK for sending emails in your app
npx @sendpigeon-sdk/cli devin one terminalSENDPIGEON_DEV=true npm run devin another- View emails at http://localhost:4100
No real emails sent. Ever.
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
Common Solutions
Cloud Sandboxes (Mailtrap, etc.)
Cloud-hosted email sandboxes with web UIs for inspecting caught emails.
Trade-offs:
- Requires internet connection
- Account signup and SMTP credential configuration
- Free tiers have email limits (e.g., 50/month)
- Paid plans for higher volumes
Self-Hosted (Mailhog, mailcatcher)
Run a local SMTP server that catches all emails.
Trade-offs:
- Requires Docker or binary installation
- SMTP configuration in your app (change host/port)
- Separate config from production code
- Works offline once set up
SDK-Integrated (SendPigeon Dev)
A different approach: the SDK routes to a local server automatically.
Benefits:
- One command to start:
npx @sendpigeon-sdk/cli dev - No code changes - just set an environment variable
- Same code path as production
- Works offline, no account needed
The Solution: Local Email Catching
Instead of sending emails to real inboxes, catch them locally. The email never leaves your machine - it's captured and displayed in a web UI.
Your App → SendPigeon SDK → localhost:4100 → 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
How It Works
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
Comparison
| Feature | SendPigeon Dev | Cloud Sandbox | Self-Hosted |
|---|---|---|---|
| Works offline | Yes | No | Yes |
| Setup | 1 command | Account + SMTP | Docker + SMTP |
| Code changes | Env var only | SMTP config | SMTP config |
| Same as production | Yes | No | No |
Each approach has its place. Cloud sandboxes work well for teams who want shared visibility. Self-hosted is great if you're already using Docker. SDK-integrated is ideal when you want zero friction between dev and production code.
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