Skip to content

Mailbox plugin

Use a named Mailbox plugin connection for signup, magic-link and OTP journeys. The remote inbox keeps the same address across plans, applies and partial reruns until you reset the selected execution group’s test data. Flows in the same group can share a name such as signup; separate names give them separate inboxes.

This plugin and the reset lifecycle require matching Reflow CLI/MCP, API and provider builds. Install the current CLI and refresh the flow’s Mailbox source and descriptor pins after inspecting its schema. Earlier pinned providers expose wait_code / wait_link; this version replaces those with capture and extract. Updating the skill alone does not add commands to an older release. The mailbox service also needs a configured delivery path: a local synthetic HTTP ingress tests your application’s mail integration, while external SMTP requires a configured inbound transport. Use the returned address; the service determines its domain.

Choose an application collection during onboarding. Use its ID when selecting the target below.

Keep the service credentials in a private bindings file, alongside your other plugin connections. Replace these example values and keep the file out of Git:

{
"site": {"provider": "Browser", "configuration": {}},
"inbox": {
"provider": "Mailbox",
"configuration": {
"apiUrl": "https://app.reflow.io",
"teamId": "your-team-id",
"token": "your-private-run-scoped-PAT"
}
}
}

The token needs run scope and access to the selected team. The plugin sends it only to the configured HTTPS origin; HTTP is supported for loopback development. Mailbox credentials belong in this configuration, never in a flow or captured output. The connection currently uses an explicit token; it does not inherit the sidecar’s login automatically.

Terminal window
reflow init --collection <collection-id>
chmod 600 /absolute/path/to/bindings.json
reflow target https://your-test-app.example.com --group account-tests --collection <collection-id> --scope shared --bindings /absolute/path/to/bindings.json

Planning reads declarations and retained evidence. It neither creates inboxes nor waits for mail. The address command provisions the named inbox during apply.

This flow assumes the application has a test account for the named address. For signup, use the returned email in your account-creation step first; repeating signup against an existing address needs your application’s usual idempotent setup or an explicit reset.

---
name: Sign in with an email code
providers:
ui: {use: Browser, binding: site}
inbox: Mailbox
---
### Choose the account
```rfl
inbox.address name="signup" -> email
```
### Request and use a new code
```rfl
open /login
fill label="Email" ${email}
inbox.cursor name="signup" -> before
click role=button name="Send code"
inbox.capture name="signup" after=${before} from="noreply@" subject="Sign-in code" timeout=30 -> email_body
inbox.extract text=${email_body} pattern="code is ([0-9]{6})" group=1 -> code
fill label="Verification code" ${code}
click role=button name="Verify"
expect page url~="/account"
```

Keep the cursor capture and the action that sends email in the same step. Reflow reruns a step as a unit: a new send then gets a new cursor, excluding mail from the earlier attempt. Splitting the cursor into a reused earlier step can select an old message. The sender and subject filters are required; an ambiguous message match fails instead of choosing one for you. inbox.capture returns the plain-text body, including line breaks, into the variable after ->. It does not look for codes or links. inbox.extract reads that variable and applies the regex you wrote. In this example the email must contain text such as Your code is 831204.

For an email containing Sign in: https://your-app.example/verify?token=..., extract the link from its body, then open it:

inbox.capture name="signup" after=${before} from="noreply@" subject="Sign in" timeout=30 -> email_body
inbox.extract text=${email_body} pattern="Sign in: (https://your-app[.]example/verify[?]token=[A-Za-z0-9_-]+)" group=1 -> link
open ${link}

Choose any valid variable names after ->; ${email_body} refers to the saved body and ${code} to the extracted value. Each command is explicit and retains its result. Plugin result bindings use the same typed references and cross-flow output rules as other variables. Both email bodies and extracted values become retained, team-visible execution evidence; use disposable test accounts.

A producer flow can capture its address and export it like any other output:

inbox.address name="customer" -> email

A consumer lists the producer under needs, declares needs.create-account.outputs.email as a string reference, then reads ${needs.create-account.outputs.email}. The plan binds that reference to the exact successful producer result. See the complete producer and consumer example.

Mailbox names are shared within the selected team and execution group, not scoped to a flow or apply. Shared groups agree across devices; device groups remain separate. Team members with run access can access named mailboxes in either scope. Selecting another target in the same group keeps its named test data; use a different group for a different environment, or reset deliberately. Parallel flows using one inbox can interleave messages. Give independent journeys different names or order them with needs.

Use reflow reset to replace the selected group’s named inboxes and generated values. Taint reruns checks with the same address. Reset does not delete accounts or other data in your application.

Messages are readable for up to 24 hours, or the service’s shorter configured period, independently of the address. Expiry does not replace the inbox or rewind its cursor. The recurring cleanup worker removes expired bodies after a two-minute grace period; expiry is not a promise of immediate physical deletion.

The separate browser-run/device-job path still supplies ${MAILBOX} when mailbox provisioning is requested, and retains its run-scoped email commands and remote MCP mailbox tools. Those disposable run mailboxes do not have this named, stable-until-reset lifecycle. Use the Mailbox provider with plan / apply for shared test data.

CommandArgumentsReturns
inbox.addressRequired name string.The address as a string; provisions it during apply if needed.
inbox.cursorRequired name string.An integer cursor; does not allocate an inbox.
inbox.captureRequired name, from, subject strings and after integer; optional timeout integer.The plain-text body of exactly one matching message, up to 64 KiB.
inbox.extractRequired text and pattern strings, and group integer.One regex match from the supplied text; no mailbox request.

Mailbox wait timeouts use seconds, from 1 to 30, with a default of 30. Both sender and subject are substring filters. No matching message before the deadline, or more than one matching message, fails the command. Use separate names for parallel journeys instead of widening filters until an old message matches.

Extraction uses Go RE2 regex syntax. Choose group=0 for the whole match or group=1 for the first parenthesized group. The group is required. No match, more than one match (even if the values are identical), a missing group or an empty selected group fails. Patterns are limited to 4096 bytes and text to 64 KiB. Lookaround and backreferences are unsupported. Use [0-9] to avoid escaping; in quoted RFL arguments a regex backslash must be doubled, for example "\\d{6}". For a link, include the expected application origin in the regex when appropriate; extraction does not infer a URL or validate its destination.

Read test accounts and shared data for application setup, reset behavior and the distinction between named test values and observed data.

Flows using this plugin can be saved in Reflow or in your repository. Read test storage for the team preference, private bindings and the shared plan/apply workflow.