fix: stop the date and time pickers offering times past the server's hold limit

This commit is contained in:
Linus Rath
2026-09-24 19:10:43 +02:00
parent 7e598c5bc9
commit 191f4f32cd
4 changed files with 30 additions and 2 deletions
+11
View File
@@ -409,6 +409,17 @@ describe('JMAPClient', () => {
expect(client.undoSendHold(30)).toBeUndefined();
});
it('gives the latest time a send can be held until, for the date pickers', async () => {
global.fetch = mockFetch([{ status: 200, json: withSubmission(2_592_000, true) }]) as any;
await client.connect('https://mail.example.com', 'user', 'pass');
const now = Date.UTC(2026, 8, 24, 12, 0, 0);
expect(client.latestHoldDate(undefined, now)?.toISOString()).toBe('2026-10-01T12:00:00.000Z');
global.fetch = mockFetch([{ status: 200, json: MOCK_SESSION }]) as any;
await client.connect('https://mail.example.com', 'user', 'pass');
expect(client.latestHoldDate(undefined, now)).toBeUndefined();
});
it('parses only hold-limit rejections', () => {
expect(parseHoldLimit('Server rejected MAIL-FROM: 501 5.5.4 Requested hold time exceeds maximum of 604800 seconds.')).toBe(604_800);
expect(parseHoldLimit('Server rejected MAIL-FROM: 550 nope')).toBeNull();
+9
View File
@@ -1087,6 +1087,15 @@ export class JMAPClient {
return max;
}
/**
* The latest time a send from `accountId` can be held until, or undefined
* when it can't be held. Caps the "Pick date & time" pickers.
*/
latestHoldDate(accountId?: string, now = Date.now()): Date | undefined {
if (!this.hasDelayedSend(accountId)) return undefined;
return new Date(now + this.getMaxDelayedSend(accountId) * 1000);
}
/**
* Remember the hold limit a rejected submission named, so the pickers only
* offer times the server accepts for the rest of the session.
+5 -1
View File
@@ -1895,7 +1895,9 @@ export default function ComposeScreen({ route, navigation }: Props) {
}, [t, scheduleSheetOpen]);
const startCustomPicker = () => {
customDraftRef.current = new Date(Date.now() + 3600 * 1000);
// An hour ahead, or the latest time the server can hold it if sooner.
const latest = jmapClient.latestHoldDate()?.getTime() ?? Infinity;
customDraftRef.current = new Date(Math.min(Date.now() + 3600 * 1000, latest));
setScheduleSheetOpen(false);
setCustomStage(Platform.OS === 'ios' ? 'datetime' : 'date');
};
@@ -2516,6 +2518,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
mode="datetime"
display="spinner"
minimumDate={new Date()}
maximumDate={jmapClient.latestHoldDate()}
onChange={onCustomPickerChange}
/>
<View style={styles.modalActions}>
@@ -2542,6 +2545,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
mode={customStage === 'time' ? 'time' : 'date'}
display="default"
minimumDate={customStage === 'date' ? new Date() : undefined}
maximumDate={customStage === 'date' ? jmapClient.latestHoldDate() : undefined}
onChange={onCustomPickerChange}
/>
)}
+5 -1
View File
@@ -201,7 +201,9 @@ export default function ScheduledScreen({ navigation }: Props) {
}, [t, rescheduleFor]);
const startCustomPicker = () => {
customDraftRef.current = new Date(Date.now() + 3600 * 1000);
// An hour ahead, or the latest time the server can hold it if sooner.
const latest = jmapClient.latestHoldDate(rescheduleFor?.accountId)?.getTime() ?? Infinity;
customDraftRef.current = new Date(Math.min(Date.now() + 3600 * 1000, latest));
setCustomStage(Platform.OS === 'ios' ? 'datetime' : 'date');
};
@@ -375,6 +377,7 @@ export default function ScheduledScreen({ navigation }: Props) {
mode="datetime"
display="spinner"
minimumDate={new Date()}
maximumDate={jmapClient.latestHoldDate(rescheduleFor?.accountId)}
onChange={onCustomPickerChange}
/>
<View style={styles.cardActions}>
@@ -396,6 +399,7 @@ export default function ScheduledScreen({ navigation }: Props) {
mode={customStage === 'time' ? 'time' : 'date'}
display="default"
minimumDate={customStage === 'date' ? new Date() : undefined}
maximumDate={customStage === 'date' ? jmapClient.latestHoldDate(rescheduleFor?.accountId) : undefined}
onChange={onCustomPickerChange}
/>
)}