fix: load push notification previews when the webmail reaches the mail server on an internal address

This commit is contained in:
Linus Rath
2026-10-26 11:33:37 +01:00
parent 1fc44b4813
commit 01a1dc47a6
2 changed files with 21 additions and 3 deletions
+6 -3
View File
@@ -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<ResolvedTarge
accounts?: Record<string, unknown>;
};
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<ResolvedTarge
Object.prototype.hasOwnProperty.call(session.accounts ?? {}, accountId);
if (mailAccountId !== accountId && !isSharedAccount) return null;
return {
target: { authHeader: ctx.authHeader, apiUrl: session.apiUrl, accountId, trusted, slot },
target: { authHeader: ctx.authHeader, apiUrl, accountId, trusted, slot },
own: mailAccountId === accountId,
};
} catch {
@@ -87,7 +90,7 @@ async function resolveDefaultTarget(creds: StalwartCredentials): Promise<Resolve
apiUrl?: string;
primaryAccounts?: Record<string, string>;
};
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 };
+15
View File
@@ -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/']);
});