// email sandbox

An email sandbox for local dev, CI and staging.

Point your app’s SMTP settings at smtp.mailqa.io with your organization’s credentials. Everything it sends is captured, filed by recipient, rendered and searchable — and none of it is delivered. The same code runs in every environment; only the host name changes.

.envany environment
# the only lines that differ from production
SMTP_HOST=smtp.mailqa.io
SMTP_PORT=587   # or 2525
SMTP_USER=your-org
SMTP_PASS=••••••••••••   # Settings → SMTP

// how it works

One host name. Nothing else changes.

01Swap the host

Your organization has one SMTP username and password — shown in the dashboard, and returned with every inbox the API creates. Put them in the environment your tests or your staging build run with.

02Send as you always do

Your app sends to the addresses it would send to anyway: your own subdomain, a customer’s Gmail, a whole list. The sandbox accepts all of them and delivers none. Nothing in your fixtures is rewritten.

03Read it, or wait for it

Each message is parsed the moment it lands and appears in the dashboard without a refresh. In a test, waitForMessage resolves the same moment, with the links and codes already extracted.

// configure your mailer

Pick your stack.

Every mailer speaks the same protocol, so every snippet says the same four things: the host, port 587 with STARTTLS, and the username and password from your environment. Copy the one for your framework into the configuration your tests or your staging build load.

NodemailerNode.js
import nodemailer from 'nodemailer';

// One environment variable separates local dev, CI and staging from
// production: point SMTP_HOST at the sandbox and nothing leaves it.
export const mailer = nodemailer.createTransport({
  host: process.env.SMTP_HOST, // smtp.mailqa.io
  port: Number(process.env.SMTP_PORT ?? 587),
  secure: false, // STARTTLS is negotiated on 587
  auth: {
    user: process.env.SMTP_USER, // your organization's SMTP username
    pass: process.env.SMTP_PASS,
  },
});

await mailer.sendMail({
  from: 'Acme <noreply@acme.dev>',
  to: 'signup+run-4187@your-org.mailqa.io',
  subject: 'Verify your email address',
  html: '<a href="https://acme.dev/verify?t=9f2c">Confirm email</a>',
});

Nodemailer is what the mailQA dev tools send with; secure: false on port 587 means STARTTLS, not plaintext.

// filed by recipient

Every address gets an inbox.

The sandbox does not care who your app thinks it is writing to. An address on your own subdomain lands in that inbox, tag and all, exactly as it would over public MX. Anything else — a real customer’s address, a colleague’s, a list — gets an inbox named for it, created the first time mail arrives.

That is what makes it safe to point a staging environment full of production-shaped data at it: the addresses stay real, and nobody outside your organization receives anything.

Your app sends toIt lands inBecause
qa@your-org.mailqa.iothe qa inboxAn address on your own subdomain is routed exactly as the public MX would route it.
qa+reset@your-org.mailqa.iothe qa inbox, tagged resetPlus-addressing tags the message, so one inbox covers a whole flow and the API filters by tag.
customer@gmail.coman inbox named customer@gmail.comAny other recipient gets an inbox of its own, created on first use. Nobody at Gmail hears about it.
qa@your-org.mailqa.io, cto@acme.devboth inboxes, one copy eachA message is filed once per recipient inbox, and each copy is stored and retained on its own.

// what you get

Not a log line. The whole message.

A console mail backend shows you a message once and forgets it. The sandbox keeps every message for your plan’s retention window, parses it the moment it lands, and hands the parts a test reaches for to you as data.

  • Rendered HTML, sanitized and shown in a sandboxed frame
  • Plain text and the raw .eml, byte for byte
  • Every header, plus SPF, DKIM and DMARC verdicts
  • Attachments, listed and downloadable one by one
  • Links and one-time codes extracted as data
  • Plus-address tags you can filter on in the API
  • Live updates, shared with everyone in the organization
  • A REST API and an SDK that wait instead of sleeping

// sandbox or real delivery

Two ways in. Same inbox either way.

The sandbox catches what your app sends. The public MX address proves it can be delivered — over the real internet, with DNS resolved and the signatures checked. Most teams use the sandbox from a laptop and CI, and the MX address from staging. Every plan has both, and the FAQ says when to pick which.

SMTP sandboxReal MX address
AddressAnything at allname@your-org.mailqa.io
NeedsYour organization’s SMTP credentialsA verified organization owner
CatchesEverything the app sendsMail sent to your subdomain
ProvesWhat your app sends, and to whomThat delivery works end to end — DNS, SPF, DKIM, DMARC
Use it forLocal development, CI, stagingStaging that must behave like production

// then, in the test

The message the shell tab just sent, read back.

Creating an inbox is idempotent, so a test can open the same one on every run. waitForMessage() then resolves as soon as the sandbox has parsed a match, and the code your app put in the subject is already in codes. No JavaScript on your side? The REST API answers the same question with one polling call.

login-code.spec.ts@mailqa/client
import { MailQA } from '@mailqa/client';

const mailqa = new MailQA({ apiKey: process.env.MAILQA_API_KEY! });
const inbox = await mailqa.inboxes.create({ name: 'signup' });

// Resolves the moment the sandbox has parsed the message —
// no sleep, no polling loop of your own.
const message = await mailqa.waitForMessage({
  inbox: inbox.id,
  subjectContains: 'login code',
});

expect(message.codes[0]).toBe('481330');

Your sandbox is one signup away.

Credentials are provisioned with the organization. Paste them into an environment file and send something.