Testing Transactional Email in Development and Staging Safely
Test transactional email without emailing real customers: recipient allowlists, redirect-to-yourself, seeded inboxes, feature flags, and a key per…
Every team has the story: a staging database restored from production, a cron job that ran, and three thousand customers who received "your trial ends tomorrow" for a product they pay for. Email testing is less about seeing your template render and more about making it impossible to reach real people from a non-production environment. This guide sets up both.
Quick answer
Give each environment its own API key and sending address, wrap every send in a guard that in non-production environments rewrites or drops recipients not on an allowlist, seed test data with addresses you own, put risky sequences behind feature flags that default off outside production, and send real test messages to your own inboxes to check rendering and authentication.
Separate keys, separate senders
- Create three API keys in OquMail: one for local development, one for staging, one for production, each named for its environment. Revoking a leaked development key must never affect production.
- Use a distinct from address per environment: staging@yourdomain.com or a dedicated test domain such as yourdomain-test.com, which the free plan can host alongside your main domain (up to 3 domains).
- Prefix subjects outside production: "[staging] Order #10482 confirmed". Nobody confuses a test email with a real one.
- Keep the keys in environment variables or a secrets manager; never in a committed .env file.
The recipient guard
This is the piece that prevents the disaster. In any environment other than production, the wrapper checks the recipient against an allowlist of addresses and domains you own. Anything else is either dropped with a log line or redirected to a catch-all inbox you read, with the original recipient noted in the subject. Put it in the one function every send goes through, so a new developer cannot bypass it by accident.
const ALLOW = (process.env.EMAIL_ALLOWLIST || "").split(",").filter(Boolean); // e.g. "@yourdomain.com,qa@yourdomain.com"
const SINK = process.env.EMAIL_SINK; // e.g. "test-inbox@yourdomain.com"
function guardRecipient(to, subject) {
if (process.env.NODE_ENV === "production") return { to, subject };
const ok = ALLOW.some((a) => (a.startsWith("@") ? to.endsWith(a) : to === a));
if (ok) return { to, subject: "[" + process.env.NODE_ENV + "] " + subject };
if (!SINK) { console.warn("email dropped (not allowlisted):", to); return null; }
return { to: SINK, subject: "[" + process.env.NODE_ENV + " -> " + to + "] " + subject };
}
export async function sendEmail({ from, to, subject, html, text }) {
const g = guardRecipient(to, subject);
if (!g) return;
const res = await fetch("https://api.oqumail.com/api/v1/emails", {
method: "POST",
headers: { "Authorization": "Bearer " + process.env.OQUMAIL_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ from, to: g.to, subject: g.subject, html, text })
});
if (!res.ok) throw new Error("send failed: " + res.status);
}Seeded inboxes and test data
- Create a few mailboxes on your domain for testing: qa@, test-inbox@, and one per developer if you like. On OquMail these are normal mailboxes readable in webmail or any IMAP client.
- Seed development and staging databases with users whose addresses are on those mailboxes, using plus addressing (qa+user1@yourdomain.com) so they stay distinct.
- When restoring production data into staging, rewrite every email column to a sink address as part of the restore script. Do this before the database is reachable by the app.
- Add an automated check that fails the deploy if a staging database contains any address not matching the allowlist.
Feature flags for sequences
Sequences that run on a schedule (dunning, trial reminders, digests, abandoned carts) are the ones that misfire, because they run without anyone pressing a button. Put each behind a flag that defaults to off outside production, and require production to turn it on explicitly. In staging, run the sequence job with a dry-run flag that logs what it would send without calling the API; review that log before enabling a new sequence in production. For the first production run of any new sequence, restrict recipients to internal addresses for a day.
What to actually check in a test send
- Rendering in Gmail web, Gmail Android, Apple Mail on iPhone and Outlook. Send the same message to a mailbox on each.
- authentication (proof that email really comes from your company): in Gmail choose "Show original" and confirm SPF, DKIM and DMARC all show PASS for your domain.
- Links: every URL points at the right environment. Staging emails linking to production is a classic.
- The plain text part reads well and includes the links.
- The delivery log in OquMail shows the message with the receiving server's SMTP response, so you know what "sent" looks like before you need to debug a real one.
Unit and integration tests
In automated tests do not call the API at all. Inject a fake transport that records calls, and assert on the from, to, subject and body. Keep one integration test, run manually or nightly, that sends a real message with the development key to a test mailbox and checks it arrives, so a revoked key or a broken domain is noticed before a release. Snapshot-test rendered HTML so template changes are reviewed in pull requests.
Common questions
Should I use a fake SMTP server like Mailpit or MailHog locally?
They are excellent for seeing HTML instantly without leaving your machine. Use them for day-to-day development and the real API with a guard for staging, where you also need to test authentication and delivery.
How do I test bounces?
Send to an address that does not exist on a domain you control and read the SMTP response in the delivery log. Do not use random addresses on other people's domains; that is how domains earn a poor reputation.
Is it safe to share one API key between staging and production?
No. Apart from the risk of a leak taking down production sending, you lose the ability to see in the delivery log which environment sent a message.
Free business email on your own domain
OquMail gives you up to 15 mailboxes on your domain — free — with guided SPF/DKIM/DMARC, webmail, IMAP/SMTP for any mail app, and a send API. Most teams are live in under fifteen minutes. Start at oqumail.com.
Get started free