npm.io
1.0.1 • Published 6d ago

mailcapture

Licence
MIT
Version
1.0.1
Deps
0
Size
131 kB
Vulns
0
Weekly
102

mailcapture

Official JavaScript/TypeScript SDK for MailCapture — a real email capture API for integration testing OTP codes, verification links, and other transactional emails.

MailCapture captures emails sent by your application during testing. Point your app at a MailCapture address, trigger your email flow, then use this SDK to retrieve and assert on what arrived — subject lines, body text, OTP codes, and more. No mock SMTP server, no inbox polling, no flaky waits.

A MailCapture account is required — free and paid plans are available. Sign up at mailcapture.app.

Installation

npm install mailcapture
# or
yarn add mailcapture
# or
pnpm add mailcapture

Requires Node.js 18+ (uses native fetch).

Quick start

import MailCapture from 'mailcapture'

const mc = new MailCapture(process.env.MAILCAPTURE_API_KEY)

// Get your capture address template
const { username } = await mc.ping()

// Send an email to username-signup@mailcapture.app, then:
const email = await mc.waitFor('signup', { timeout: 15_000 })

console.log(email.subject)   // "Welcome to Acme!"
console.log(email.otp)       // "123456" — extracted automatically

The pattern for integration tests

  1. Clear the inbox before each test
  2. Trigger the action that sends the email (register, reset password, etc.)
  3. Wait for the email — waitFor holds the connection open and returns the instant it arrives
  4. Assert on subject, OTP, body, links
  5. Clean up after
// vitest / jest example
describe('signup flow', () => {
  let mc: MailCapture
  let inbox: Inbox

  beforeAll(async () => {
    mc = new MailCapture(process.env.MAILCAPTURE_API_KEY)
    await mc.ping()                    // validates key, caches username
    inbox = mc.inbox('signup')
  })

  beforeEach(() => inbox.clear())      // clean slate for every test

  it('sends a 6-digit OTP on signup', async () => {
    await registerUser(inbox.address)  // e.g. "alice-signup@mailcapture.app"

    const email = await inbox.waitFor({ timeout: 10_000 })

    expect(email.subject).toBe('Verify your email')
    expect(email.otp).toMatch(/^\d{6}$/)
  })
})

API reference

new MailCapture(apiKey, options?)
const mc = new MailCapture('mc_...')

// With options
const mc = new MailCapture('mc_...', {
  baseUrl: 'http://localhost:3002',  // for local dev
  requestTimeout: 15_000,           // ms, default 10_000
})

mc.ping()PingResult

Validates your API key and returns your capture address template. Also caches your username internally so mc.address(tag) works without a network call.

const { username, address_template, example } = await mc.ping()
// username: "alice"
// address_template: "alice-{tag}@mailcapture.app"
// example: "alice-signup@mailcapture.app"

mc.waitFor(tag, options?)Capture

Long-polls the API and returns the first email captured for the given tag. This is efficient — the server holds the connection open and responds the moment an email arrives. No polling delay.

const email = await mc.waitFor('signup', {
  timeout: 15_000,     // total wait time in ms (default: 30_000)
  pollTimeout: 10,     // per-poll server timeout in seconds (default: 10, max: 30)
  after: new Date(),   // only emails received after this (default: 60s ago)
})

Throws MailCaptureTimeoutError if no email arrives before timeout.


mc.inbox(tag)Inbox

Returns a scoped Inbox object for a specific tag. Cleaner than passing the tag to every call.

const inbox = mc.inbox('password-reset')

inbox.address           // "alice-password-reset@mailcapture.app" (requires ping() first)
await inbox.waitFor()   // same as mc.waitFor('password-reset')
await inbox.list()      // same as mc.list({ tag: 'password-reset' })
await inbox.clear()     // same as mc.delete('password-reset')

mc.address(tag)string

Generates the capture email address for a tag. Synchronous — requires ping() to have been called first.

await mc.ping()
mc.address('signup')  // "alice-signup@mailcapture.app"

mc.list(options?)CaptureList

List recent captures (newest first).

const { items, count } = await mc.list({
  tag: 'signup',        // filter by tag
  limit: 25,            // default 25, max 100
  after: someDate,      // only captures after this date
})

mc.get(id)Capture

Get a single capture by ID.

const email = await mc.get('e0f5922d-d8a9-4b03-bc60-9507b2e2f665')

Throws MailCaptureNotFoundError if not found.


mc.delete(tag)void

Delete all captures for a tag. Use this before each test for a clean starting state.

await mc.delete('signup')

// or via inbox:
await inbox.clear()

The Capture object

interface Capture {
  id: string           // UUID
  tag: string          // e.g. "signup"
  subject: string      // email subject line
  otp: string | null   // extracted OTP/code, if detected
  body_text: string | null  // plain text body
  body_html: string | null  // HTML body
  latency_ms: number   // time from send to capture, in ms
  status: string       // e.g. "captured"
  received_at: string  // ISO 8601 timestamp
}

The otp field is automatically extracted from the email body. If your OTP is embedded in a sentence, the service finds it for you. If no code is detected, it's null.


Error handling

All errors extend MailCaptureError and have a .code property.

import {
  MailCaptureAuthError,
  MailCaptureTimeoutError,
  MailCaptureNotFoundError,
  MailCaptureNetworkError,
  MailCaptureApiError,
} from 'mailcapture'

try {
  const email = await mc.waitFor('signup', { timeout: 10_000 })
} catch (err) {
  if (err instanceof MailCaptureTimeoutError) {
    // No email arrived in time
    console.error(`Waited ${err.waitedMs}ms for tag "${err.tag}"`)
    console.error('Did the email send? Check your email service logs.')
  } else if (err instanceof MailCaptureAuthError) {
    // Invalid or revoked API key
    console.error('Check your MAILCAPTURE_API_KEY')
  } else if (err instanceof MailCaptureNetworkError) {
    // Could not reach the API
    console.error('Network error:', err.cause)
  } else {
    throw err
  }
}
Error class .code When
MailCaptureAuthError UNAUTHORIZED Invalid or revoked API key
MailCaptureTimeoutError TIMEOUT waitFor exceeded its timeout
MailCaptureNotFoundError NOT_FOUND get(id) — capture doesn't exist
MailCaptureNetworkError NETWORK_ERROR Could not reach the API
MailCaptureApiError varies Unexpected API error

Running tests against a local server

const mc = new MailCapture('mc_your_key_here', {
  baseUrl: 'http://localhost:3002',
})

Parallel tests

Each tag is its own inbox — safe to run in parallel.

const [signupEmail, resetEmail] = await Promise.all([
  mc.waitFor('signup', { timeout: 15_000 }),
  mc.waitFor('password-reset', { timeout: 15_000 }),
])

Just make sure your test tags are unique per test run if tests share an API key.


Environment variable

The SDK reads no environment variables automatically. Pass your key explicitly:

const mc = new MailCapture(process.env.MAILCAPTURE_API_KEY!)

Get your API key at mailcapture.app/admin/api-keys.

Keywords