Files
webmail/lib/__tests__/jmap-delayed-send-limit.test.ts
Linus Rath bab19898d9 fix: cap scheduled send at the 7 days Stalwart actually accepts
Stalwart advertises `maxDelayedSend` as a fixed 30 days, but its MTA
rejects any hold beyond `futureRelease`, 7 days by default. Scheduling
a message 8 to 30 days ahead passed the picker and then failed with
"501 5.5.4 Requested hold time exceeds maximum of 604800 seconds".

On Stalwart the advertised 30 days now counts as 7. When a submission
is still rejected for its hold time, the limit from the error message
replaces it for the session, and the composer shows the translated
"later than the server allows" message instead of the raw SMTP reply.
2026-09-22 20:31:53 +02:00

50 lines
2.1 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { JMAPClient, ScheduleTooLateError, parseHoldLimit } from '../jmap/client';
const THIRTY_DAYS = 30 * 24 * 60 * 60;
const SEVEN_DAYS = 7 * 24 * 60 * 60;
function clientWith(maxDelayedSend: number, stalwart: boolean): JMAPClient {
const client = new JMAPClient('https://jmap.example.com', 'user@example.com', 'pass');
const accountCapabilities: Record<string, unknown> = {
'urn:ietf:params:jmap:submission': { maxDelayedSend, submissionExtensions: { FUTURERELEASE: [] } },
};
if (stalwart) accountCapabilities['urn:stalwart:jmap'] = {};
Object.assign(client, {
accountId: 'a1',
capabilities: { 'urn:ietf:params:jmap:submission': {} },
session: {
primaryAccounts: { 'urn:ietf:params:jmap:submission': 'a1' },
accounts: { a1: { accountCapabilities } },
},
});
return client;
}
describe('scheduled send limit', () => {
it('clamps the fixed 30 days Stalwart advertises to its 7-day hold limit', () => {
expect(clientWith(THIRTY_DAYS, true).getMaxDelayedSend()).toBe(SEVEN_DAYS);
});
it('trusts the advertised limit on other servers and other values', () => {
expect(clientWith(THIRTY_DAYS, false).getMaxDelayedSend()).toBe(THIRTY_DAYS);
expect(clientWith(3600, true).getMaxDelayedSend()).toBe(3600);
});
it('learns the limit from a rejected submission', () => {
const client = clientWith(THIRTY_DAYS, false);
const reject = () => (client as unknown as { throwIfHoldTooLong(e: { description?: string }): void })
.throwIfHoldTooLong({
description: 'Server rejected MAIL-FROM: 501 5.5.4 Requested hold time exceeds maximum of 172800 seconds.',
});
expect(reject).toThrow(ScheduleTooLateError);
expect(client.getMaxDelayedSend()).toBe(172800);
});
it('parses only hold-limit rejections', () => {
expect(parseHoldLimit('501 5.5.4 Requested hold time exceeds maximum of 604800 seconds.')).toBe(604800);
expect(parseHoldLimit('Server rejected MAIL-FROM: 550 nope')).toBeNull();
expect(parseHoldLimit(undefined)).toBeNull();
});
});