cluster:developer CI/CD GitHub Actions Email testing API

Using a Disposable Inbox in CI/CD Pipelines (GitHub Actions & More)

7 min read

Using a Disposable Inbox in CI/CD Pipelines (GitHub Actions & More)

A disposable inbox in CI/CD should isolate each test run, not disappear before its verification email arrives. Give every job a unique recipient, trigger mail from your own staging application, and wait for the specific message with a strict deadline and request budget. Keep credentials and login links out of logs. With Inboxto, persistent addresses and custom domains can support that design, but API permissions, retention, and delivery still need checking.

This guide combines a GitHub Actions wiring example with an Inboxto-specific capacity calculation. It is a review of public documentation, not a report of a live authenticated integration test. The example deliberately leaves your application and provider adapter implementation explicit.

Why email tests fail even when the application works

An assertion that passes locally can become unreliable in a matrix build. Two workers reading the same inbox may accept each other's message. A rerun may find yesterday's verification email. A fixed sleep can finish before delivery, while aggressive polling can exhaust the account's allowance before the last job starts.

Separate three identities: the workflow run, the recipient, and the message. Include the repository identity, run ID, run attempt, matrix case, and a fresh random suffix in your test namespace. GitHub's variables reference explains that GITHUB_RUN_ID stays the same on reruns, while GITHUB_RUN_ATTEMPT increases. A run ID alone therefore does not distinguish retries.

Create the recipient before triggering the application. Record a test-start timestamp and a unique correlation marker that your staging application places in the message. Match recipient, expected sender, marker, and timestamp together. A subject such as “Verify your email” is not sufficient evidence.

For the broader architecture, start with Inboxto's developer guide.

A GitHub Actions skeleton with an honest adapter boundary

Run credential-bearing email checks only against code and infrastructure you trust. This illustrative workflow runs on pushes to a protected main branch; it is not a safe way to test arbitrary fork code with secrets. Replace the checkout marker with a reviewed full commit SHA before use, and implement the named script in your own repository.

name: Staging email contract
on:
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  email:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    strategy:
      max-parallel: 2
      matrix:
        case: [signup, reset]
    steps:
      - uses: actions/checkout@REPLACE_WITH_REVIEWED_FULL_COMMIT_SHA
      - name: Check staging email
        env:
          INBOX_API_KEY: ${{ secrets.INBOX_API_KEY }}
          TEST_CASE: ${{ matrix.case }}
          TEST_MAIL_DOMAIN: ${{ vars.TEST_MAIL_DOMAIN }}
        run: python3 scripts/ci_email_check.py

INBOX_API_KEY, TEST_CASE, and TEST_MAIL_DOMAIN are example variables for your script, not an Inboxto SDK interface. Read the key inside the process and send it only to the intended API host. Do not print the environment, request headers, OTPs, or message bodies. GitHub's secrets documentation notes that ordinary secrets are not passed to fork-triggered workflows and recommends avoiding command-line secret arguments where possible. Do not weaken that boundary to make a test pass.

The script should implement this contract; the following is pseudocode, not a shipped client:

namespace = repository + run_id + run_attempt + case + random_suffix
try:
    recipient = provision_owned_test_recipient(namespace)
    started_at = now()
    marker = random_correlation_marker()
    trigger_owned_staging_application(recipient, marker)
    repeat within both deadline and shared request budget:
        messages = list_messages_for_exact_recipient(recipient)
        match = find_expected_sender_marker_and_timestamp(messages, marker, started_at)
        if match:
            assert_expected_staging_behavior(match)
            record_redacted_success()
            stop
        wait_with_backoff_and_jitter()
    fail_with_redacted_diagnostic_if_no_match()
finally:
    cleanup_job_owned_messages_and_app_fixtures_using_supported_operations()

Keep cleanup in a finally-style path, including after assertion failures. If provisioning times out, inspect the exact recipient before attempting creation again. A lost response does not prove the first request failed. Never delete a shared inbox to tidy up one job. The cited API reference documents message deletion, not address deletion; retire addresses through supported account-management operations rather than inventing an endpoint.

Map the contract to documented Inboxto behavior

The public Inboxto API reference, checked on November 9, 2026, documents the base URL https://inboxto.app/api and bearer-key authentication. It lists POST /v1/addresses with an explicit address parameter and addresses:write permission. It does not describe that operation as a random-address allocator: your adapter must choose an address that your account and domain configuration permit.

For reading, GET /v1/messages accepts an address filter plus pagination parameters and requires messages:read. Match against the returned to, from, subject, received_at, and body fields as appropriate. Verify pagination and response shape in your own authorized environment before relying on the adapter.

Avoid using POST /v1/messages/pop as a general parallel-test reader. The reference describes it as retrieving and deleting the latest unread message. That destructive queue behavior is different from finding the message belonging to a particular assertion. Prefer listing, matching, and then deleting only a message that your job owns, if your retention policy calls for deletion and your key has the required permission.

These are documented contracts, not a promise that every account has the same enabled features. The current public homepage places API access and custom domains in Premium. Free provides 3 persistent addresses, 7-day ordinary-message retention, and up to 5 permanently saved starred messages; Premium lists 90-day ordinary-message retention. Persistent addresses do not mean every message is retained forever.

That distinction matters for a failed Friday build investigated later: a stable recipient may remain available even after its ordinary messages expire. Preserve a redacted diagnostic at test time rather than relying on the mailbox as an indefinite CI archive. See the API getting-started guide for the wider setup context.

Budget requests before increasing parallelism

Here is a concrete planning check based on Inboxto's public reference. It currently states a default allowance of 100 requests per hour and documents HTTP 429 for rate limiting. Treat the actual account's headers, plan, and current documentation as authoritative; this is not a measured throughput guarantee.

Suppose each job uses one address-creation call, at most six list calls, and one message-deletion call. That is 8 requests per job before any extra detail lookup, retry, or provisioning check. Ten jobs would budget 80 requests; twenty would require 160, exceeding the documented default if they share that allowance within the hour. Slowing simultaneous jobs does not reduce the total requests they consume.

Decision Practical consequence
Increase matrix size Recalculate the shared hourly budget, not just per-job delay.
Receive HTTP 429 Honor a valid Retry-After if supplied; otherwise use bounded backoff and the overall deadline.
Receive HTTP 401 or 403 Stop and diagnose authentication or permissions; do not keep polling.
Reach the deadline Fail with recipient namespace and redacted timing/status information, not an inbox dump.

A maximum of six polls is an illustrative budget, not a universal delivery SLA. Choose intervals and deadlines from your own measured staging behavior. Coordinate the allowance across runners; separate local counters cannot enforce one shared account budget.

Parallel aliases, safe assertions, and the release gate

A verified catch-all domain can simplify unique recipient naming, but only if routing and provider filtering actually preserve isolation. Confirm that each generated address reaches the intended inbox and can be filtered precisely. Catch-all routing does not itself prove that arbitrary aliases are accepted by the API or create independent mailboxes. Follow the catch-all domain guide and test your specific configuration first.

Persistent recipients are useful for deliberately repeated scenarios, while fresh aliases reduce accidental reuse. Neither a custom domain nor a persistent address guarantees delivery or bypasses another platform's restrictions. Test your own application, not bulk third-party signups.

If the message contains a verification link, parse it and allowlist the expected staging scheme, host, and path before following it. Disable automatic redirects, or validate every redirect destination against the same staging allowlist. Never send the Inboxto API key to the verification-link endpoint. Never fetch arbitrary message URLs, and never point the test at production account recovery. The confirmation-email testing walkthrough expands that boundary.

Before making the check a release gate, confirm four things: recipients remain isolated during parallel runs; stale messages cannot satisfy assertions; failures and cleanup are bounded; and logs reveal no secrets or login tokens. First validate the adapter in a controlled staging run. Then make a small, measured email contract part of CI rather than turning every build into an unbounded mailbox load test.

Open Inboxto to assess the inbox workflow, then confirm API access, domain setup, and your request budget before connecting it to a trusted pipeline.

Ready to Protect Your Privacy?

Create your first anonymous email address in seconds. No registration required.

Get Started