TryGhost/Ghost · error · Error

No staff invitation email found for ${email}${detail}

Error message

No staff invitation email found for ${email}${detail}

What it means

getInvitationToken() calls emailClient.search({subject:'has invited you to join', to:email}). If search() rejects, the catch wraps the underlying error: 'No staff invitation email found for <email>: <detail>'. This is a search-layer failure (mail server unreachable, auth error, IMAP/Graph API exception), not an empty result — the empty-result case is error 114.

Source

Thrown at e2e/helpers/playwright/fixture.ts:200

        analyticsPage.header.waitFor({state: 'visible'}),
        billingIframe.waitFor({state: 'visible'})
    ]);

    return {
        page,
        context,
        ghostAccountOwner: authenticatedSession.ghostAccountOwner
    };
}

async function getInvitationToken(emailClient: EmailClient, email: string): Promise<string> {
    let messages;

    try {
        messages = await emailClient.search({subject: 'has invited you to join', to: email});
    } catch (error) {
        const detail = error instanceof Error ? `: ${error.message}` : '';
        throw new Error(`No staff invitation email found for ${email}${detail}`);
    }

    const message = messages[0];

    if (!message) {
        throw new Error(`No staff invitation email found for ${email}`);
    }

    const detailedMessage = await emailClient.getMessageDetailed(message);
    const inviteUrl = new URL(extractInviteLink(detailedMessage));
    const token = inviteUrl.pathname.split('/').filter(Boolean).at(-1);

    if (!token) {
        throw new Error(`No invitation token found in URL for ${email}`);
    }

    return token;
}

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Read detail in the message — it carries the original error (auth, timeout, connection refused).
  2. Verify the mail server fixture (mailServer) is running and reachable before the test.
  3. Confirm EmailClient credentials/config match the running mail backend.
  4. Retry the search after a short backoff for transient mail-server hiccups.

Example fix

// before
try {
    messages = await emailClient.search({subject:'has invited you to join', to:email});
} catch (error) {
    throw new Error(`No staff invitation email found for ${email}${detail}`);
}

// after
for (let i = 0; i < 3; i++) {
    try {
        messages = await emailClient.search({subject:'has invited you to join', to:email});
        break;
    } catch (e) {
        if (i === 2) throw new Error(`Mail search failed for ${email}: ${e.message}`);
        await new Promise(r => setTimeout(r, 1000));
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

async function mailReachable(c: EmailClient): Promise<boolean> {
    try { await c.search({subject: 'healthcheck', to: 'nobody@example.com'}); return true; }
    catch { return false; }
}

Try / catch

let messages;
for (let i = 0; i < 3; i++) {
    try {
        messages = await emailClient.search({subject:'has invited you to join', to:email});
        break;
    } catch (e) {
        if (i === 2) throw new Error(`Mail search failed for ${email}: ${e.message}`, {cause: e});
        await new Promise(r => setTimeout(r, 1000));
    }
}

Prevention

When it happens

Trigger: Mail server (Mailgun/test mailhog) is down or returns an error. EmailClient auth invalid. Network/timeout contacting the mail backend. Search query shape unsupported by the configured mail provider.

Common situations: Mailgun server fixture not started; wrong MAIL_* env config; mail API credentials expired; transient network failure to the mail backend in CI.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/299b721bcd632375. Report an issue: GitHub.