diff --git a/app/api/push/preview/route.ts b/app/api/push/preview/route.ts index 5a0578a36..2099f3258 100644 --- a/app/api/push/preview/route.ts +++ b/app/api/push/preview/route.ts @@ -9,6 +9,7 @@ import { type StalwartCredentials, } from '@/lib/stalwart/credentials'; import { fetchJmapServer, isTrustedJmapServerUrl } from '@/lib/stalwart/server-fetch'; +import { rebaseApiUrl } from '@/lib/stalwart/jmap-api'; import { DisallowedUrlError } from '@/lib/security/url-guard'; export const runtime = 'nodejs'; @@ -56,7 +57,9 @@ async function resolveTargetForAccount(accountId: string): Promise; }; const mailAccountId = session.primaryAccounts?.['urn:ietf:params:jmap:mail']; - if (!session.apiUrl || !mailAccountId) return null; + // On the host this process reaches, not the server's public one (#627). + const apiUrl = rebaseApiUrl(session, serverUrl); + if (!apiUrl || !mailAccountId) return null; // Shared and group mailboxes are never the primary mail account - // they only appear in session.accounts. Accept those too, or the // SW falls back to a generic "New mail" notification for them. (#839) @@ -65,7 +68,7 @@ async function resolveTargetForAccount(accountId: string): Promise; }; - const apiUrl = session.apiUrl; + const apiUrl = rebaseApiUrl(session, creds.serverUrl); const accountId = session.primaryAccounts?.['urn:ietf:params:jmap:mail']; if (!apiUrl || !accountId) return null; return { authHeader: creds.authHeader, apiUrl, accountId, trusted: creds.trusted }; diff --git a/lib/__tests__/push-preview-route.test.ts b/lib/__tests__/push-preview-route.test.ts index f6d846494..cf96df827 100644 --- a/lib/__tests__/push-preview-route.test.ts +++ b/lib/__tests__/push-preview-route.test.ts @@ -214,3 +214,18 @@ it('names no message when the delivered one cannot be read, rather than another' status: 200, body: { email: null, unreadTotal: 3, slot: 0 }, }); }); + +it('sends the method calls to the server address it was configured with, not the public one', async () => { + // Stalwart advertises its own public hostname in apiUrl, which the webmail + // may not reach when it talks to the server over an internal address (#627). + fetchJmapServer.mockReset(); + mockJmap(); + const session = { ...SESSION, apiUrl: 'https://public.example.org/jmap/' }; + const reply = fetchJmapServer.getMockImplementation()!; + fetchJmapServer.mockImplementation(async (url: string, init?: { body?: string }) => + url.endsWith('/.well-known/jmap') ? jsonResponse(session) : reply(url, init)); + + expect((await callRoute('a')).status).toBe(200); + const posted = fetchJmapServer.mock.calls.map(([url]) => url).filter((url) => !url.endsWith('/.well-known/jmap')); + expect(posted).toEqual(['https://mail.example.com/jmap/', 'https://mail.example.com/jmap/']); +});