mailfixture
v0.6.0
Published
Email, SMS, and webhook fixtures for test suites — programmatic inboxes, capture hooks, long-polling, and OTP extraction.
Maintainers
Readme
mailfixture
Email fixtures for your test suite. Create a programmatic inbox in one call, point your app's signup / OTP / password-reset flow at it, then wait for the email — parsed, with the OTP and links already extracted.
Receive-only, built for CI: long-polling instead of retry loops, zero runtime
dependencies, TypeScript types included, Node 18+ (or anywhere with fetch).
npm install -D mailfixtureQuickstart
Create a key in Dashboard → API keys and
export it as MAILFIXTURE_API_KEY.
import { test, expect } from "@playwright/test";
import { MailFixture } from "mailfixture";
const mfx = new MailFixture(); // reads MAILFIXTURE_API_KEY
test("signup sends a one-time code", async ({ page }) => {
const inbox = await mfx.createInbox({ ttlSeconds: 900 });
await page.goto("/signup");
await page.fill("#email", inbox.emailAddress);
await page.click("text=Send code");
const otp = await inbox.waitForOtp({ timeout: 30_000 }); // long-polls; no sleep()
await page.fill("#otp", otp);
await expect(page.locator("h1")).toHaveText("Welcome");
});Waiting for a magic link instead:
const link = await inbox.waitForLink({ kind: "verify", timeout: 30_000 });
await page.goto(link.url);Capturing a webhook your app sends:
const hook = await mfx.createHook({ label: "checkout", ttlSeconds: 900 });
await configureAppWebhook(hook.ingestUrl); // your test setup
await triggerCheckout();
const request = await hook.waitForRequest({ match: "body:order.created" });
expect(request.method).toBe("POST");
await hook.delete();The wait helpers long-poll server-side: the request is held open until a
matching message arrives, or the timeout passes and a MailFixtureTimeout
is thrown.
API surface
| Method | Purpose |
| --- | --- |
| createInbox({ localPart?, domain?, ttlSeconds? }) | new inbox; returns an Inbox handle |
| inbox.waitForMessage({ match?, timeout? }) | first matching message, full body + extraction |
| inbox.waitForOtp({ match?, timeout? }) | best extracted OTP as a string |
| inbox.waitForLink({ kind?, match?, timeout? }) | first link, optionally verify\|reset\|unsubscribe |
| inbox.messages({ since?, match?, wait? }) / clear() / delete() | raw listing & lifecycle |
| getMessage(id), getOtp(id), getLinks(id) | direct message access |
| downloadAttachment(messageId, index) | decoded attachment bytes |
| createHook({ label?, ttlSeconds? }), listHooks() | webhook capture URL and lifecycle handles |
| hook.waitForRequest({ match?, since?, timeout? }) | first matching captured request, including full headers and body |
| hook.requests(...), clear(), delete() / getHookRequest(id) | capture listing, teardown, and direct detail access |
| hookRequestBodyBytes(request) | exact body bytes (Uint8Array) for signature verification — handles the base64 branch for you |
| createDomain(fqdn), verifyDomain(id), listDomains(), deleteDomain(id) | custom domains |
| createKey(label?), listKeys(), revokeKey(id) | API keys |
match filters with subject:, from:, or to: prefixes; a bare term
matches the subject. Timeouts are in milliseconds.
Capture-hook matching accepts method:, path:, and body:; a bare term
matches the first 32 KiB of request body bytes. Returned detail bodies are
complete.
Errors are thrown as MailFixtureError carrying the API's RFC 7807
status / title / detail fields.
Development
npm test # builds ESM + CJS, runs the node:test suiteDocs: https://mailfixture.com/docs
