@lanbox/client
Typed TypeScript SDK for LanBox — local email infrastructure for developers.
LanBox captures SMTP email locally, provides an interactive browser inbox, and exposes event-driven REST APIs to wait for incoming mail, OTP verification codes, and magic links without polling. @lanbox/client is the zero-dependency TypeScript SDK for interacting with those APIs in unit, integration, and end-to-end test suites.
Install
npm install --save-dev @lanbox/client
pnpm add -D @lanbox/client
Prerequisites
- Node.js:
v22.0.0or newer (uses built-infetchandcrypto). - LanBox running locally:
docker compose up -d
| Service | Default Address |
|---|---|
| Web UI | http://127.0.0.1:8025 |
| REST API | http://127.0.0.1:8025/api/v1 |
| SMTP Server | 127.0.0.1:1025 |
| Virtual Domain | lanbox.test |
Configure your application under test to deliver SMTP traffic to 127.0.0.1:1025 (without TLS or authentication).
Quickstart: The Race-Safe Test Pattern
In automated tests, email delivery is asynchronous. To eliminate race conditions and flaky tests, register the wait promise before triggering the application action that sends the email:
import { test, expect } from 'vitest';
import { LanBox } from '@lanbox/client';
test('user signup and email OTP verification', async () => {
const lanbox = new LanBox();
// 1. Generate a collision-resistant address locally (e.g. signup-a1b2c3d4e5f6@lanbox.test)
const email = lanbox.generateAddress('signup');
// 2. Register the waiter BEFORE triggering signup
const otpPromise = lanbox.waitForOtp(email, { timeout: 10_000 });
// 3. Trigger your application's signup flow
await app.signUp({ email, password: 'SecretPassword123' });
// 4. Await extracted OTP and complete verification
const { otp } = await otpPromise;
const result = await app.verifyOtp({ email, otp });
expect(result.status).toBe('verified');
});
Virtual Addresses: LanBox does not require mailbox pre-provisioning. Any arbitrary address ending in
@lanbox.test(or your configured domain) is accepted and indexed dynamically on receipt.
OTP Workflows
Extract 4–8 digit verification codes from incoming plain text or HTML emails.
Wait for Incoming OTP
Use waitForOtp() when the email is expected to arrive during the test:
const { otp, messageId, subject, receivedAt } = await lanbox.waitForOtp(
'user@lanbox.test',
{
timeout: 10_000,
length: 6, // Optional: enforce exact digit length
subject: 'Verification Code', // Optional: filter by subject substring
}
);
Extract OTP from Stored Email
Use otp() when the email has already been received:
const { otp } = await lanbox.otp('user@lanbox.test', { length: 6 });
If no OTP is found or multiple conflicting codes appear without clear context, the promise rejects with LanBoxOtpError (OTP_NOT_FOUND or OTP_AMBIGUOUS).
Isolated Test Sessions
When running parallel test suites (e.g. multi-worker Playwright or Vitest runs), use sessions to avoid cross-test message collisions.
import { LanBox } from '@lanbox/client';
const lanbox = new LanBox();
// Create an isolated session (TTL between 60s and 7 days; defaults to 1 hour)
const session = await lanbox.createSession({ ttlSeconds: 900 });
try {
// Generates scoped plus-address: e.g. reset+s_1234567890abcdef12345678@lanbox.test
const email = session.address('reset');
// Register session-scoped waiter
const linkPromise = session.waitForLink('reset', {
host: 'app.example.test',
pathIncludes: '/reset-password',
timeout: 10_000,
});
// Trigger password reset in your app
await app.requestPasswordReset(email);
const { link } = await linkPromise;
// Use `link` in your browser automation test...
} finally {
// Purges session metadata and all emails delivered to this session
await session.delete();
}
Magic Links
Extract HTTP/HTTPS action links from captured emails.
const { link, host, pathname } = await lanbox.waitForLink(
'user@lanbox.test',
{
host: 'app.example.test', // Optional: filter by target host
pathIncludes: '/auth/magic-login', // Optional: filter by path fragment
timeout: 10_000,
}
);
Security Guarantee: LanBox parses and ranks links based on context and query parameters, but never fetches, opens, or executes extracted URLs. If multiple top-ranked candidates match equally, the request rejects with
LanBoxLinkError(LINK_AMBIGUOUS).
Waiting for Messages
Wait for a full RFC822 parsed message:
const message = await lanbox.wait('billing@lanbox.test', {
from: 'receipts@example.test',
subject: 'Invoice #2026-001',
after: '2026-08-21T00:00:00.000Z',
timeout: 15_000,
});
console.log(message.subject);
console.log(message.textBody);
console.log(message.htmlBody);
console.log(message.attachments);
Assertion Helpers
The SDK provides assertion aliases that throw a LanBoxAssertionError on mismatch:
// Wait and assert exact OTP match
await lanbox.toContainOtp('signup@lanbox.test', '849201', { timeout: 10_000 });
// Wait and assert link pattern match
await lanbox.toContainLink('user@lanbox.test', /\/verify\?token=[a-f0-9]+/, {
timeout: 10_000,
});
Cancellation and Timeouts
Pass an AbortSignal to cancel pending operations when test runners enforce their own test deadlines:
const controller = new AbortController();
const waitPromise = lanbox.wait('user@lanbox.test', {
timeout: 30_000,
signal: controller.signal,
});
// Cancel if needed
controller.abort();
try {
await waitPromise;
} catch (error) {
if (error instanceof LanBoxAbortError) {
console.log('Wait operation was aborted.');
}
}
- Aborted requests reject with
LanBoxAbortError. - Server timeouts reject with
LanBoxTimeoutError.
Custom Configuration
Configure remote or LAN instances:
const lanbox = new LanBox({
baseUrl: 'http://192.168.1.20:8025/api/v1',
domain: 'mail.example.test',
});
baseUrl: Base URL of the LanBox REST API (defaults tohttp://127.0.0.1:8025/api/v1).domain: Virtual domain forgenerateAddress()(must match server'sSMTP_DOMAIN).fetch: Customfetchimplementation (defaults toglobalThis.fetch).
Error Handling
All SDK errors inherit from LanBoxError and include structured properties (code, message, status, details):
| Error Class | Error Code | Description |
|---|---|---|
LanBoxTimeoutError |
WAIT_TIMEOUT |
Wait deadline expired before matching mail arrived. |
LanBoxAbortError |
WAIT_CANCELLED |
Client aborted the request via AbortSignal. |
LanBoxOtpError |
OTP_NOT_FOUND / OTP_AMBIGUOUS |
OTP code missing or conflicting numbers detected. |
LanBoxLinkError |
LINK_NOT_FOUND / LINK_AMBIGUOUS |
Link missing or multiple ambiguous candidates found. |
LanBoxHttpError |
HTTP_* |
Server returned a 4xx or 5xx response. |
LanBoxAssertionError |
ASSERTION_FAILED |
Assertion alias mismatch or confirmation failure. |
import { LanBox, LanBoxTimeoutError, LanBoxOtpError } from '@lanbox/client';
try {
await lanbox.waitForOtp('user@lanbox.test', { timeout: 5_000 });
} catch (err) {
if (err instanceof LanBoxTimeoutError) {
console.error('Email did not arrive within 5 seconds.');
} else if (err instanceof LanBoxOtpError) {
console.error('Email arrived but OTP could not be determined:', err.details);
}
}
Method Reference
| Method | Return Type | Description |
|---|---|---|
status(signal?) |
Promise<HealthStatus> |
Get runtime health of API, DB, and SMTP. |
generateAddress(prefix?) |
string |
Generate collision-resistant virtual address. |
latest(to, options?) |
Promise<Message> |
Get newest matching message already in DB. |
wait(to, options?) |
Promise<Message> |
Wait for matching incoming message. |
otp(to, options?) |
Promise<OtpResult> |
Extract OTP from latest message. |
waitForOtp(to, options?) |
Promise<OtpResult> |
Wait for message and extract OTP. |
link(to, options?) |
Promise<LinkResult> |
Extract magic link from latest message. |
waitForLink(to, options?) |
Promise<LinkResult> |
Wait for message and extract magic link. |
createSession(options?) |
Promise<LanBoxSession> |
Create an isolated test session. |
getSession(id) |
Promise<LanBoxSession> |
Retrieve an existing test session. |
listInboxes(options?) |
Promise<{ inboxes, total }> |
List virtual inboxes with message counts. |
clearInbox(address) |
Promise<void> |
Clear all deliveries for a virtual inbox. |
clearAll(confirm) |
Promise<void> |
Clear all messages and attachments (requires true). |
CI & Automated Test Integration
- Start LanBox in your CI workflow via Docker Compose.
- Wait for
http://127.0.0.1:8025/api/v1/healthbefore running test suites. - Use unique addresses via
generateAddress()orcreateSession()per test worker. - Clean up sessions in test
afterEachorfinallyblocks.
See working examples in the repository:
Links & Resources
- Repository: github.com/tushar-gour/LanBox
- CLI Package: @lanbox/cli
- Issue Tracker: github.com/tushar-gour/LanBox/issues
- Architecture Documentation: docs/architecture.md
License
No open-source license is currently selected. The repository owner must choose one before external reuse or redistribution.