MailSlurp logo

Waiting for matching entities

MailSlurp documentation.

View Markdown Agent setup

How to wait and search for matching emails, TXT messages, and events.

Info. Email and SMS delivery is asynchronous. In tests and production workflows use sufficient wait timeouts and, when needed, retry wrappers so short delivery delays do not cause flaky failures.

Waiting for messages

MailSlurp provides convenient methods for waiting for emails, SMS, and events to arrive. You can use these methods to wait for messages to arrive in tests or to wait for messages to arrive in production applications. You can also match based on the message content or metadata.

Examples

Here are examples of how to fetch emails in code.

Creating a client

First install a MailSlurp SDK for your language. Then create a client instance using your API key. You can find your API key on the MailSlurp dashboard.

const {MailSlurp} = await import("mailslurp-client");
const mailslurp = new MailSlurp({
    apiKey: YOUR_API_KEY
});
const inbox = await mailslurp.createInboxWithOptions({
    // expires in 5 minutes
    expiresIn: 300_000
});
expect(inbox.emailAddress).toContain("@mailslurp");

Wait for latest email

The default wait method is to wait for the latest email. This means the first email matching the conditions. The client will hold a connection until the conditions are met.

// send an email
await mailslurp.inboxController.sendEmailAndConfirm({
    inboxId: inbox.id,
    sendEmailOptions: {
        to: [inbox.emailAddress],
        subject: "First email",
    }
})
// wait for the first unread email to arrive
const email = await mailslurp.waitController.waitForLatestEmail({
    timeout: 120_000,
    inboxId: inbox.id,
    unreadOnly: true
})
expect(email.subject).toContain('First email')

In this example we create an inbox and then send an email from it to itself. Next we wait for the email to arrive and make assertions about its subject.

Wait for matching emails

A more complex example is to wait for messages that match particular criteria, such as subject line.

// send two emails
for (const i of [1, 2]) {
    await mailslurp.inboxController.sendEmailAndConfirm({
        inboxId: inbox.id,
        sendEmailOptions: {
            to: [inbox.emailAddress],
            // send a different message each time
            subject: `Match subject test-${i}`,
        }
    })
}
// wait for 2 emails matching the subject with a pattern
const emails = await mailslurp.waitController.waitForMatchingEmails({
    inboxId: inbox.id,
    timeout: 120_000,
    unreadOnly: true,
    count: 2,
    matchOptions: {
        matches: [
            {
                // expect subject to contain "Match subject"
                value: "Match subject",
                field: MatchOptionFieldEnum.SUBJECT,
                should: MatchOptionShouldEnum.CONTAIN
            }
        ]
    }
})
// assert we received two emails matching the subject
expect(emails.length).toEqual(2)
// the subjects contain the test number from the loop
expect(emails.filter(it => it.subject.includes("Match subject test-1")).length).toEqual(1)

For stricter matching, use the generic wait endpoint when you need one request body that combines count, timeout, unread state, sort order, and field-level match rules.

POST /waitFor

Wait for an email to match the provided filter conditions such as subject contains keyword.

Generic waitFor method that will wait until an inbox meets given conditions or return immediately if already met

Request, parameters, and responses

Request body (required)

WaitForConditions application/json
FieldTypeRequiredDescription
inboxIdstring:uuidYesID of inbox to search within and apply conditions to. Essentially filtering the emails found to give a count.
countinteger:int32NoNumber of results that should match conditions. Either exactly or at least this amount based on the `countType`. If count condition is not met and the timeout has not been reached the `waitFor` method will retry the operation.
delayTimeoutinteger:int64NoMax time in milliseconds to wait between retries if a `timeout` is specified.
timeoutinteger:int64YesMax time in milliseconds to retry the `waitFor` operation until conditions are met.
unreadOnlybooleanNoApply conditions only to **unread** emails. All emails begin with `read=false`. An email is marked `read=true` when an `EmailDto` representation of it has been returned to the user at least once. For example you have called `getEmail` or `waitForLatestEmail` etc., or you have viewed the email in the dashboard.
countTypeenum: EXACTLY | ATLEASTNoHow result size should be compared with the expected size. Exactly or at-least matching result?
matchesMatchOption[]NoConditions that should be matched for an email to qualify for results. Each condition will be applied in order to each email within an inbox to filter a result list of matching emails you are waiting for.
sortDirectionenum: ASC | DESCNoDirection to sort matching emails by created time
sincestring:date-timeNoISO Date Time earliest time of email to consider. Filter for matching emails that were received after this date
beforestring:date-timeNoISO Date Time latest time of email to consider. Filter for matching emails that were received before this date
Request example
{
  "inboxId": "00000000-0000-4000-8000-000000000000",
  "timeout": 1,
  "count": 1,
  "delayTimeout": 1,
  "unreadOnly": true,
  "countType": "EXACTLY"
}

Responses

StatusSchemaDescription
200EmailPreview[]OK
HTTP and SDK snippets

HTTP

HTTP
POST /waitFor HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: application/json
Content-Type: application/json

{
  "inboxId": "00000000-0000-4000-8000-000000000000",
  "timeout": 1,
  "count": 1,
  "delayTimeout": 1,
  "unreadOnly": true,
  "countType": "EXACTLY"
}

cURL

cURL
curl -X POST "https://api.mailslurp.com/waitFor" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"inboxId":"00000000-0000-4000-8000-000000000000","timeout":1,"count":1,"delayTimeout":1,"unreadOnly":true,"countType":"EXACTLY"}'

JavaScript SDK

JavaScript SDK
import { Configuration, WaitForControllerApi } from "mailslurp-client";

const config = new Configuration({ apiKey: "YOUR_API_KEY" });
const waitForController = new WaitForControllerApi(config);
const request = {
  "waitForConditions": {
    "inboxId": "00000000-0000-4000-8000-000000000000",
    "timeout": 1,
    "count": 1,
    "delayTimeout": 1,
    "unreadOnly": true,
    "countType": "EXACTLY"
  }
};

const result = await waitForController.waitFor(request);

Python SDK

Python SDK
import mailslurp_client
from mailslurp_client.api.wait_for_controller_api import WaitForControllerApi

configuration = mailslurp_client.Configuration()
configuration.api_key["x-api-key"] = "YOUR_API_KEY"

with mailslurp_client.ApiClient(configuration) as api_client:
    waitForController = WaitForControllerApi(api_client)
    wait_for_conditions = {
      "inboxId": "00000000-0000-4000-8000-000000000000",
      "timeout": 1,
      "count": 1,
      "delayTimeout": 1,
      "unreadOnly": True,
      "countType": "EXACTLY"
    }
    result = waitForController.wait_for(wait_for_conditions)
POST /waitForMatchingEmails

Wait or return list of emails that match simple matching patterns

Perform a search of emails in an inbox with the given patterns. If results match expected count then return or else retry the search until results are found or timeout is reached. Match options allow simple CONTAINS or EQUALS filtering on SUBJECT, TO, BCC, CC, and FROM. See the `MatchOptions` object for options. An example payload is `{ matches: [{field: 'SUBJECT',should:'CONTAIN',value:'needle'}] }`. You can use an array of matches and they will be applied sequentially to filter out emails. If you want to perform matches and extractions of content using Regex patterns see the EmailController `getEmailContentMatch` method.

Request, parameters, and responses

Query parameters

NameTypeRequiredDescription
inboxIdstring:uuidYesId of the inbox we are fetching emails from
countinteger:int32YesNumber of emails to wait for. Must be greater or equal to 1
beforestring:date-timeNoFilter for emails that were received before the given timestamp
sincestring:date-timeNoFilter for emails that were received after the given timestamp
sortenum: ASC | DESCNoSort directionValues: ASC, DESC
delayinteger:int64NoMax milliseconds delay between calls
timeoutinteger:int64NoMax milliseconds to wait
unreadOnlybooleanNoOptional filter for unread only

Request body (required)

MatchOptions application/json
FieldTypeRequiredDescription
matchesMatchOption[]NoZero or more match options such as `{ field: 'SUBJECT', should: 'CONTAIN', value: 'Welcome' }`. Options are additive so if one does not match the email is excluded from results
conditionsConditionOption[]NoZero or more conditions such as `{ condition: 'HAS_ATTACHMENTS', value: 'TRUE' }`. Note the values are the strings `TRUE|FALSE` not booleans.
Request example
{
  "matches": [
    {
      "field": "SUBJECT",
      "should": "MATCH",
      "value": "value"
    }
  ],
  "conditions": [
    {
      "condition": "HAS_ATTACHMENTS",
      "value": "TRUE"
    }
  ]
}

Responses

StatusSchemaDescription
200EmailPreview[]OK
HTTP and SDK snippets

HTTP

HTTP
POST /waitForMatchingEmails?inboxId=00000000-0000-4000-8000-000000000000&count=value&before=value HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: application/json
Content-Type: application/json

{
  "matches": [
    {
      "field": "SUBJECT",
      "should": "MATCH",
      "value": "value"
    }
  ],
  "conditions": [
    {
      "condition": "HAS_ATTACHMENTS",
      "value": "TRUE"
    }
  ]
}

cURL

cURL
curl -X POST "https://api.mailslurp.com/waitForMatchingEmails?inboxId=00000000-0000-4000-8000-000000000000&count=value&before=value" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"matches":[{"field":"SUBJECT","should":"MATCH","value":"value"}],"conditions":[{"condition":"HAS_ATTACHMENTS","value":"TRUE"}]}'

JavaScript SDK

JavaScript SDK
import { Configuration, WaitForControllerApi } from "mailslurp-client";

const config = new Configuration({ apiKey: "YOUR_API_KEY" });
const waitForController = new WaitForControllerApi(config);
const request = {
  "inboxId": "00000000-0000-4000-8000-000000000000",
  "count": null,
  "before": "value",
  "matchOptions": {
    "matches": [
      {
        "field": "SUBJECT",
        "should": "MATCH",
        "value": "value"
      }
    ],
    "conditions": [
      {
        "condition": "HAS_ATTACHMENTS",
        "value": "TRUE"
      }
    ]
  }
};

const result = await waitForController.waitForMatchingEmails(request);

Python SDK

Python SDK
import mailslurp_client
from mailslurp_client.api.wait_for_controller_api import WaitForControllerApi

configuration = mailslurp_client.Configuration()
configuration.api_key["x-api-key"] = "YOUR_API_KEY"

with mailslurp_client.ApiClient(configuration) as api_client:
    waitForController = WaitForControllerApi(api_client)
    match_options = {
      "matches": [
        {
          "field": "SUBJECT",
          "should": "MATCH",
          "value": "value"
        }
      ],
      "conditions": [
        {
          "condition": "HAS_ATTACHMENTS",
          "value": "TRUE"
        }
      ]
    }
    result = waitForController.wait_for_matching_emails(inbox_id="00000000-0000-4000-8000-000000000000", count=NaN, before="value", match_options)

Wait for plus-addressed emails

If you route multiple workflows through one inbox using plus aliases, match on the full recipient address in the TO field. This lets you wait for one alias deterministically without scanning large inbox result sets.

BASE_URL="https://api.mailslurp.com"
API_KEY="REPLACE_WITH_API_KEY"
INBOX_ID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
FULL_PLUS_ADDRESS="app-user+signup-9f3a@example.test"

curl -sS -X POST "$BASE_URL/waitFor" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -H "accept: application/json" \
  --data "{
    \"inboxId\": \"$INBOX_ID\",
    \"count\": 1,
    \"countType\": \"EXACTLY\",
    \"timeout\": 180000,
    \"delayTimeout\": 500,
    \"unreadOnly\": true,
    \"sortDirection\": \"DESC\",
    \"matches\": [
      {
        \"field\": \"TO\",
        \"should\": \"CONTAIN\",
        \"value\": \"$FULL_PLUS_ADDRESS\"
      }
    ]
  }"

For a full plus-address lifecycle (get/create alias, wait, then list by alias ID) see plus addressing.

Wait for email number

We can also wait for the "nth" email, an email at a particular index. This is useful for testing pagination or other scenarios where you want to wait for a particular email.

const emailCount = await mailslurp.inboxController.getInboxEmailCount({inboxId: inbox.id})
const indexOfEmail0Based = emailCount.totalElements
await mailslurp.sendEmail(inbox.id, {
    to: [inbox.emailAddress],
    subject: "Next email"
})
const nthEmail = await mailslurp.waitController.waitForNthEmail({
    inboxId: inbox.id,
    index: indexOfEmail0Based
})
expect(nthEmail.subject).toContain('Next email');

Wait for SMS

You can also wait for inbound SMS messages on phone numbers using wait-for methods.

const sms = await mailslurp.waitController.waitForLatestSms({
  waitForSingleSmsOptions: {
    phoneNumberId: phone.id,
    timeout: 30_000,
    unreadOnly: true,
  },
});
expect(sms.body).toContain('Here is your code');
expect(sms.fromNumber).toEqual('+13252527014');

SMS waits use the same deterministic pattern as email waits. After a message arrives you can also extract verification codes with the SMS controller.

POST /waitForSms

Wait for an SMS message to match the provided filter conditions such as body contains keyword.

Generic waitFor method that will wait until a phone number meets given conditions or return immediately if already met

Request, parameters, and responses

Request body (required)

WaitForSmsConditions application/json
FieldTypeRequiredDescription
phoneNumberIdstring:uuidYesID of phone number to search within and apply conditions to. Essentially filtering the SMS found to give a count.
limitinteger:int32NoLimit results
countinteger:int64YesNumber of results that should match conditions. Either exactly or at least this amount based on the `countType`. If count condition is not met and the timeout has not been reached the `waitFor` method will retry the operation.
delayTimeoutinteger:int64NoMax time in milliseconds to wait between retries if a `timeout` is specified.
timeoutinteger:int64YesMax time in milliseconds to retry the `waitFor` operation until conditions are met.
unreadOnlybooleanNoApply conditions only to **unread** SMS. All SMS messages begin with `read=false`. An SMS is marked `read=true` when an `SMS` has been returned to the user at least once. For example you have called `getSms`, or you have viewed the SMS in the dashboard.
countTypeenum: EXACTLY | ATLEASTNoHow result size should be compared with the expected size. Exactly or at-least matching result?
matchesSmsMatchOption[]NoConditions that should be matched for an SMS to qualify for results. Each condition will be applied in order to each SMS within a phone number to filter a result list of matching SMSs you are waiting for.
sortDirectionenum: ASC | DESCNoDirection to sort matching SMSs by created time
sincestring:date-timeNoISO Date Time earliest time of SMS to consider. Filter for matching SMSs that were received after this date
beforestring:date-timeNoISO Date Time latest time of SMS to consider. Filter for matching SMSs that were received before this date
Request example
{
  "phoneNumberId": "00000000-0000-4000-8000-000000000000",
  "count": 1,
  "timeout": 1,
  "limit": 1,
  "delayTimeout": 1,
  "unreadOnly": true
}

Responses

StatusSchemaDescription
200SmsPreview[]OK
HTTP and SDK snippets

HTTP

HTTP
POST /waitForSms HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: application/json
Content-Type: application/json

{
  "phoneNumberId": "00000000-0000-4000-8000-000000000000",
  "count": 1,
  "timeout": 1,
  "limit": 1,
  "delayTimeout": 1,
  "unreadOnly": true
}

cURL

cURL
curl -X POST "https://api.mailslurp.com/waitForSms" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"phoneNumberId":"00000000-0000-4000-8000-000000000000","count":1,"timeout":1,"limit":1,"delayTimeout":1,"unreadOnly":true}'

JavaScript SDK

JavaScript SDK
import { Configuration, WaitForControllerApi } from "mailslurp-client";

const config = new Configuration({ apiKey: "YOUR_API_KEY" });
const waitForController = new WaitForControllerApi(config);
const request = {
  "waitForSmsConditions": {
    "phoneNumberId": "00000000-0000-4000-8000-000000000000",
    "count": 1,
    "timeout": 1,
    "limit": 1,
    "delayTimeout": 1,
    "unreadOnly": true
  }
};

const result = await waitForController.waitForSms(request);

Python SDK

Python SDK
import mailslurp_client
from mailslurp_client.api.wait_for_controller_api import WaitForControllerApi

configuration = mailslurp_client.Configuration()
configuration.api_key["x-api-key"] = "YOUR_API_KEY"

with mailslurp_client.ApiClient(configuration) as api_client:
    waitForController = WaitForControllerApi(api_client)
    wait_for_sms_conditions = {
      "phoneNumberId": "00000000-0000-4000-8000-000000000000",
      "count": 1,
      "timeout": 1,
      "limit": 1,
      "delayTimeout": 1,
      "unreadOnly": True
    }
    result = waitForController.wait_for_sms(wait_for_sms_conditions)
POST /waitForLatestSms

Wait for the latest SMS message to match the provided filter conditions such as body contains keyword.

Wait until a phone number meets given conditions or return immediately if already met

Request, parameters, and responses

Request body (required)

WaitForSingleSmsOptions application/json
FieldTypeRequiredDescription
phoneNumberIdstring:uuidYes
timeoutinteger:int64Yes
unreadOnlybooleanNo
beforestring:date-timeNo
sincestring:date-timeNo
sortDirectionenum: ASC | DESCNo
delayinteger:int64No
Request example
{
  "phoneNumberId": "00000000-0000-4000-8000-000000000000",
  "timeout": 1,
  "unreadOnly": true,
  "before": "2026-06-21T00:00:00.000Z",
  "since": "2026-06-21T00:00:00.000Z",
  "sortDirection": "ASC"
}

Responses

StatusSchemaDescription
200SmsDtoOK
HTTP and SDK snippets

HTTP

HTTP
POST /waitForLatestSms HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: application/json
Content-Type: application/json

{
  "phoneNumberId": "00000000-0000-4000-8000-000000000000",
  "timeout": 1,
  "unreadOnly": true,
  "before": "2026-06-21T00:00:00.000Z",
  "since": "2026-06-21T00:00:00.000Z",
  "sortDirection": "ASC"
}

cURL

cURL
curl -X POST "https://api.mailslurp.com/waitForLatestSms" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"phoneNumberId":"00000000-0000-4000-8000-000000000000","timeout":1,"unreadOnly":true,"before":"2026-06-21T00:00:00.000Z","since":"2026-06-21T00:00:00.000Z","sortDirection":"ASC"}'

JavaScript SDK

JavaScript SDK
import { Configuration, WaitForControllerApi } from "mailslurp-client";

const config = new Configuration({ apiKey: "YOUR_API_KEY" });
const waitForController = new WaitForControllerApi(config);
const request = {
  "waitForSingleSmsOptions": {
    "phoneNumberId": "00000000-0000-4000-8000-000000000000",
    "timeout": 1,
    "unreadOnly": true,
    "before": "2026-06-21T00:00:00.000Z",
    "since": "2026-06-21T00:00:00.000Z",
    "sortDirection": "ASC"
  }
};

const result = await waitForController.waitForLatestSms(request);

Python SDK

Python SDK
import mailslurp_client
from mailslurp_client.api.wait_for_controller_api import WaitForControllerApi

configuration = mailslurp_client.Configuration()
configuration.api_key["x-api-key"] = "YOUR_API_KEY"

with mailslurp_client.ApiClient(configuration) as api_client:
    waitForController = WaitForControllerApi(api_client)
    wait_for_single_sms_options = {
      "phoneNumberId": "00000000-0000-4000-8000-000000000000",
      "timeout": 1,
      "unreadOnly": True,
      "before": "2026-06-21T00:00:00.000Z",
      "since": "2026-06-21T00:00:00.000Z",
      "sortDirection": "ASC"
    }
    result = waitForController.wait_for_latest_sms(wait_for_single_sms_options)
POST /sms/{smsId}/codes

Extract verification codes from an SMS

Extract one-time passcodes and verification tokens from SMS body content. Deterministic `PATTERN` extraction is available now. Use method flags to control fallback behavior for QA.

Request, parameters, and responses

Path parameters

NameTypeRequiredDescription
smsIdstring:uuidYesID of SMS to extract codes from

Request body

ExtractCodesOptions application/json
FieldTypeRequiredDescription
methodenum: AUTO | PATTERN | LLM | OCR | OCR_THEN_LLMNoExtraction strategy for verification codes. Unsupported strategies may fall back when allowFallback is true.
allowFallbackbooleanNoAllow fallback to deterministic pattern extraction when the selected method is unavailable.
minLengthinteger:int32NoMinimum code length to consider. Typical OTP values are between 4 and 8 characters.
maxLengthinteger:int32NoMaximum code length to consider.
maxCandidatesinteger:int32NoMaximum number of code candidates to return. Best candidate is also returned separately.
customPatternsstring[]NoOptional custom regex patterns for code extraction. Each pattern should have either one capture group for the code or match the full code directly.
Request example
{
  "method": "AUTO",
  "allowFallback": true,
  "minLength": 4,
  "maxLength": 10,
  "maxCandidates": 5,
  "customPatterns": [
    "value"
  ]
}

Responses

StatusSchemaDescription
200ExtractCodesResultOK
HTTP and SDK snippets

HTTP

HTTP
POST /sms/00000000-0000-4000-8000-000000000000/codes HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: application/json
Content-Type: application/json

{
  "method": "AUTO",
  "allowFallback": true,
  "minLength": 4,
  "maxLength": 10,
  "maxCandidates": 5,
  "customPatterns": [
    "value"
  ]
}

cURL

cURL
curl -X POST "https://api.mailslurp.com/sms/00000000-0000-4000-8000-000000000000/codes" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"method":"AUTO","allowFallback":true,"minLength":4,"maxLength":10,"maxCandidates":5,"customPatterns":["value"]}'

JavaScript SDK

JavaScript SDK
import { Configuration, SmsControllerApi } from "mailslurp-client";

const config = new Configuration({ apiKey: "YOUR_API_KEY" });
const smsController = new SmsControllerApi(config);
const request = {
  "smsId": "00000000-0000-4000-8000-000000000000",
  "extractCodesOptions": {
    "method": "AUTO",
    "allowFallback": true,
    "minLength": 4,
    "maxLength": 10,
    "maxCandidates": 5,
    "customPatterns": [
      "value"
    ]
  }
};

const result = await smsController.getSmsCodes(request);

Python SDK

Python SDK
import mailslurp_client
from mailslurp_client.api.sms_controller_api import SmsControllerApi

configuration = mailslurp_client.Configuration()
configuration.api_key["x-api-key"] = "YOUR_API_KEY"

with mailslurp_client.ApiClient(configuration) as api_client:
    smsController = SmsControllerApi(api_client)
    extract_codes_options = {
      "method": "AUTO",
      "allowFallback": True,
      "minLength": 4,
      "maxLength": 10,
      "maxCandidates": 5,
      "customPatterns": [
        "value"
      ]
    }
    result = smsController.get_sms_codes("00000000-0000-4000-8000-000000000000", extract_codes_options)

Waiting longer

If network latency or provider delivery varies, wrap wait calls in a small retry utility that retries up to three times until no exception is thrown.

Javascript

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function retryWait<T>(fn: () => Promise<T>, attempts = 3, delayMs = 1500): Promise<T> {
  let lastError: unknown;
  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (attempt < attempts) await sleep(delayMs);
    }
  }
  throw lastError;
}

const email = await retryWait(() =>
  mailslurp.waitController.waitForLatestEmail({
    inboxId,
    timeout: 120000, // 2 minutes
    unreadOnly: true,
  })
);

Java

import java.util.concurrent.Callable;

public static <T> T retryWait(Callable<T> fn, int attempts, long delayMs) throws Exception {
  Exception lastError = null;
  for (int attempt = 1; attempt <= attempts; attempt++) {
    try {
      return fn.call();
    } catch (Exception ex) {
      lastError = ex;
      if (attempt < attempts) {
        Thread.sleep(delayMs);
      }
    }
  }
  throw lastError;
}

Email email = retryWait(
    () -> waitForControllerApi.waitForLatestEmail(
        inboxId, null, 120000L, true, null, null, null, null, null, null, null, null),
    3,
    1500L
);