diff --git a/app/api/push/preview/route.ts b/app/api/push/preview/route.ts index f58e2243e..8be988743 100644 --- a/app/api/push/preview/route.ts +++ b/app/api/push/preview/route.ts @@ -161,10 +161,16 @@ export async function GET(request: NextRequest) { }; const inboxBody = inboxData.methodResponses.find( - ([method]) => method === 'Mailbox/query', + ([method, , callId]) => method === 'Mailbox/query' && callId === 'mb', )?.[1] as { ids?: string[] } | undefined; - const inboxId = inboxBody?.ids?.[0]; + // A method-level JMAP error still has HTTP 200. Do not turn it into + // an empty Inbox: the service worker would silently discard the push. + if (!Array.isArray(inboxBody?.ids)) { + return NextResponse.json({ error: 'JMAP mailbox query failed' }, { status: 502 }); + } + + const inboxId = inboxBody.ids[0]; const emailProperties = ['id', 'threadId', 'from', 'subject', 'preview', 'receivedAt']; if (!inboxId && !requestedEmailId) { @@ -239,6 +245,19 @@ export async function GET(request: NextRequest) { methodResponses: [string, Record, string][]; }; + // Missing/failed method responses are preview failures, not proof that + // there is no unread mail. A non-2xx response lets the SW show its fallback. + for (const call of methodCalls as [string, unknown, string][]) { + const response = data.methodResponses.find(([method, , callId]) => + method === call[0] && callId === call[2]); + const result = response?.[1]; + if (!result || (call[0] === 'Email/query' + ? !Array.isArray(result.ids) || typeof result.total !== 'number' + : !Array.isArray(result.list))) { + return NextResponse.json({ error: 'JMAP email query failed' }, { status: 502 }); + } + } + type EmailLite = { id: string; threadId: string; @@ -270,6 +289,13 @@ export async function GET(request: NextRequest) { unreadTotal = Math.max(unreadTotal, 1); } email = delivered; + } else if (requestedEmailId) { + // The server named the message that arrived and it could not be read - + // deleted since, or filed where this login cannot see it. "Newest unread + // in the Inbox" is then some other message, and announcing it would be + // confidently wrong. Name no message: the unread total still comes back, + // so the SW posts its generic toast instead of staying silent. + email = null; } // Some servers build the preview from HTML without skipping