{"record":{"id":"299b721bcd632375","repo":"TryGhost/Ghost","slug":"no-staff-invitation-email-found-for-email-deta","errorCode":null,"errorMessage":"No staff invitation email found for ${email}${detail}","messagePattern":"No staff invitation email found for (.+?)(.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"e2e/helpers/playwright/fixture.ts","lineNumber":200,"sourceCode":"        analyticsPage.header.waitFor({state: 'visible'}),\n        billingIframe.waitFor({state: 'visible'})\n    ]);\n\n    return {\n        page,\n        context,\n        ghostAccountOwner: authenticatedSession.ghostAccountOwner\n    };\n}\n\nasync function getInvitationToken(emailClient: EmailClient, email: string): Promise<string> {\n    let messages;\n\n    try {\n        messages = await emailClient.search({subject: 'has invited you to join', to: email});\n    } catch (error) {\n        const detail = error instanceof Error ? `: ${error.message}` : '';\n        throw new Error(`No staff invitation email found for ${email}${detail}`);\n    }\n\n    const message = messages[0];\n\n    if (!message) {\n        throw new Error(`No staff invitation email found for ${email}`);\n    }\n\n    const detailedMessage = await emailClient.getMessageDetailed(message);\n    const inviteUrl = new URL(extractInviteLink(detailedMessage));\n    const token = inviteUrl.pathname.split('/').filter(Boolean).at(-1);\n\n    if (!token) {\n        throw new Error(`No invitation token found in URL for ${email}`);\n    }\n\n    return token;\n}","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/e2e/helpers/playwright/fixture.ts#L182-L218","documentation":"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.","triggerScenarios":"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.","commonSituations":"Mailgun server fixture not started; wrong MAIL_* env config; mail API credentials expired; transient network failure to the mail backend in CI.","solutions":["Read detail in the message — it carries the original error (auth, timeout, connection refused).","Verify the mail server fixture (mailServer) is running and reachable before the test.","Confirm EmailClient credentials/config match the running mail backend.","Retry the search after a short backoff for transient mail-server hiccups."],"exampleFix":"// before\ntry {\n    messages = await emailClient.search({subject:'has invited you to join', to:email});\n} catch (error) {\n    throw new Error(`No staff invitation email found for ${email}${detail}`);\n}\n\n// after\nfor (let i = 0; i < 3; i++) {\n    try {\n        messages = await emailClient.search({subject:'has invited you to join', to:email});\n        break;\n    } catch (e) {\n        if (i === 2) throw new Error(`Mail search failed for ${email}: ${e.message}`);\n        await new Promise(r => setTimeout(r, 1000));\n    }\n}","handlingStrategy":"try-catch","validationCode":"async function mailReachable(c: EmailClient): Promise<boolean> {\n    try { await c.search({subject: 'healthcheck', to: 'nobody@example.com'}); return true; }\n    catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"let messages;\nfor (let i = 0; i < 3; i++) {\n    try {\n        messages = await emailClient.search({subject:'has invited you to join', to:email});\n        break;\n    } catch (e) {\n        if (i === 2) throw new Error(`Mail search failed for ${email}: ${e.message}`, {cause: e});\n        await new Promise(r => setTimeout(r, 1000));\n    }\n}","preventionTips":["Start and verify the mail fixture before tests that depend on invitations.","Keep MAIL_* env in sync with the running mail backend.","Retry transient mail-server failures before surfacing them as invite failures."],"tags":["e2e","email","invitations","mailgun","external-service"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}