mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
fix: restart push verification when the mail server skipped it after a quick off and on
This commit is contained in:
@@ -472,6 +472,41 @@ describe('enableWebPush', () => {
|
||||
// Registrations made before the client learned about the delivery filter are
|
||||
// repaired in the background on app start - nobody should have to find the
|
||||
// settings toggle to stop the spam pushes.
|
||||
describe('enableWebPush when the verification does not come', () => {
|
||||
// The relay's poll stays empty until `arrivesAfterTouch` calls come in, like a
|
||||
// Stalwart that skipped the verification until the subscription is touched.
|
||||
function installSilentRelay(codeOnceTouched: () => boolean) {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
const json = (body: unknown) => ({ ok: true, status: 200, json: async () => body }) as Response;
|
||||
if (url.endsWith('/api/push/vapid-public-key')) return json({ publicKey: 'QUJD' });
|
||||
if (url.includes('/api/push/verify/')) return json({ verificationCode: codeOnceTouched() ? 'late-code' : null });
|
||||
return json({});
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
}
|
||||
|
||||
afterEach(() => { vi.useRealTimers(); });
|
||||
|
||||
it('touches the subscription after the server\'s minute, which makes it send the code', async () => {
|
||||
vi.useFakeTimers();
|
||||
const client = makeClient([]);
|
||||
installSilentRelay(() => (client.updatePushSubscription as ReturnType<typeof vi.fn>).mock.calls.length > 0);
|
||||
|
||||
const run = enableWebPush({ client, relayBaseUrl: RELAY, inboxOnly: false });
|
||||
const settled = run.then(() => 'done', (error: Error) => error.message);
|
||||
await vi.advanceTimersByTimeAsync(58_000);
|
||||
expect(client.updatePushSubscription).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
|
||||
expect(client.updatePushSubscription).toHaveBeenCalledTimes(1);
|
||||
expect((client.updatePushSubscription as ReturnType<typeof vi.fn>).mock.calls[0][0]).toBe('push-new');
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
expect(await settled).toBe('done');
|
||||
expect(client.verifyPushSubscription).toHaveBeenCalledWith('push-new', 'late-code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resyncWebPush when the relay changed (#903)', () => {
|
||||
afterEach(() => setNoThirdPartyTraffic(false));
|
||||
|
||||
|
||||
+23
-6
@@ -632,17 +632,32 @@ async function relayReportsDead(
|
||||
return (await relayStatusFor(relayBaseUrl, subscriptionId)) === 'inactive';
|
||||
}
|
||||
|
||||
// Stalwart rate-limits PushVerification per account (60 s by default).
|
||||
const VERIFICATION_RESEND_AFTER_MS = 62_000;
|
||||
const VERIFICATION_TIMEOUT_MS = 80_000;
|
||||
|
||||
async function pollVerificationCode(
|
||||
relayBaseUrl: string,
|
||||
subscriptionId: string,
|
||||
askAgain: () => Promise<unknown>,
|
||||
): Promise<string> {
|
||||
// Stalwart per-account rate-limits PushVerification posts (default 60s).
|
||||
// If there are leftover unverified subscriptions on the account, our new
|
||||
// one queues up behind them - so we wait long enough to clear one verify
|
||||
// window even in the unlucky case.
|
||||
const timeoutAt = Date.now() + 75_000;
|
||||
// If there are leftover unverified subscriptions on the account, our new one
|
||||
// queues up behind them, so we wait long enough to clear one verify window.
|
||||
//
|
||||
// Stalwart hands out the lowest free subscription id, so one destroyed and
|
||||
// created again within the window (off and on again, or a retry) gets the same
|
||||
// id. It takes that for a repeat and drops the verification for good, on 0.16.27
|
||||
// and 1.0 alike (push.rs: `verified_id == subscription.id`). Any change to the
|
||||
// subscription after the window makes it send one.
|
||||
const startedAt = Date.now();
|
||||
const timeoutAt = startedAt + VERIFICATION_TIMEOUT_MS;
|
||||
let askedAgain = false;
|
||||
let delay = 400;
|
||||
while (Date.now() < timeoutAt) {
|
||||
if (!askedAgain && Date.now() - startedAt >= VERIFICATION_RESEND_AFTER_MS) {
|
||||
askedAgain = true;
|
||||
await askAgain().catch(() => undefined);
|
||||
}
|
||||
const res = await fetch(
|
||||
buildRelayUrl(relayBaseUrl, `/api/push/verify/${encodeURIComponent(subscriptionId)}`),
|
||||
);
|
||||
@@ -866,7 +881,9 @@ export async function enableWebPush(
|
||||
const serverAssignedId = created.result;
|
||||
rememberRefusedEmailPushAccounts(accountId, refusedBefore, emailPush, created.emailPush);
|
||||
|
||||
const verificationCode = await pollVerificationCode(relayBaseUrl, deviceClientId);
|
||||
const verificationCode = await pollVerificationCode(relayBaseUrl, deviceClientId, () =>
|
||||
params.client.updatePushSubscription(serverAssignedId, { expires: expiresFromNow(SUBSCRIPTION_EXPIRES_DAYS) }),
|
||||
);
|
||||
await params.client.verifyPushSubscription(serverAssignedId, verificationCode);
|
||||
localStorage.setItem(subIdKey, serverAssignedId);
|
||||
localStorage.setItem(relayBaseKey(accountId), relayBaseUrl);
|
||||
|
||||
Reference in New Issue
Block a user