---
title: "Maestro | Proxy Mail QA"
description: "Test email verification codes and magic links in Maestro flows for iOS, Android, React Native and Flutter apps with disposable Proxy Mail QA inboxes."
url: https://proxymail.qa/docs/maestro
---

# Maestro

One script covers native iOS and Android, React Native and Flutter apps, because Maestro drives the app from outside. Save it next to your flows as `proxymail.js`:

proxymail.js

```js
// Proxy Mail QA for Maestro flows: works for native iOS and Android, React
// Native and Flutter apps. Guide: https://proxymail.qa/docs/maestro
//
//   - runScript: { file: proxymail.js, env: { ACTION: create } }
//       output.address, output.password, output.since. Add ENVIRONMENT
//       (staging), AGE (adult, under18…) or COUNTRY (GB) to set them.
//   - runScript: { file: proxymail.js, env: { ACTION: code } }   (or link)
//       output.code (or output.link), for mail received since output.since.
//     Add APP_LINK: true for the link that opens the app (myapp://…) when
//     the email also has a web one, and UNUSED: true to skip a used code.
//   - runScript: { file: proxymail.js, env: { ACTION: count } }
//       output.count, the emails received since output.since
//   - runScript: { file: proxymail.js, env: { ACTION: check, EXPECT_HOST: links.example.com } }
//       fails the flow if the newest email fails a check (SPF, DKIM, DMARC,
//       local or http links, where its link goes…); output.failed lists them
//   - runScript: { file: proxymail.js, env: { ACTION: none } }
//       fails the flow if an email arrives within 30 seconds (TIMEOUT)
//   - runScript: { file: proxymail.js, env: { ACTION: signin, TAGS: verified } }
//       output.address and output.password of a saved test account; needs a
//       key allowed to read saved passwords
//   - runScript: { file: proxymail.js, env: { ACTION: checkout, TAGS: verified } }
//       borrows an account no other test has: output.address, output.password,
//       output.token. ACTION: return gives it back (ADD_TAGS, REMOVE_TAGS).
//
// The API key comes from the run: maestro test -e PROXYMAIL_QA_API_KEY=... flow.yaml
// Add -e PROXYMAIL_QA_RUN=$PROXYMAIL_QA_RUN to add the calls to a run (the
// GitHub Action sets it), and TEST: a name, in env, to name the step.
const API = 'https://api.proxymail.qa/v1';
const headers = { Authorization: 'Bearer ' + PROXYMAIL_QA_API_KEY, 'Content-Type': 'application/json' };
if (typeof PROXYMAIL_QA_RUN !== 'undefined' && PROXYMAIL_QA_RUN) {
  headers['Proxy-Mail-QA-Run'] = PROXYMAIL_QA_RUN;
  // Percent-encoded: a test's name can hold characters headers can't.
  if (typeof TEST !== 'undefined' && TEST) headers['Proxy-Mail-QA-Test'] = encodeURIComponent(TEST);
}

function call(method, path, body, extra) {
  const options = { headers: extra || headers };
  if (body) options.body = JSON.stringify(body);
  const response = method === 'POST' ? http.post(API + path, options) : http.get(API + path, options);
  return { ok: response.ok, status: response.status, body: response.body ? json(response.body) : {} };
}

function fail(response) {
  // When nothing arrives, the hint says why (an email provider in sandbox mode, say).
  throw new Error('Proxy Mail QA: ' + (response.body.message || response.status) + ' ' + (response.body.hint || ''));
}

function tags(value) {
  return String(value).split(',').map(function (tag) { return tag.trim(); }).filter(function (tag) { return tag; });
}

function account(body) {
  output.address = body.address;
  output.password = body.password;
  output.since = new Date().toISOString();
}

const at = typeof output.address === 'undefined' ? '' : '/addresses/' + encodeURIComponent(output.address) + '/';
const since = '?since=' + encodeURIComponent(output.since);

if (ACTION === 'create') {
  const body = { password: true, persona: true };
  if (typeof ENVIRONMENT !== 'undefined') body.environment = ENVIRONMENT;
  if (typeof AGE !== 'undefined') body.age = AGE;
  if (typeof COUNTRY !== 'undefined') body.country = COUNTRY;
  const created = call('POST', '/addresses', body);
  if (!created.ok) fail(created);
  account(created.body);
} else if (ACTION === 'count') {
  const counted = call('GET', at + 'count' + since);
  if (!counted.ok) fail(counted);
  output.count = counted.body.count;
} else if (ACTION === 'check') {
  const host = typeof EXPECT_HOST !== 'undefined' ? '&expect_host=' + encodeURIComponent(EXPECT_HOST) : '';
  const checked = call('GET', at + 'checks' + since + host);
  if (!checked.ok) fail(checked);
  output.failed = checked.body.failed.join(', ');
  const failures = checked.body.checks.filter(function (c) { return c.status === 'fail'; }).map(function (c) { return c.check + ': ' + c.detail; });
  if (failures.length) throw new Error('Proxy Mail QA: the email failed ' + failures.join(' '));
} else if (ACTION === 'none') {
  const timeout = typeof TIMEOUT !== 'undefined' ? TIMEOUT : 30;
  const watched = call('GET', at + 'none' + since + '&timeout=' + timeout);
  if (!watched.ok) fail(watched);
} else if (ACTION === 'signin') {
  const body = { tags: typeof TAGS !== 'undefined' ? tags(TAGS) : [] };
  if (typeof ENVIRONMENT !== 'undefined') body.environment = ENVIRONMENT;
  const found = call('POST', '/addresses/sign-in', body);
  if (!found.ok) fail(found);
  account(found.body);
} else if (ACTION === 'checkout') {
  const held = call('POST', '/addresses/checkout', { tags: tags(TAGS) });
  if (!held.ok) fail(held);
  // Its saved password, when the key may read it.
  const secret = call('GET', '/addresses/' + encodeURIComponent(held.body.address) + '/password');
  account({ address: held.body.address, password: secret.ok ? secret.body.secret : undefined });
  output.token = held.body.checkout.token;
} else if (ACTION === 'return') {
  const body = { token: output.token };
  if (typeof ADD_TAGS !== 'undefined') body.add_tags = tags(ADD_TAGS);
  if (typeof REMOVE_TAGS !== 'undefined') body.remove_tags = tags(REMOVE_TAGS);
  const returned = call('POST', at + 'return', body);
  if (!returned.ok) fail(returned);
} else {
  // Each request waits up to 10 seconds; four tries cover the email taking up to 40.
  const kind = ACTION === 'link' ? 'link' : 'code';
  const app = kind === 'link' && typeof APP_LINK !== 'undefined' && String(APP_LINK) === 'true' ? '&app=true' : '';
  const unused = typeof UNUSED !== 'undefined' && String(UNUSED) === 'true' ? '&unused=true' : '';
  let found = null;
  for (let i = 0; i < 4 && !found; i++) {
    // Only the last try's "nothing arrived" counts in a run's report.
    const retry = JSON.parse(JSON.stringify(headers));
    if (i < 3) retry['Proxy-Mail-QA-Run-Retry'] = 'true';
    found = call('GET', at + kind + since + '&timeout=10' + app + unused, null, retry);
    if (!found.ok && i < 3) found = null;
  }
  if (!found.ok) throw new Error('Proxy Mail QA: no ' + kind + ' arrived. ' + (found.body.hint || ''));
  output[kind] = found.body.value;
}
```

Then use it in a flow: create the address, sign up with it, and fill in the code.

signup.yaml

```yaml
appId: com.example.app
---
- launchApp
- runScript:
    file: proxymail.js
    env:
      ACTION: create
- tapOn: "Email"
- inputText: ${output.address}
- tapOn: "Password"
- inputText: ${output.password}
- tapOn: "Sign up"
- runScript:
    file: proxymail.js
    env:
      ACTION: code
- inputText: ${output.code}
```

Run it with your [API key](https://proxymail.qa/docs/api-keys):

Terminal window

```bash
maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY signup.yaml
```

For a link instead of a code, use `ACTION: link` and then `- openLink: ${output.link}`, which opens it in the app. If the email has both a web link and one that opens the app (`myapp://…`), add `APP_LINK: true` to the script’s `env` for the app one.

## Check the email, not just the code

```yaml
- runScript:
    file: proxymail.js
    env:
      ACTION: count
- assertTrue: ${output.count == 1}
- runScript:
    file: proxymail.js
    env:
      ACTION: check
      EXPECT_HOST: links.example.com
```

`ACTION: check` fails the flow, naming what failed, when the newest email fails a check: SPF, DKIM and DMARC, links to localhost or plain http, where its link goes (`EXPECT_HOST`, the domain your app’s links use), a stated expiry, its size and more ([what each check means](https://proxymail.qa/docs/rest-api#checking-an-emails-sending-and-build)). `ACTION: none` fails it if an email arrives within 30 seconds, for “an unsubscribed user gets nothing”. After a resend, `UNUSED: true` on `ACTION: code` skips the code already used.

On `ACTION: create`, `ENVIRONMENT: staging` records which environment the account is for, and `AGE: under18` or `COUNTRY: GB` give its persona a date of birth or a country, for forms that ask.

## Sign in as a saved account, or borrow one

For a sign-in flow, `ACTION: signin` with `TAGS: verified` sets `output.address` and `output.password` to a saved test account’s. When flows run in parallel, `ACTION: checkout` with `TAGS: verified` lends one no other flow has, with its password, and `ACTION: return` gives it back (`ADD_TAGS` and `REMOVE_TAGS` record what the flow changed):

```yaml
- runScript:
    file: proxymail.js
    env:
      ACTION: checkout
      TAGS: verified
# …sign in with ${output.address} and ${output.password}…
- runScript:
    file: proxymail.js
    env:
      ACTION: return
```

Both read saved passwords, so the key needs **Can read saved passwords** (Project settings → API keys). Tag accounts with their state in the app to make pools: see [an account pool](https://proxymail.qa/docs/rest-api#an-account-pool-for-parallel-tests).

## A report on the pull request

With the [GitHub Action](https://proxymail.qa/docs/ci#a-report-on-the-pull-request) in the job, pass the run it starts like the key, and add `TEST` to each script’s `env` to name the step:

Terminal window

```bash
maestro test -e PROXYMAIL_QA_API_KEY=$PROXYMAIL_QA_API_KEY -e PROXYMAIL_QA_RUN=$PROXYMAIL_QA_RUN signup.yaml
```

[Previous Overview](https://proxymail.qa/docs/mobile.html) [Next iOS (XCUITest)](https://proxymail.qa/docs/xcuitest.html)
