fix: show the relay push still goes through after the mail server refused a new one

This commit is contained in:
Linus Rath
2026-10-29 02:35:36 +01:00
parent db47162367
commit d3d28d551e
3 changed files with 78 additions and 18 deletions
@@ -31,7 +31,7 @@ import {
revokePushDevice,
serverSupportsEmailPush,
setStoredRelayBaseUrl,
setupPushNotifications,
enablePushFromSettings,
getStoredPushTransport,
setStoredPushTransport,
type PushDevice,
@@ -137,11 +137,13 @@ export function NotificationSettings() {
const relayValid = isValidRelayUrl(trimmed);
const busy = pushStatus.kind === 'busy';
const refreshDevices = useCallback(async () => {
// `relay` is the relay an enable run just settled on: the field's state
// has not caught up with it yet.
const refreshDevices = useCallback(async (relay?: string) => {
if (!activeAccountId || !client) return;
setDevices({ kind: 'loading' });
try {
const list = await listPushDevices({ accountId: activeAccountId, relayBaseUrl: trimmed });
const list = await listPushDevices({ accountId: activeAccountId, relayBaseUrl: relay ?? trimmed });
setDevices({ kind: 'loaded', devices: list });
} catch {
setDevices({ kind: 'error' });
@@ -196,21 +198,19 @@ export function NotificationSettings() {
kind: 'busy',
message: t('settings.notifications.push.status_busy', 'Working…'),
});
try {
// The setup stores the relay itself, and keeps the one in use when the
// mail server refuses the new one's address.
await setupPushNotifications({
relayBaseUrl: trimmed,
accountLabel: username ?? undefined,
forceRecreate,
});
setPushEnabled(true);
setPushStatus({ kind: 'enabled' });
} catch (error) {
setPushStatus({ kind: 'error', message: describeError(error) });
} finally {
void refreshDevices();
}
// The setup stores the relay itself, and keeps the one in use when the
// mail server refuses the new one's address: the field follows it.
const outcome = await enablePushFromSettings({
relayBaseUrl: trimmed,
accountLabel: username ?? undefined,
forceRecreate,
});
setRelayUrl(outcome.relayBaseUrl);
setPushEnabled(outcome.enabled);
setPushStatus(outcome.error
? { kind: 'error', message: describeError(outcome.error) }
: { kind: 'enabled' });
void refreshDevices(outcome.relayBaseUrl);
};
// Switching the transport while push is on re-registers immediately so the
@@ -110,6 +110,7 @@ import {
setupPushNotifications,
deviceClientIdKey,
disablePushForAccount,
enablePushFromSettings,
forgetPushServerState,
isValidRelayUrl,
mailAccountsWithoutPush,
@@ -420,6 +421,31 @@ describe('setupPushNotifications subscription shape', () => {
expect(fetchMock.mock.calls.some((c) => (c[1] as RequestInit | undefined)?.method === 'DELETE')).toBe(false);
});
it('hands the settings the relay in use back after the server refused a new one', async () => {
await AsyncStorage.setItem('push:relayBaseUrl:v1', RELAY);
await AsyncStorage.setItem(SUB_KEY, 'existing');
listMock.mockResolvedValue([{ id: 'existing', deviceClientId: OUR_DCID, expires: new Date(Date.now() + 5 * 86400000).toISOString(), types: ['EmailDelivery'] }]);
createMock.mockRejectedValueOnce(new JMAPSetError('invalidProperties', 'Host is not a public domain name', ['url']));
const outcome = await enablePushFromSettings({ relayBaseUrl: 'https://relay.home.lan/', forceRecreate: true });
expect(outcome.error).toBeInstanceOf(PushSetupError);
expect(outcome.relayBaseUrl).toBe(RELAY);
expect(outcome.enabled).toBe(true);
// Nothing in use yet: the typed relay stays, so it can be corrected.
await AsyncStorage.clear();
await AsyncStorage.setItem(deviceClientIdKey(ACCOUNT_ID), OUR_DCID);
listMock.mockResolvedValue([]);
createMock.mockRejectedValueOnce(new JMAPSetError('invalidProperties', 'Host is not a public domain name', ['url']));
expect(await enablePushFromSettings({ relayBaseUrl: 'https://relay.home.lan/' }))
.toMatchObject({ relayBaseUrl: 'https://relay.home.lan', enabled: false });
// A run that works shows the relay it registered with.
expect(await enablePushFromSettings({ relayBaseUrl: 'https://relay.example.com/' }))
.toEqual({ error: null, relayBaseUrl: RELAY, enabled: true });
});
it('rejects plain-http relay URLs', async () => {
await expect(setupPushNotifications({ relayBaseUrl: 'http://relay.example.com' })).rejects.toMatchObject({ phase: 'relay' });
expect(createMock).not.toHaveBeenCalled();
+34
View File
@@ -1706,6 +1706,40 @@ export async function resyncPushNotifications(
return setupPushNotifications(params);
}
export interface PushEnableOutcome {
error: unknown;
// The relay the settings should show afterwards.
relayBaseUrl: string;
// Whether the account has a subscription recorded once the run is over.
enabled: boolean;
}
/**
* The enable and re-register buttons of the notification settings: run the
* setup and say what it left behind. When the mail server refuses a new
* relay, the setup goes back to the one in use; the relay field must show
* that one again rather than the refused address, since the device list asks
* the relay in the field about each device and a revoke goes to it. With no
* relay in use the typed one stays, so the user can correct it.
*/
export async function enablePushFromSettings(params: PushSetupParams): Promise<PushEnableOutcome> {
const accountId = jmapClient.username && jmapClient.serverUrl
? generateAccountId(jmapClient.username, jmapClient.serverUrl)
: null;
let error: unknown = null;
try {
await setupPushNotifications(params);
} catch (err) {
error = err;
}
const typed = (params.relayBaseUrl ?? DEFAULT_RELAY_BASE_URL).replace(/\/+$/, '');
return {
error,
relayBaseUrl: (await getStoredRelayBaseUrl()) ?? typed,
enabled: accountId ? await isPushEnabledForAccount(accountId) : false,
};
}
/**
* Re-apply an account's push types to its subscription after device sync was
* turned on or off for it (#34). The account the client serves goes through