Set Up an Email QA Pipeline
Email is hard to test because production email is hard to mock without diverging from reality. mailbot's sandbox inbox solves that: every test gets a real, throwaway inbox that sends and receives real email, but on the sandbox domain so nothing leaks to a customer.
This guide shows the full pattern with Jest, but the same approach works in any test runner.
Pattern
beforeAll → create sandbox inbox via API
test → send mail, assert it arrived
afterAll → disable inbox (audit trail preserved)
The inbox is short-lived, scoped to the test run, and isolated from your production traffic.
Prerequisites
- mailbot API key set as
MAILBOT_API_KEYin test env - the Node SDK (
@yopiesuryadi/mailbot-sdk) - Jest 29+ (or any runner with
before/afterhooks)
Setup
// tests/email/setup.js
import { MailBot } from '@yopiesuryadi/mailbot-sdk';
export const mailbot = new MailBot({ apiKey: process.env.MAILBOT_API_KEY });
export async function createTestInbox(label) {
const inbox = await mailbot.inboxes.create({
username: `test-${Date.now()}`,
labels: ['qa', label],
});
return inbox;
}
export async function disableTestInbox(inboxId) {
await mailbot.inboxes.disable(inboxId);
}
A complete test
// tests/email/onboarding-email.test.js
import { mailbot, createTestInbox, disableTestInbox } from './setup';
describe('onboarding email', () => {
let inbox;
beforeAll(async () => {
inbox = await createTestInbox('onboarding');
});
afterAll(async () => {
await disableTestInbox(inbox.id);
});
it('sends a welcome email when a user signs up', async () => {
// trigger your app's signup flow with the test inbox address
await fetch('https://yourapp.example.com/signup', {
method: 'POST',
body: JSON.stringify({ email: inbox.address }),
headers: { 'Content-Type': 'application/json' },
});
// wait for the email to land
const message = await waitForMessage(inbox.id, {
subject: /welcome/i,
timeoutMs: 30_000,
});
expect(message).toBeDefined();
expect(message.body_text).toContain('Welcome');
expect(message.from).toBe('hello@yourapp.example.com');
});
});
The waitForMessage helper
Email is asynchronous. Poll with a timeout instead of expecting an immediate result.
export async function waitForMessage(inboxId, opts = {}) {
const { subject, fromContains, timeoutMs = 30_000, pollMs = 1000 } = opts;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const messages = await mailbot.messages.list({ inbox_id: inboxId, direction: 'inbound' });
for (const m of messages) {
if (subject && !subject.test(m.subject)) continue;
if (fromContains && !m.from.includes(fromContains)) continue;
return m;
}
await new Promise((r) => setTimeout(r, pollMs));
}
const snapshot = await mailbot.messages.list({ inbox_id: inboxId, direction: 'inbound' });
throw new Error(
`waitForMessage timed out. Found ${snapshot.length} messages in inbox ${inboxId}: ` +
JSON.stringify(snapshot.map((m) => ({ subject: m.subject, from: m.from }))),
);
}
The error includes a snapshot of what was in the inbox, which makes failures actionable. "Expected welcome email, got password-reset email" is a real bug; without the snapshot you would just see "timed out".
Asserting tracking
Open and click events are tracked automatically. Test them by sending a message and waiting for the engagement event.
it('tracks the onboarding link click', async () => {
const sent = await mailbot.messages.send({
inbox_id: inbox.id,
to: ['recipient@example.com'],
subject: 'Onboarding',
body_html: '<a href="https://app.example.com/onboard">Start</a>',
});
// simulate the click (or wait for a real one in an E2E run)
// ...
const events = await waitForEvent(sent.id, 'message.clicked');
expect(events.length).toBeGreaterThan(0);
});
Practical tips
- Use one inbox per test file. Sharing inboxes across test files leads to flaky asserts when messages from one test bleed into another.
- Disable, do not delete. Disabled inboxes preserve audit history. The test address is unreusable but the failure record stays.
- Run sequentially or label aggressively. The
labelsfield on inbox creation is your friend for filtering during debugging. - Cap parallelism. mailbot's sandbox burst limit is
25/minper inbox. For high-parallelism CI, create one inbox per test file rather than per test. - Do not test production with sandbox inboxes. The sandbox domain has its own reputation profile. Your real outbound goes through your verified domain.