# Email codes and links

> Connect a test mailbox and use its codes and sign-in links without reading their secret values.

Source: https://rehearsal.dev/retest/docs/guides/email-codes-and-links

If sign-in sends a code or link by email, connect a test mailbox to Retest. Your test can enter the code or open the link without receiving its secret value.

## Connect a mailbox

Supply a function that reads your mailbox and returns messages newest first. This example reads a JSON outbox endpoint; replace the request with one for your test mail service.

```ts filename="retest.config.ts"
import type { InboxReader } from '@rehearsal-labs/retest'
import { chrome, defineConfig } from '@rehearsal-labs/retest'

const baseUrl = 'http://127.0.0.1:4173'
const readMailbox: InboxReader = async ({ signal }) => {
  const response = await fetch(new URL('/outbox', baseUrl), { signal })
  if (!response.ok) throw new Error('Could not read the test mailbox')
  return response.json()
}

const config = defineConfig({
  apps: { web: chrome({ baseUrl }) },
  inboxes: { owner: readMailbox },
  locks: ['inbox'],
})

export default config

declare module '@rehearsal-labs/retest' {
  interface Register {
    config: typeof config
  }
}
```

The reader returns an array of `InboxMessage` objects, which Retest validates on each read. Your adapter must extract the codes and links; Retest does not parse an email's HTML to find them.

| Field | What to return |
| --- | --- |
| `subject`, `from`, `text` | Strings, with the message body as plain text. |
| `codes` | An array of one-time codes as strings. |
| `links` | An array of `{ url, text }` objects with each full address and its visible text. |
| `receivedAt` | The arrival time as a `Date`, an ISO time string or milliseconds since 1970. |

Pass the abort signal to your mail request, as the example does. Retest aborts it when the read times out or the test stops, so the request can stop too.

## Enter a code

In this email sign-in example, the app asks for an address and sends a code. Request it through the form, wait for the code field, then read the message with `latest()`.

```ts filename="tests/email-sign-in.retest.ts"
import { expect, inbox, test } from '@rehearsal-labs/retest'

test('verifies an email code', { locks: ['inbox'] }, async ({ page }) => {
  await page.goto('/')
  await page.getByLabel('Email').fill('owner@tasks.test')
  await page.getByRole('button', { name: 'Send code' }).click()
  await expect(page.getByLabel('Code')).toBeVisible()

  const message = await inbox('owner').latest({ subject: 'sign-in code' })
  await page.getByLabel('Code').fill(message.code)
  await page.getByRole('button', { name: 'Verify' }).click()
  await expect(page.getByTestId('result')).toHaveText('Verified')
})
```

`latest()` waits for the newest matching message received since the test started. The subject string matches a part of the subject, without case sensitivity; you can also pass a `RegExp`.

`message.code` names the first code in that message. Retest types it as a secret and records `{{code}}`, so check the app's verification result rather than comparing the code itself.

## Open a sign-in link

If the message offers a sign-in link, replace the code-entry steps with navigation using its handle. With no argument, `link()` chooses the first token-bearing link, or the first link if none carries a token.

```ts
await page.goto(message.link())
await expect(page.getByRole('heading')).toHaveText('Signed in by link')
```

When an email has several links, use `message.link(2)` for its second link or `message.link({ path: '/verify' })` for one at or below that path. `message.links` exposes each link's text, origin, redacted path and token flag, without its query or fragment.

A code may be typed, and a link may open, only on the test apps' base URL origins. Add another destination to the inbox's `origins` when your sign-in service uses a different site.

```ts filename="retest.config.ts"
defineConfig({
  // Keep your apps and locks here.
  inboxes: {
    owner: {
      messages: readMailbox,
      origins: ['https://login.tasks.example'],
    },
  },
})
```

Keep the `readMailbox` function from the first example and replace its `inboxes` setting with this one. An unlisted destination fails with `not_actionable` before Retest types the code or opens the link.

## Fresh mail and failures

Each read advances this test's position in the inbox, so it never returns the same message twice or returns an older message afterwards. Await each read directly; `latest()` is not allowed inside `expect.poll()`.

Give every test sharing this mailbox the `inbox` lock from the example. It prevents those tests from running together within one run, so they cannot take each other's mail. Separate concurrent runs still need separate mailboxes or coordination outside Retest.

If no matching fresh message arrives within the assertion budget, the read fails with `timeout` and reports the message counts. Check the subject and `receivedAt` first; the mailbox clock must agree with the machine running Retest.

For a slower mail service, give `latest()` an explicit `timeout`, such as `15_000` milliseconds. The wait can last no longer than the test's remaining time, so adjust the test budget too if needed.

A reader that throws or returns an invalid message fails with `setup_failed`, as does a message with a code shorter than four characters. A message without a code fails with `usage` when you try to fill `message.code`.

## Redaction and platforms

Retest redacts codes and token-bearing links from message text, recorded addresses, logs and reports. Inbox reads appear in the report timeline, so you can see how long a test waited without seeing the secret values.

> [!WARNING]
> **Pixels are not redacted**
>
> A code or link that your app displays can appear in screenshots or recordings. [Recordings and evidence](https://rehearsal.dev/retest/docs/guides/recordings-and-evidence.md) explains the capture rules and their limits.

Inbox flows are supported on Chrome and Chromium, and have been checked on Chrome. Firefox and WebKit receive the same text and address, but their inbox flows have not been checked. Electron accepts a code only on the inbox's explicit origins and has no `goto()` for links. Native apps cannot fill inbox codes.

## Next steps

- [Signing in and secrets](https://rehearsal.dev/retest/docs/guides/signing-in.md): Reuse the browser state after sign-in succeeds.
- [Configuration options](https://rehearsal.dev/retest/docs/reference/config.md): Look up the mailbox reader and lock settings.
- [Debugging a failure](https://rehearsal.dev/retest/docs/essentials/debugging.md): Read the steps and inbox waits behind a failed test.
